2026-10-31 FIELD NOTES · RECALL
Recall: the page always comes back
One ledger for every change, and how we test it.
Fizlet runs on someone else's page, so it has to be able to leave. Every change it makes to the page goes through one ledger, and destroy() puts back everything the engine still owns, newest first. The DOM then matches a snapshot taken before the engine started. A newer edit by the page always wins.
By Chris Cooley ↗ · · 3.3 KB gz
One ledger
five kinds of change
The ledger knows five kinds of change, and the engine makes no others. text(node, value) sets a text node's value. style(el, prop, value) sets one inline style property. attr(el, name, value) sets or removes an attribute. cls(el, name, on) adds or removes a class. add(node) records a node the engine put on the page.
Each records the original the first time it touches something, and what it wrote each time after. restore() walks that record backwards. A style attribute comes back byte for byte, not just the same property values.
Three rules
- Change-only. Writing the value that is already there touches nothing. A browser test writes the same values twice and counts zero DOM mutations; the HUD's test counts 0 mutations over 120 frames where nothing changed.
- Host wins. If the page changed something after the engine did, the ledger stops writing it and never restores over it. The write returns
false, so a brick can tell. - One signal. Every listener and observer an engine adds is bound to one
AbortSignal.destroy()aborts it, so nothing is left listening.
How the bricks use it
The page as the board
fizlet/board-pageturns a shot word's letters into 0s and 1s by writing into the page's own Text nodes withledger.text: no wrappers, spans or clones. Recall gives every word back, byte for byte.Timelines
fizlet/timelinewrites keyframe values through the ledger. Its browser test destroys a timeline mid-play on an element with its own inline style, and the style comes back byte for byte.Things the engine adds
The touch pad, the perf overlay and the match overlay each add one element through the ledger, so
engine.destroy()removes them too. The perf test checks the host is back byte for byte.Slots
fizlet/slotmounts a scene in its own contained stage inside an Astro slot;destroy()puts the slot back exactly as the page drew it.
Why not reload?
A reload throws away the visitor's state: their scroll, their form fields, whatever the page's own code was doing. Recall undoes only what the engine did. That is what lets a game knock a page's headings around, or a hero take over a section, and leave it as the visitor left it.
You can try it on the home page: blast the little page, then press Recall. That key runs engine.ledger.restore(), the same call destroy() makes.
Questions
What if my own code writes the DOM?
Then the ledger doesn't know about it and can't give it back. Write through engine.ledger in render, or use a brick that does.
Can I restore without destroying the engine?
Yes: engine.ledger.restore(). The engine keeps running and the next write records a fresh original.