blackbox 0.10.4
blackbox: ^0.10.4 copied to clipboard
State management with one law: output = compute(input, state). Boxes with private memory, an explicit dependency graph you can render as a map, built-in persistence and caching. No code generation.
0.10.4 (docs) #
- Field corrections to the Shared truth chapters, from the app that
applied 0.10.3 whole:
.latereadiness is decided by the output type: nullable output → ready immediately with null (null is a value); consumers only wait on a non-nullable output withoutinitialValue.- The driving module takes the shared truth as a required
constructor parameter — a
?? PremiumBox.late(...)default is a silent mode switch that can split the app into two truths without tripping the one-declarer guard. updateInputForTestis the canonical way to move a shared truth in tests: the test plays the driving graph.- The "typed input for effects" pattern rewritten as a warning: its own canonical example was deleted in the field once every field failed the ring criterion. Such a box is scaffolding with a demolition date, not architecture.
0.10.3 #
- One declarer per box: declaring a box (
add/addMultiBox) on a second live graph now throws — declaring means driving the input and owning dispose, and the law allows one writer. Reading a foreign box viawhenReadyremains a subscription and stays legal. Re-declaring after the previous owner's dispose is allowed. Closes the API gap found by the "one fact, three graphs" field report. - ARCHITECTURE.md grew the missing chapters, all field-tested twice: the top floor (shared truth lives above the modules), shared-truth lifecycle (created above / driven by one / read by all, with the reader-nature table: graph → wire, non-graph → listen, other process → disk), feedback rings through memory, trigger-in-input vs condition-in-run, the coordinator (decision-as-state), time is delivered never read, "don't know" is a value, debug is data, no optimism, loud emptiness, external readers, and what a finished app map looks like.
0.10.2 #
- Ownership fix:
graph.dispose()now disposes only what was declared on the builder (add/addMultiBox). Sources registered lazily —whenReadyon another graph's box — are unsubscribed, never disposed: they belong to whoever declared them. Cross-graph reads are now safe by construction (field report from a two-graph catalog/player app that had to route them through manual subscriptions). - ARCHITECTURE.md: three new field-proven rules — the three-roles
question (cache / truth / small thing of its own), honest slot keys
(if the answer depends on a value, the value is in the key; keys hold
only things with
==), and the typed-input-for-effects pattern.
0.10.1 #
- Docs now ship inside the package:
doc/MODEL.md(the law),doc/ARCHITECTURE.md(the seven-floor application pattern),doc/MIGRATION.md(0.8/0.9 migration + script) — readable offline from the pub cache, greppable by tooling. - README: "Coming from Riverpod" and "Coming from MobX" mapping tables — port the roles (truth → cells, fetch → Cache, formula → compute), not the lines.
- Fixed a stale
Cachedocstring that still showed the pre-0.10 spelling.
0.10.0 — the subtraction release #
Breaking. The road to 1.0 is subtraction; this release is most of it.
The law is now the whole signature: output = compute(input, state).
previousis gone from everycomputeand theMultiBoxcompute:compute(input),compute()for no-input boxes,void compute(input)for multiboxes. Three field reports showedpreviouslying (two computes in one frame) and every real box that needed "what did I show before" ended up owning that memory anyway. Migration: delete the parameter; if you actually used it, hold the memory in astate(...)cell (boxes) or a private field (multiboxes).- Deprecated legacy deleted:
Persisted,AsyncPersisted,AsyncManagedCache,ManagedCachemixins,ValueStateBox/valueBox,LateAsyncBox, and the deprecatedMultiBox.track()alias. Their replacements (state(persist:),CachedBox/CachedValueBox+Cache,AsyncBox.late,connect) have been the documented spelling since 0.9.x. On-disk data is untouched — the envelope format and keys are the same, so persisted values written by old mixins are read by state cells and caches as before. - docs/MODEL.md status updated: the released API now matches the contract verbatim.
0.9.5 #
graph.box<T>()— type lookup for places wherecontext.boxis unavailable (background isolates, tests, composition code). Loud by design: throws on a missing type and on twins. Promoted from a hand-rolled extension in a production app.MultiBox.inputgetter — the current input, mirroringBox.input: readable from buttons, listeners and helpers (insidecomputeit already holds the new input). Kills the manual mirror field (_streamUrl) every real multibox grew.docs/ARCHITECTURE.md— the seven-floor application pattern (runtime / skeleton / module / leaf / effects / projection / UI), the resource ladder, the events-as-numbered-values pattern, ports and the headless graph. Distilled from a production radio app.- Feature freeze: from here to 1.0 the plan is subtraction only —
remove
previousfrom compute signatures, drop the deprecated legacy, add lints that guard the law.
0.9.4 #
addMultiBoxno longer requiresinput:— omit it for a self-driven module (MultiBox<void>or nullable input) that lives off its own streams: the graph delivers a singlenullinput on the first pump, socomputeruns exactly once to start the module..addMultiBox(billing)replaces the last dummy in the system,input: (d) {}. Omittinginput:for a non-nullable input type asserts loudly.
0.9.3 #
graph.own(release)— the graph becomes the owner of resources created for it (HTTP clients, databases, files): they are released indispose(), after every box has stopped, in reverse registration order. Kills the composition-wrapper class whose only job was to remember twoclose()calls:buildApp(ctx)..own(client.close). Own only what was created for the graph — injected gateways belong to their creators. Extends the existing rule (graph dies → everything its own dies) to non-box resources; no new concept.MultiBox.track()is deprecated and gone from the public vocabulary. One resource rule for the whole library: streams feeding cells go throughconnect()(auto-released before the next compute and on dispose, as before); everything else lives in a field and is released at the top ofcomputeand indispose— same as in ordinary boxes. Also removes the name collision with UI read-tracking.
0.9.2 #
Box.late(initialValue:)/AsyncBox.late(initialValue:)(andCachedBox.late/CachedValueBox.late) — create a box without an input; the graph delivers the first one. Kills the dummy-input problem: no moreMyBox(input: (config: null, ...))seeds in composition roots. Until the first input a sync late box showsinitialValue(ornullfor nullable outputs) or has no output yet (dependents wait,valuethrows a clearStateError); an async late box showsAsyncLoading/AsyncData(initialValue). Field report from a real radio app's ~280-line AppBox.graph.boxes— boxes declared on the builder (add/addMultiBox), in declaration order. The provider list in one line:BoxProvider.multi(boxes: graph.boxes, child: ...)— no hand-rolledexportslist to keep in sync.LateAsyncBoxis deprecated — extendAsyncBoxand use thesuper.late(initialValue:)constructor; same behavior, one class fewer.
0.9.1 #
- Added
ProvidableBox— the common marker of everything deliverable through a BoxProvider.OutputSourceandMultiBoximplement it, so a composite (e.g. a player) goes intoBoxProviderand comes out viacontext.box<PlayerBox>()like any other box — no second delivery mechanism (custom InheritedWidget) needed. Field report from a real audio-player app.
0.9.0 #
- The model (docs/MODEL.md): a box is
output = compute(input, state)— input is what it is given, state is what it remembers, output is what it shows; one writer per thing. - Added
state(...)/StateCell<T>— declared box memory: a write re-runs compute and emits (equal writes are no-ops);persist:binds a global storage slot,persistFor: (input) => keybinds a slot per input (re-slots on input change — no cross-slot leaks by construction);codec:overrides the registry locally. - Added
CachedBox/NoInputCachedBox— an async box whose compute is a cachedfetch. OneCache(ttl: ..., persist: 'menu')declaration replaces theAsyncPersisted+AsyncManagedCachemixin pair; the contract lives in the names (fetch= "go get fresh, the cache decides when";computeon a cached box is sealed). In-memory TTL by default;persist:adds a disk slot (instant cold start, disk timestamp drives expiration);persistFor: (input) => keykeeps one slot per input;refresh()/invalidateCache()are now available on every async box. Plain async boxes cannot declare a cache — only the named classes can. Mixins remain as the legacy spelling with identical semantics and on-disk format. - Added protected
inputgetter on sync and async boxes — actions read the current input instead of caching it into fields inside compute. - Cells work on async boxes too: a write re-runs the async compute
(emitting
AsyncLoadingwith the previous data first) — the search-box pattern;persist:/persistFor:behave as on sync boxes. - Added
CachedValueBox— the sync twin ofCachedBox(theManagedCachesemantics, declaratively): the value is always readable synchronously starting frominitialValue/the disk slot,fetchruns in the background, fetch errors are swallowed.refresh(),invalidateCache(), TTL-on-access included. action(...)now batches: cell writes inside it emit once at the end (on async boxes — for the synchronous part of the body).codecForfalls back to identity for nullable primitives and to an assignable registered codec (e.g. aServicecodec servesService?).- Breaking: removed
FlowBox.stategetter (collided with thestate(...)declarator) — usevalue. - Graph tools:
graph.settled()— resolves when synchronous propagation is done; the test-friendly replacement for microtask-flushing loops.graph.toMermaid()— renders the dependency graph as a Mermaid flowchart (edges recorded during pumps; multibox ownership dashed).- Pump-storm detector: a dependency cycle (or a box emitting a never-equal value each recompute) no longer freezes the isolate — after a bounded number of consecutive pump cycles the graph stops pumping and throws a diagnostic StateError.
- Deprecated the legacy spelling (removal before 1.0):
Persisted,AsyncPersisted,AsyncManagedCache,ManagedCache— each replaced by one declaration (state(persist:),CachedBox,CachedValueBox). Example apps ported; on-disk data stays compatible. - README rewritten around the model (three things, one law, three words); migration table from the 0.8 mixins included.
MultiBox.connect(stream, cell, {map})— the one-word form of the dominant compute pattern (stream → output cell): subscribes, maps, dispatches, and auto-releases on the next input cycle and on dispose. Asserts on a type mismatch whenmapis omitted.MultiBox.child(initial)now declares aChildCell— the outward twin of a state cell: an ordinaryOutputSourcethe graph/UI observe, with no public setter (only the owning multibox writes viadispatch); distinct by default; disposed with the multibox.ValueStateBox/valueBoxare deprecated (child(initial)replaces the composite-leaf use;state(...)replaces the internal-memory use);dispatchAsyncremoved — an async child belongs in the graph, fed by a multibox output.
0.8.0 #
- Breaking / Fixed (persistence):
- Restore precedence is now uniform: a disk-cached value wins over
initialValueforPersistedandAsyncPersisted, matchingManagedCache.initialValueis only the first-boot fallback. PreviouslyinitialValuesilently shadowed the persisted value. - A persist-key change (
persistKeyForreturning a new key after an input change) now re-initializes the box in the new slot:onFirstComputeruns again with the new slot's cached value (ornullfor an empty slot), and the old slot's value no longer leaks into — or gets saved under — the new key. Previously a key switch could expose and persist the previous slot's data (e.g. one user's cart saved under another user's key). AsyncPersistedsevers the old slot's state on rekey: it never appears aspreviousDatain loading/error outputs of the new slot.ManagedCacheon a slot switch adopts the new slot's cached value, or falls back to the constructorinitialValuefor an empty slot; an in-flight fetch for the previous input is invalidated and a fetch for the new input is started.ValueStateBoxcomposed withPersistednow actually restores: the persisted value becomes the effective initial input. The documentedThemeBoxpattern previously never restored from disk.
- Restore precedence is now uniform: a disk-cached value wins over
- Added:
MultiBox<I>— composite black box: a single graph-driven input and N observable child cells. Children are owned viachild(...)(late final status = child(valueBox(...))), driven only through the guardeddispatch/dispatchAsyncbridge (asserts ownership in debug), and disposed together with the multibox.track(...)registers per-input-cycle cancels (auto-released before everycomputeand on dispose). Wire withGraphBuilder.addMultiBox(mb, input: ..., onError: ...)—onErrormirrorsadd.ValueStateBox<T>/valueBox<T>— identity-compute leaf cell. Distinct by default: pushing a value equal (==) to the current one is a no-op — no listener notifications, no graph pump. Native platform streams re-emit identical values constantly; distinct cells absorb that noise at the source. Passdistinct: falsefor values mutated in place.resolvePreviousForInput(I input, O? previous)— protected hook on sync and async boxes that maps the effectivepreviousvalue when a new input arrives; the persistence mixins use it for slot re-initialization.
- Hardening:
- Box disposal is idempotent and final: after dispose a box ignores input
pushes and never notifies listeners again (late native events and
in-flight async completions are swallowed); async dispose invalidates
in-flight computes. Graph and MultiBox both go through the same
idempotent path, so combining
mb.dispose()withgraph.dispose()is safe.
- Box disposal is idempotent and final: after dispose a box ignores input
pushes and never notifies listeners again (late native events and
in-flight async completions are swallowed); async dispose invalidates
in-flight computes. Graph and MultiBox both go through the same
idempotent path, so combining
0.7.1 #
- Added:
DependencyResolver.whenReadyOrNull<T>(source)— returns the source value when ready, ornullotherwise, without skipping the pump cycle. Use when a dependent box accepts an optional input and should compute regardless of whether the upstream has produced data yet. ComplementswhenReady<T>.
0.7.0 #
- Breaking:
- Renamed the async cache mixin
ManagedCache→AsyncManagedCache. Updatewith AsyncPersisted<...>, ManagedCache<...>towith AsyncPersisted<...>, AsyncManagedCache<...>. AsyncManagedCacheis now relaxed toon _AsyncBoxBaseand works standalone (in-memory TTL) withoutAsyncPersisted.
- Renamed the async cache mixin
- Added:
- New sync
ManagedCache<I, O>mixin onBox<I, O>— sync always-available value backed by an asyncfetch(input). Provides TTL, background refresh on access,refresh(),invalidateCache(), fail-open error handling, and automatic re-fetch on input change. Compose withPersistedto persist the cached value. - Sync
ManagedCachecomposed withPersistedprefers the disk-cached value overinitialValueon boot —initialValuebecomes the empty- disk fallback rather than a default that overwrites cached state.
- New sync
0.6.0 #
- Breaking:
- Removed
listenSync()andlistenAsync()— uselisten()instead. - Renamed
SyncOutput→SyncData. - Renamed
prepare()→onFirstCompute(). - Renamed
shouldEmitLoadingBeforeCompute()→shouldEmitLoading(). - Removed
Runtimeclasses — state management inlined into box base classes. - Sync
action()now returnsvoidinstead ofFuture<void>. AsyncBox.lateinit()replaced byLateAsyncBoxclass.
- Removed
- Added:
listen()accepts{bool skipFirst}to skip the immediate callback with current state.beforeCompute()hook on sync boxes (symmetric with async).Persistedmixin now supports dynamic rekey when input changes (symmetric withAsyncPersisted).
- Changed:
LateAsyncBoxextracted into its own file.- Async box base simplified: removed
_initialized,_pendingListeners,_requireInput.
0.5.1 #
- Fixed:
ManagedCacheno longer starts duplicate stale-cache refreshes while the first refresh is still in flight.refresh()now completes after the underlying async recompute finishes, not just after it is scheduled.
0.5.0 #
- Breaking:
- Removed
persistKeyparameter from all box constructors (Box,NoInputBox,AsyncBox,NoInputAsyncBox). - Removed
CachedAsyncSupportmixin — replaced byManagedCache. - Removed
persistenceKey,persistedAt, andclearPersistedValue()from_AsyncBoxBase— use mixin members instead.
- Removed
- Added:
Persisted<I, O>mixin for sync boxes — addwith Persisted<I, O>and implementpersistKeyFor(I input).AsyncPersisted<I, O>mixin for async boxes — save/restore only.ManagedCache<I, O>mixin (onAsyncPersisted) — TTL, stale-while-refresh,refresh(),invalidateCache().- Lifecycle hooks on box base classes:
resolveInitialValue(),onInitialized(),beforeCompute().
- Changed:
- Persistence is fully extracted from box base classes into opt-in mixins.
- Cache management is a separate layer:
AsyncPersistedfor save/restore, addManagedCachefor TTL and refresh controls. - Persist key is resolved once at init and never changes — to switch keys, destroy the box and create a new one.
0.4.4 #
- Add
CachedAsyncSupportfor persisted async cache with TTL-based lazy refresh. - Let async boxes control loading emission via
shouldEmitLoadingBeforeCompute(...). - Persist timestamps alongside cached values while keeping legacy raw cache reads compatible.
0.4.3 #
- Add
OutputSource.valueOrNullandOutputSource.requireValuefor ergonomic ready-value access. - Improve not-ready
StateErrormessages to include the current output state.
0.4.2 #
- Add
GraphBuilder.addEffect(...)for explicit fire-and-forget graph effects withcurrentandpreviousinputs.
0.4.1 #
- Add
awaitNextValueAfterAction(...)as a@visibleForTestinghelper for box and graph assertions. - Refresh persistence and setup documentation across the package README.
0.4.0 #
- Breaking:
- Removed
Box.lateinit()constructor — sync boxes always require input at construction. UseAsyncBox.lateinit()for deferred initialization. _SyncBoxBase._runtimeis nowlate final(non-nullable)
- Removed
0.3.2 #
- Fixed:
AsyncBox.lateinit()now returnsAsyncLoadingfromoutputandlistenbefore initialization (instead of throwingStateError)- Pending listeners are automatically flushed to the runtime when the box receives its first input
0.3.1 #
- Fixed:
Graph.start()no longer crashes onlateinitboxes — defers subscription until runtime is created by first pump cycle
0.3.0 #
- Breaking:
when()loading callback signature:() → R→(T? previousData) → Rwhen()error callback signature:(Object, StackTrace?) → R→(Object, StackTrace?, T? previousData) → R
- Added:
AsyncLoading.previousData— carries last known value during refreshAsyncError.previousData— carries last known value on error after refresh
0.2.0 #
- Breaking:
- Removed
LazyBox— usepersistKeyparameter on Box/AsyncBox constructors - Removed
computeValue()—NoInputBoxandNoInputAsyncBoxnow usecompute()directly - Renamed
dependencies:parameter toinput:in Graph.add() - Renamed
d.ready()tod.whenReady()in DependencyResolver - Removed
d.output()from DependencyResolver (usebox.outputdirectly) - Removed public
inputgetter from Box/AsyncBox
- Removed
- Added:
prepare(I input, O? previous)lifecycle hook — called once before first computedispose()lifecycle hook — called by Graph.dispose() for resource cleanuppersistKeyparameter on Box/AsyncBox constructors for built-in persistenceBlackboxPersistence.registerCodec<T>()for global codec registry- Graph signal tracing:
build(trace: true)for console output,onTrace:for custom handler PumpTrace/BoxTracedata classes for programmatic trace access
- Changed:
- Box hierarchy refactored: shared
_SyncBoxBase/_AsyncBoxBaseinternal base classes NoInputBox<O>andNoInputAsyncBox<O>are now independent fromBox<I,O>/AsyncBox<I,O>(both extend shared base)- All box types use
compute()as the override method name
- Box hierarchy refactored: shared
0.1.0 #
- Breaking:
Connector->GraphConnectorBuilder->GraphBuilderconnect(...)->add(...)connectWith(...)->addWith(...)PipelineBuilder.addWithDependencies(...)->addWith(...)FlowBoxBuilder()is now created viaFlowBox.builder()
- Changed:
- Updated docs, tests, and examples to the new graph/flow builder API
0.0.7 #
- Renamed:
- StateObserver -> FlowBox
- Breaking:
FlowBox<S>now requiresS extends FlowState
- Added:
FlowBoxBuilder.onLoading(...)andonError(...)for reacting toAsyncLoadingandAsyncError
- Changed:
- FlowBox is now a sync box without input (
Box<O>) onLoading(...)andonError(...)are compile-time restricted to async sources only
- FlowBox is now a sync box without input (
0.0.4 #
- Renamed:
- Graph -> Connector
- Flow -> StateObserver
0.0.3 #
- Added GraphBuilder
- Added Pipeline of Boxes
0.0.2 #
- Updated docs
0.0.1 #
- Initial release