Observable Runtime Spike
Date: 2026-05-09
Outcome
Do not adopt @observablehq/runtime as the core Lapis notebook runtime for the current refactor.
Lapis should keep its own explicit notebook execution runtime and continue borrowing Observable-compatible API shapes selectively, especially for viewof, Inputs, html, svg, FileAttachment, generators, and helper naming. A future compatibility layer may still use Observable concepts or selected packages, but the core runtime should remain Lapis-owned until there is a concrete design that preserves Lapis execution, persistence, worker, and renderer contracts without forcing an Observable notebook execution model into the product plugin.
Spike Inputs
The upstream @observablehq/runtime package exposes a reactive module/variable runtime:
new Runtime(builtins, global)creates a runtime with builtins and unresolved-global resolution.runtime.module(define, observer)creates modules and observer factories.module.variable(observer).define(name, inputs, definition)defines reactive variables.module.import(...),module.derive(...), andmodule.redefine(...)support imports and replacement variables.module.value(name)resolves the next value of a named variable.- observers receive
pending,fulfilled, andrejectednotifications. - variables compute when observed, and generator variables are pulled by the runtime.
Those mechanics are well-suited to Observable notebooks, where the runtime owns reactive variables and observers are the primary output surface.
Compatibility Findings
Observable runtime can model dependency-driven JavaScript values, but it does not directly match the Lapis notebook execution contract.
| Requirement | Finding |
|---|---|
| Explicit note-level, stale-cell, current-cell, and inline execution commands | Observable variables recompute through runtime dependency invalidation and observers. Lapis commands need deterministic command-scoped execution, cancellation, progress, and selective downstream rerun policy. |
| Generated-state persistence keyed by markdown cell ids and source fingerprints | Observable variables are runtime entities whose names can change or be anonymous. Lapis persists output and cell state by stable parsed markdown cell ids and source fingerprints. |
| TypeScript cells | Observable runtime accepts JavaScript definitions; Lapis still needs TypeScript transform and error mapping before execution. |
| SQL and dynamic markdown cells | Observable runtime does not provide Lapis SQL interpolation, DuckDB table registration, or dynamic markdown output semantics. These remain Lapis runtime responsibilities. |
| Worker/main execution selection | Lapis chooses worker versus in-process execution from DOM, DuckDB, dependency-loader, and host capability facts. Observable runtime does not solve the product-specific worker and host bridge split. |
| Notebook-owned typed output renderers | Observable observers/inspectors are output-oriented, while Lapis needs typed table, chart, markdown, DOM, image, and error outputs routed through notebook-owned renderers and generated-state persistence. |
Decision
Keep the Lapis runtime as the core execution engine.
Use Observable as an API-shape reference rather than as the execution substrate:
- keep constrained
viewof name = expressionsupport through@lapis-notes/notebook-input - keep Observable Inputs and htl loading lazy and DOM-scoped
- keep
lapis.FileAttachmentvault-backed rather than Observable-host-backed - keep user dependencies as validated bare globals rather than Observable module imports for now
- evaluate specific Observable concepts later only when they can be adapted behind the existing Lapis runtime contracts
This closes the current compatibility spike. Fuller Observable JavaScript syntax, imports, mutable values, generators, and invalidation should not be adopted wholesale or tracked as one standing implementation task. Future compatibility work may propose a specific Observable concept only when it preserves the existing Lapis execution, persistence, worker, and renderer contracts.