Internals
- One update path
- Module map
- Layering rules
- Where to look when something breaks
- Budgets
- 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:
- The diff is the contract. The patch a local binding reacts to is the
patch that crosses the socket.
packages/wire/spec/certifies external hosts against those op shapes, so a change to the op vocabulary is a protocol change, not an implementation detail. - Precision is a property of reads, not writes. Application code writes immutable updates and never declares dependencies. Everything fine-grained comes from the reader side recording what it touched.
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 --> HOSTRead 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.
system.tsstays 1:1 with upstream alien-signals. It is a faithful MIT port, attributed inTHIRD-PARTY-NOTICES.md. Path precision is layered ingraph.tsvia gates precisely so the port never needs editing. Changes go ingraph.ts.packages/corenever touches the DOM, andpackages/wirestays isomorphic and DOM-free (host-universal globals reached throughglobalThisare fine).- Budgets are CI assertions. Tighten them when you can; never loosen one to make a change fit.
- New public concepts must dissolve at least two existing problems. The registry earned its place by being DI, query cache, and remote addressing at once. Otherwise it belongs in recipes.md as userland code.
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:
- Leak tests need
gc({ execution: 'async' }). Plaingc()false-fails under V8 conservative stack scanning. - happy-dom's
MutationObservermissescharacterData, so DOM-write tests spy onText#dataandsetAttributeinstead of observing mutations.