caffeine 3.0.0
caffeine: ^3.0.0 copied to clipboard
A reactive microstore for Dart. Event-driven stateful stores, lazy derived state with automatic dependency tracking, glitch-free reactivity, and hierarchical scope-based lifecycle management.
3.0.0 — 2026-05-28 #
Breaking #
Event<void>shorthand:signal(source)replacessignal(source, null). The shadowing extension constrains the generic variant toT extends Object, so events with nullable payloads (Event<int?>) lose the call shorthand and must usesource.fire(event, value)directly.ctx.onnow takes aSource<E>, not just anEvent<E>.Source<T>is a new marker interface implemented by bothEvent<T>andStore<T>. You can react to another store's value changes the same way you react to an event:ctx.on(otherStore, (value) async* { yield ...; }). Each new value (post-flush) triggers the handler.- Stores are no longer callable as events.
someStore(source, value)does not compile — the firing extension is onEvent<T>only. Stores are immutable from outside; mutate them only through their own handlers. (Reading viasomeStore(source)still works.) Scope.read/StoreAcc.readno longer takelisten:. The parameter was meaningless outside derive bodies. The read API is now split:StateSource.read(node)has nolisten:,DerivedSource.read(node, {listen})does.ScopeandStoreAccimplement onlyStateSource, solisten: falseoutside a derive body is a compile error — not a runtime throw.- Operations on a disposed scope throw
StateError.read,fire,stream,fork,listenall checkisDisposedfirst. Event<T>is nowfinaland acceptsdebugLabel. Subclassing is no longer allowed.
Added #
Source<T>marker unifiesEvent<T>andStore<T>asctx.onsources (see above).ctx.disposeevent. Each accum store exposes a per-storeEvent<void> get disposeon its context that fires when the owning scope is disposed. Subscribe like any other event to clean up external resources:final timer = Timer.periodic(...); ctx.on(ctx.dispose, (_) async* { timer.cancel(); });- Multiple handlers per source.
ctx.on(event, ...)can be called more than once for the same source; all handlers run on every emission. Previously the second call silently overwrote the first. - Concurrency strategies for handlers.
ctx.on(source, handler, concurrency: ...)acceptsConcurrency.parallel(default — caffeine ≤ 2 semantics),drop,restart, andqueue. - Cycle detection. Direct or indirect self-references in
Store.derivethrowStateErrorinstead of overflowing the stack. - Debug labels. Optional
debugLabel:parameter onEvent(...),Store.derive(...), andStore.accum(...). Included intoStringand cycle-detection error messages. - Custom equality. Optional
equals: (a, b) => ...parameter onStore.deriveandStore.accumreplaces the default==for change detection. Scope.listen(event, handler)subscribes to events without owning a store. Returns aStreamSubscriptionfor cancellation.Scope.isDisposedplus introspection accessorsdebugBoundStores,debugBoundEvents,debugChildren.Store<T>.select((value) => slice)extension creates a derived projection inline.
Fixed #
Scope.stream(derived)now wires dependencies eagerly. Previously subscribers received no events until the derived store was also read; nowstreamforces evaluation.
2.0.0 #
- Event binding: passing an
Event<T>in scopeoverridesbinds it to that scope;fire()from the owning scope or any descendant broadcasts through the entire subtree, enabling global and semi-global event routing - Unbound events default to root: unbound events broadcast from the root scope, consistent with how unbound stores are globally accessible; bind an event to an intermediate scope to restrict its broadcast to that subtree
- Automatic scope promotion: derived stores are now placed in the deepest scope that owns their dependencies rather than the requesting scope, so all reads within a subtree share one instance
- Constant derived stores live on root: derived stores with no dependencies are promoted to the root scope, consistent with unbound accum stores
Event<T>now implementsStoreOverride(extracted tooverride.dartto avoid circular imports)
1.0.0 #
Initial release.
Store<S, E>— reactive state machine with pure update function and explicit effect streamsStateful<S>— lazy derived reactive value with automatic dependency tracking viaSnapshotScope— runtime that manages the reactive graph, dispatches events, executes effects, and controls store lifecyclesStoreOverride— transparent store replacement for dependency injection and testing- Scope forking for hierarchical store lifetime management
- External stream subscriptions via
Store'ssubscribeparameter - Glitch-free update compression: every
Statefulnode recomputes at most once per event cycle regardless of how many upstream dependencies changed