Thank you for being patient! We're working hard on resolving the issue
RenderScene is the declarative model adapters reconcile against.
DOM, CLI, and TUI all build the same scene from a sheet, then
diff between scenes to drive incremental updates.
interface RenderScene {
cells: Map<SceneKey, CellNode>;
legends: Map<SceneKey, LegendNode>;
overlays: Map<SceneKey, RowOverlayNode>;
}
interface CellNode {
key: SceneKey;
rowId: RowLocalId;
columnIndex: number;
columnDate: NaiveDate;
content: CellContent;
pluginId: string;
pluginRole: Role;
}
A SceneKey is (rowId, columnIndex, surface) — stable across
re-renders so diff can match nodes by identity.
import {
buildScene, RenderPluginRegistry,
PER_TYPE_SPECS, calendarListDataProvider,
// ... other plugin data providers
} from "@tento-lona/sheets";
const registry = new RenderPluginRegistry();
for (const spec of PER_TYPE_SPECS) registry.plugin(spec);
const scene = buildScene({
sheet,
registry,
rowInputs: sheet.allRows().map(row => ({
row,
plan: renderPlan.for(row), // RenderPlan output
columns: visibleColumns,
})),
providers: { calendarList: calendarListDataProvider, /* ... */ },
});
buildScene is pure — same inputs → same scene. No IO, no
async. Run it on every invalidation; cheap because the scene is
just stable-keyed nodes.
import { diffScene } from "@tento-lona/sheets";
const diff = diffScene(prev, next);
for (const change of diff) {
switch (change.kind) {
case "insert":
case "update":
case "remove":
case "style-only":
// platform reconciler applies the change
}
}
SceneChange carries the key, the previous + next nodes, and
the kind so a reconciler can pick the cheapest update path.
CLI tools use serializeScene / deserializeScene to compare
scenes between separate render-sheet-tree runs (e.g.
--prev scene.json to color the diff).
import { serializeScene, deserializeScene } from "@tento-lona/sheets";
await Deno.writeTextFile("scene.json", JSON.stringify(serializeScene(scene)));
const restored = deserializeScene(JSON.parse(await Deno.readTextFile("scene.json")), registry);
diffScene(restored, freshScene); // reports zero changes if nothing moved
The reactive wrapper subscribes to sheet.invalidation, calls
buildScene on each change, then diffScene to emit incremental
updates. TreeWatcher (see Tree Pipeline) is
the canonical bridge for tree consumers; scene consumers usually
roll their own:
let prev = buildScene(inputs);
sheet.invalidation.addListener(this, () => {
const next = buildScene(refreshInputs());
for (const change of diffScene(prev, next)) reconcile(change);
prev = next;
});
CellNode
carries the pluginId it was rendered bybuildSceneTree projects
a scene into a ParsedTree for snapshot testsTuiReconciler
consumes scene + diff