States and guarantees
States
Section titled “States”| Status | Meaning |
|---|---|
idle |
Never run, or reset. |
waiting |
Asked to run, but an input is missing, errored, or hasn’t run yet. Runs by itself once the input arrives. |
running |
run was called and hasn’t settled. |
done |
The last run resolved. state.result holds what run returned. |
skipped |
The last run returned SKIP / skip(reason), or an input was skipped and the skip spread here (0.0.1, see Skipping). state.skip is { source, reason }. |
error |
The last run threw or rejected (state.error), or the node is on a dependency cycle (state.cycle === true). |
Every state also carries version (settled runs), runId (started runs) and stale (a manual node whose inputs or
definition changed since its last run).
Things the table doesn’t say:
- There is no “queued” status. A queued node keeps showing its previous status (often
done) until it startsrunning. If a UI needs a “queued” badge, track your own calls. - “Never run” and “asked to run” differ. A node nobody asked to run stays
idlewhen an input fails. A node that was queued (byrun,runAllor an upstream change) becomeswaitinginstead, and runs once its input recovers. waitingresolves only when an input settles (done, orskipped, which then spreads). Adding a missing input withset()doesn’t wake its dependents; running that input does.versioncounts settled runs, errors and the node’s own skips included. A skip spreading from an input isn’t a run, so it leavesversionalone.runIdalso moves when a run is superseded (including by a spreading skip), the node is markedwaiting, or the graph isreset(), so a late result can always be recognized as stale.resultis cleared on error (result: undefined) and byreset().- Listeners get a copy.
onChangeandsubscribereceive a snapshot of the state, andget(id)returns a copy too; mutating either changes nothing in the scheduler.
Guarantees
Section titled “Guarantees”- Glitch-free. A node never starts while anything upstream of it is queued or running. In a diamond (
a → b,a → c,b + c → d), changingarunsdonce, after bothbandc. - Newest run wins. Rerunning a node while a previous run is in flight aborts the previous run’s
AbortSignaland drops its result, even if it settles later. Write your side effects behindif (!signal.aborted). - Errors don’t cascade as errors. When a node fails, its dependents become
waiting, noterror; only the node that broke shows an error, and the dependents rerun on their own once it’s fixed. - Manual nodes stay put. A node with
autorun: falsekeeps its last result when inputs change, and is flaggedstale: trueuntil yourunit. - Cycles are refused, not looped. Every node on a cycle goes to
errorwithcycle: trueas soon as the edge is added, and comes back toidlewhen the cycle is broken. A self-dependency is a cycle. - Skips spread, they don’t stall. A node that returns
SKIPisskipped, and so is every dependent, without running, until one that declaredconsumesSkipdecides what to do. Nothing changes for a graph that never skips. See Skipping.
What “newest run wins” does not cover:
- It drops the result, not the work. The superseded
runkeeps going until it checkssignal.aborted(see Limitations and traps). - Write-then-return races are yours. In the quick start,
values.set(id, value)is guarded by the signal. Without the guard, a slow stale run can still overwrite the value you keep elsewhere, even though dataflow drops itsstate.result. Example 02 is that case.
What a manual node does and doesn’t do:
invalidate(id)on a manual node marks itstaleonly if it isdone. On anidlemanual node it does nothing.runAll({ all: false })skips manual nodes; plainrunAll()runs them too.run(id)runs any node, whatever itsautorunflag, and its autorun dependents follow.