Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Scene & Diff - Lona Docs Log in

Scene & Diff

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.

Scene shape

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.

Building a scene

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.

Diffing two scenes

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.

Serializing for cross-invocation diff

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

Reactive driver

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;
});

See also