Skip to content

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.

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 in onChange) and return early if you need it.
  • What run returns is kept. It becomes state.result and stays referenced until the next run of that node, an error, or reset(). 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.

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.

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.

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.

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.

The graph is in-memory and not shared across tabs or machines.

  • 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 its deps changed. Call invalidate(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 run returns a skip or onChange reports skipped.
  • SKIP can’t cross postMessage. It’s a symbol; a Worker returns its own marker and run maps it to SKIP.
  • Unknown ids are ignored silently by run, invalidate and delete; get returns undefined.
  • 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.