This project has been succeeded by GraphReFly. New development happens at graphrefly-ts. npm install @graphrefly/graphrefly
Skip to content

March 24, 2026

Five Primitives, Two Tiers, Zero Schedulers

Arc 4, Post 13 — Architecture v3: The Type 3 Breakthrough


The core API is intentionally small. Five names cover how data enters the graph, transforms, and exits:

PrimitiveRole
producerGeneric callbag source — emit, signal, lifecycle
stateErgonomic producer — set / update, TC39-friendly equals
operatorMulti-dep transform with full STATE + DATA handler
derivedoperator shaped as “computed store” sugar
effectTerminal sink — runs when deps resolve, no downstream store

dynamicDerived fits beside derived: same operator lineage, but dependencies are discovered at runtime via tracking reads — still a transform, not a sixth conceptual axis (source / transform / sink).

Two tiers: where STATE stops

  • Tier 1 — synchronous transforms, static dependency lists, full DIRTY / RESOLVED / DATA protocol. Use operator (or derived). Diamond resolution and bitmask logic live here.
  • Tier 2 — timers, promises, inner subscriptions, dynamic upstream. Use producer with autoDirty: true and imperative subscribe() inside the producer body. Tier-2 nodes start fresh DIRTY+DATA cycles per emission; they do not inherit upstream two-phase STATE the same way tier-1 nodes do.

The split is how we keep RxJS-shaped async operators without pretending they are the same animal as a pure map. Async boundaries are producer-shaped; sync graph logic stays operator-shaped.

Zero schedulers

There is no enqueueEffect, no global tick, no queueMicrotask layer deciding order. When all dirty deps of an effect have resolved (DATA or RESOLVED), the effect function runs inline, synchronously, in the same call stack as the resolution — same rule as in our architecture doc’s performance story: deterministic ordering, glitch-friendly batching, and no hidden microtask priority inversions.

For single-dep reactions that do not need DIRTY/RESOLVED bookkeeping, subscribe stays the lightweight DATA sink (see architecture §1.19).

One producer base

state is not a parallel implementation — it rides producer with defaults users expect (Object.is, set(same) no-op semantics). producer unifies “event stream,” “async boundary,” and “bare metal source” so we are not maintaining three competing source classes.

Further reading


Chronicle continues with Output Slot: How null->fn->Set Saves 90% Memory — our first Arc 5 deep dive.

Released under the MIT License.