Limitations and traps
Traps first: the behavior you won’t guess from the type signatures. Each was checked against the 0.0.0 source, and the skip rules against 0.0.1’s.
It doesn’t store values
Section titled “It doesn’t store values”That is deliberate, but it has two consequences.
- “Rerun only if the value actually changed” (equality cut-off) is yours. Every settled run queues every autorun
dependent, even when it produced the same value as last time. Compare in your
run(or inonChange) and return early if you need it. - What
runreturns is kept. It becomesstate.resultand stays referenced until the next run of that node, an error, orreset(). Returning the value itself defeats the point when the value is large or must stay in another realm: return a preview or a token instead.
Cancellation is cooperative
Section titled “Cancellation is cooperative”A superseded run’s result is dropped and its signal aborts, but the scheduler can’t stop code that ignores the signal.
The signal aborts when the run is superseded, when the node is deleted, when it becomes waiting, when it lands on a
cycle, and on reset(). Check signal.aborted before every side effect, and pass signal on to anything that accepts
one (fetch, for example).
If you need a hard kill, run in something you can terminate (a Worker), terminate it, and call reset() after
restarting it. With andbox, a timeout that hard-kills the Worker takes every value with it; reset() then
runAll() recomputes them.
Listeners run inside the scheduler
Section titled “Listeners run inside the scheduler”onChange and subscribe listeners are called synchronously, from inside set(), run() and every settlement, so keep
them cheap (schedule rendering rather than doing it inline). One that throws is reported through reportError (in a
browser, the console’s uncaught-error path), like an EventTarget listener: the other listeners still run and scheduling
carries on.
No first-to-finish race
Section titled “No first-to-finish race”Glitch-freedom means a node with skip-consuming inputs runs once all of them settled, never on the first one. Make branches mutually exclusive and coalesce them instead; see Skipping.
Scans are linear
Section titled “Scans are linear”Finding dependents walks every node; that is fine for hundreds or a few thousand nodes (a notebook, a spreadsheet tab), not for a million-cell sheet.
One process
Section titled “One process”The graph is in-memory and not shared across tabs or machines.
Smaller traps
Section titled “Smaller traps”- A queued node keeps its previous status until it starts
running; there is no “queued” state. See States. set()on an existing node doesn’t rerun it, even when itsdepschanged. Callinvalidate(id).idle()resolves while paused, even with work queued.- A skip doesn’t clear your stored value. The scheduler never had it: drop it yourself when
runreturns a skip oronChangereportsskipped. SKIPcan’t crosspostMessage. It’s a symbol; a Worker returns its own marker andrunmaps it toSKIP.- Unknown ids are ignored silently by
run,invalidateanddelete;getreturnsundefined. - Run order within one flush follows queue order among nodes whose inputs are ready; only upstream/downstream order
is guaranteed. Use
order()if you need a deterministic topological order for something else.