clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/fix-sync-enrollment-startup
lofi / docs / spikes / preact-integration / evidence / 2026-07-15.md
2026-07-15.md 5.4K source

Preact integration evidence — 2026-07-15

Issue: #5
Baseline commit: 0843833
Machine: macOS 26.5.1, Deno 2.9.0, Headless Chrome 150.0.0.0
Pins: Jazz 2.0.0-alpha.53, Preact 10.29.7, Astro 7.0.9

Decision rule

Correct loading, updates, local durability waits, teardown, errors, recreation, two roots, and five HMR edits are hard gates for a Preact integration candidate. The exact vanilla lane is the vendor API control established in #7, not a component-adapter candidate. A compatibility strategy may use documented aliases, but it may not add a lofi-owned implementation of React semantics. When multiple candidates pass, fewer author-facing concepts and less owned maintenance win; bundle contribution is the tie-breaker.

Results

Strategy Correctness Reachable production JS Framework-specific burden Result
Exact vanilla Jazz control Core query/write/persistence control 213,080 B raw / 57,823 B gzip One imperative DOM view API baseline
preact/compat + Jazz React bindings Fail at build Not meaningful Would require a React 19 semantic polyfill Reject
Thin Preact adapter over vanilla Jazz Pass candidate gates 228,829 B raw / 64,806 B gzip 524 runtime LOC plus 242 test LOC Graduate

The thin Astro/Preact page has 15,749 B raw / 6,983 B gzip more reachable JavaScript than the single-view vanilla API control. This is an indicative floor, not a same-UI bundle claim. Both load the same 9,799,438 B Jazz WASM payload (3,078,164 B with local gzip). Browser resource timing, rather than every emitted file, defines “reachable”; the Astro/Jazz build emits additional server/client copies that the page does not request. The compat candidate has no valid production contribution because it cannot build without changing the semantic contract.

Compatibility control

Reproduce the rejected lane:

deno run -A npm:vite@8.0.1 build spikes/preact-integration/compat-control \
  --config spikes/preact-integration/compat-control/vite.config.ts

Vite transforms 332 modules and then stops because jazz-tools/dist/react-core/use-all.js statically imports React 19's use, which preact@10.29.7/compat does not export. The exact Jazz package declares optional peer react >=19; Preact compat identifies as React 18.3.1. A homemade use implementation would own more unstable semantics than the thin adapter and would invalidate the comparison.

Thin-adapter gates

deno test apps/prototype/src/runtime retains deterministic coverage for:

  • loading to ready and reactive delivery to two consumers;
  • one underlying Jazz query shared by both roots;
  • idempotent unmount and final vendor unsubscribe;
  • insert/update waits at the public local durability tier;
  • rejected local writes and public mutation errors reaching UI state.
  • concurrent recreation coalescing and idempotent database shutdown;
  • HMR disposal before deferred database creation never attaching an obsolete adapter;
  • rejected component-boundary mutations being handled after runtime state projection.

Production-browser results:

  • a north-island write appeared in both roots and reported local only after WriteHandle.wait;
  • reload retained the note;
  • explicit database recreation retained the note and settled at one active client, two consumers, and one vendor subscription;
  • the final five-edit rerun after lifecycle hardening measured 318, 131, 130, 126, and 29 ms (130 ms median, 318 ms slowest);
  • after every edit, retained data remained visible and steady cardinality stayed 1 / 2 / 1;
  • no browser error or unhandledrejection event was observed on the live write, recreation, or HMR paths.

The rejected-local-write path is retained deterministically at the store and UI promise boundary; the exact browser database does not expose a supported fault-injection hook, so this evidence does not claim a browser-injected storage failure.

Total subscription and unsubscribe counters increase together across HMR because adapter code is replaced. The database client is deliberately preserved; preserving the adapter itself would leave edited adapter code stale.

The final rerun measured from filesystem modification time to the browser-observed replacement DOM attribute. It followed the same five-edit protocol after adding generation cancellation, serialized client recreation, and mutation-listener diagnostics; the retained row and 1 / 2 / 1 / 1 steady state were checked after every edit.

Honest limitation

The public Db.subscribeAll() callback has no asynchronous error channel. The adapter surfaces synchronous query setup failures, rejected local/global write waits, and db.onMutationError(). It does not claim that every deferred subscription-start failure can reach UI state. This is a Jazz API gap, not a hidden success state.

Contract delta

DX-HMR-01 changes from “one subscription per consumer” to “one underlying vendor subscription per shared query, with consumer count reported separately.” This is stricter about resource ownership and matches the proven two-island topology.