Notification semantics
Commits and delivery
Updates run through the current middleware chain. A value different by Object.is is committed immediately. The store captures that value and current subscriptions in a notification record, then drains records synchronously in FIFO order. No timers, microtasks, or batching are involved.
An unchanged reference, a same-value updater, or middleware that does not call next produces no notification. Middleware runs in outer-to-inner order; ordinary delivery remains inside next, before middleware code following it.
Reentrant writes
If a listener commits 2 while delivering 1, remaining selectors receive 1 before receiving 2. Selectors evaluate the captured state and receive next/previous selection arguments. getState() always reads the latest committed state, even while an older record is being delivered. Plain subscribe callbacks receive no arguments and are invalidation signals.
New subscriptions do not receive records captured before registration. Unsubscribe is immediate and idempotent: an inactive subscription is skipped even if present in a captured record. Registering the same callback twice creates independent subscriptions.
FIFO delivery avoids recursive stack growth; it does not prevent logically infinite update loops. Listener-driven updates still need a terminating condition.
Errors
A selector error during registration propagates immediately and registers nothing. Errors from a selector, equality function, or listener during delivery are collected. Other active subscriptions and queued records still run. After draining, the outermost notification call throws an AggregateError containing original thrown values in order. The queue is cleared and remains usable for the next update.
The state is already committed and is not rolled back. A failed selector/equality evaluation retains its previous selection baseline; a listener failure occurs after that baseline advances. Updater and middleware errors outside notification delivery propagate directly.
Explicit writes
set(value) treats functions as values; update(updater) treats its argument as computation. Both use the same middleware chain as setState(valueOrUpdater).
Commit-aware integrations
Store.subscribeCommit(listener) receives StoreCommit<T> records containing
state, previousState, sequence, and source. The sequence starts at zero
before any change; getCommitSequence() advances only when a different value is
committed. Rejected or delayed middleware actions do not advance it until a commit.
Commit observers run before ordinary subscribers for each captured FIFO record. The captured subscription list, immediate unsubscription, duplicate registration ownership, and aggregated errors follow the ordinary notification contract. Observer errors do not prevent other observers or subscribers from receiving the commit and do not roll state back.
const restore = Symbol('restore');
const stop = store.subscribeCommit(({ state, previousState, sequence, source }) => {
record({ state, previousState, sequence, restored: source === restore });
});
store.replaceState(snapshot, restore);
stop();replaceState(value, source?) bypasses all middleware and commits a literal value,
including callable state. It still skips identical values and uses normal FIFO
notification delivery. A caller must perform any required validation before this
operation. The optional source belongs only to this replacement; user updates
triggered by its notifications have their own ordinary, undefined source.
These capabilities are on the concrete Store class. Existing structural
ReadableStore and StoreApi implementations do not need to add methods.