Observable value containers for reactive state. A Store<T> holds a single current value, broadcasts changes to all active consumers, and integrates with React Suspense out of the box. Stores suppress duplicate emissions using deep equality, so consumers only see genuine changes.
Concepts
Loading state — a store starts in a loading state represented internally by the NONE sentinel. Reading Store.value while loading throws a Promise (the store's internal DeferredSequence), which React Suspense catches and waits on. Reading Store.loading is safe and never throws.
Error state — setting Store.reason puts the store in an error state. Subsequent reads of .value throw that reason, which React error boundaries can catch.
Async iteration — Store<T> implements AsyncIterable<T>. Iterating with for await...of first emits the current value (if one exists), then emits each subsequent value as it changes. The iterator blocks between values using the store's internal DeferredSequence.
Duplicate suppression — deep equality is checked before emitting. Setting the same value twice only triggers one emission.
Starters — store.starter accepts a function that runs when the store has at least one active iterator and stops when none remain. Use this to wire up external subscriptions (e.g. a database realtime feed) that should only be active while something is listening.
Every store shares the same core: set .value, read it back (or for await it), and consumers see the change. The base Store page covers the full lifecycle; each subclass page covers its own helpers.
As an integration example, Store.through() bridges any AsyncIterable source into a store — it sets the store's value for each item yielded and re-yields it:
ts
import { Store, NONE } from "shelving/store";
const store = new Store<number>(NONE);
async function connect(stream: AsyncIterable<number>) {
for await (const _ of store.through(stream)) {
// store.value is updated on each iteration; any consumers re-render
}
}