Internals

  1. One update path
  2. Module map
  3. Layering rules
  4. Where to look when something breaks
  5. Budgets
  6. Test environment notes

These notes explain the core implementation. For the public contract, read concepts.md. This section covers internal mechanisms, invariants, and changes that can break them.

doc module what it covers
reconcile.md reconcile.ts + pointer.ts the structural diff, the op vocabulary, array splices
tracking.md track.ts + paths.ts + unwrap.ts recording proxies, path trees, patch intersection
graph.md graph.ts + watch.ts + queue.ts + system.ts sources, gates, mid-run publishes, scheduling
publication.md graph.ts (commit) + process.ts, wire/pump.ts discovery vs publication, the paths a change takes in, batching and retention
process.md process.ts + mailbox.ts, scope.ts, calls.ts, instrument.ts mailbox, drive loop, ownership, dispose ordering
registry.md registry.ts + key.ts key encoding, sharing, refcounting, eviction
dom.md dom/src/render.ts and its siblings regions, keyed LIS moves, rebinding, listeners, exit

One update path

The whole library is one pipeline with adapters on either end. Everything downstream of a yield is the same code whether the state lives in this tab or on a server:

Diagram
flowchart LR
    Y["yield next"] --> R["reconcile(prev, next)"]
    R --> P["patch<br/>(set / del / splice on JSON pointers)"]
    P --> A{"intersects this reader's<br/>recorded paths?"}
    A -->|no| S["reader sleeps"]
    A -->|yes| G["bump its gate"]
    G --> F["flush queue<br/>(one drain per microtask)"]
    F --> E["reader runs"]
    P -.->|"same patch, JSON-encoded"| W["wire → a remote reader"]

Keep two consequences in mind when changing this code:

Module map

Diagram
flowchart TD
    subgraph core["@nonchalant/core"]
        TY["types.ts<br/>the Process type surface"]
        RC["reconcile.ts + pointer.ts<br/>diff + apply"]
        TR["track.ts + paths.ts + unwrap.ts<br/>read recording, patch matching"]
        SY["system.ts<br/>alien-signals port; do not layer here"]
        GR["graph.ts + watch.ts + queue.ts<br/>source / gate / effect / flush"]
        PR["process.ts + mailbox / scope / calls / instrument<br/>mailbox + drive loop + ownership"]
        RG["registry.ts + key.ts<br/>get-or-spawn by name"]
        IX["index.ts<br/>spawn, derive, cell, mount"]
    end
    DOM["@nonchalant/dom<br/>VNode → DOM, keyed regions"]
    WIRE["@nonchalant/wire<br/>codec, connect, expose"]
    HOST["@nonchalant/host<br/>Node WebSocket host"]

    RC --> TR
    RC --> GR
    TR --> GR
    SY --> GR
    GR --> PR
    PR --> RG
    GR --> IX
    PR --> IX
    RG --> IX
    TY -.-> PR
    TY -.-> RG
    IX --> DOM
    IX --> WIRE
    RC --> WIRE
    WIRE --> HOST

Read bottom-up on first contact: reconcile → track → graph → process → registry. Each layer only knows about the ones below it.

Layering rules

These are the constraints that keep the design coherent; CLAUDE.md carries the full list.

Where to look when something breaks

symptom start here
a reader wakes too often, or not at all tracking.md for precision rules, then affects
a producer's supplied ops wake too little publication.md for the commit contract, then opAffects
a reader misses an update that landed mid-run graph.md for deferred operations and finalizeGates
a patch is wrong, or an apply throws reconcile.md for array trimming and path escaping
a process outlives its parent, or dies early process.md for the ambient ownership window
shared state respawns or lingers registry.md for watcher counting and the idle timer
effects run in the wrong order or too late graph.md for scheduling and flush
a remote handle behaves unlike a local one wire/client.ts; each remote ref is a local pump process (wire/pump.ts)
the DOM writes, moves, or re-listens more than expected dom.md for no-op skipping, keepers, and dispatch

Budgets

Every performance or granularity claim in the docs points at the test that enforces it:

budget enforced in
reconcile: 1 change in 10k ≤ 100 µs packages/core/test/reconcile.perf.test.ts
Mario: 1 view yield, ≤ 2 DOM writes/frame, 0 structural ops examples/mario/mario.golden.test.ts
nothing retained after dispose packages/core/test/process.leaks.test.ts

Test environment notes

Two environment quirks that cause confusing false failures:

This page's source on GitHub