> **See also:** [ROADMAP.md](../ROADMAP.md) ## v1.5.0 — PGlite Reactive Integration > **Release Theme** > This release completes the PGlite story by bridging the gap between > database-side incremental view maintenance and front-end UI reactivity. > By connecting stream table deltas to PGlite's `live.changes()` API and > providing framework-specific hooks (`useStreamTable()` for React and > Vue), pg_trickle becomes the first IVM engine to offer truly reactive > UI bindings — where DOM updates are proportional to changed rows, not > result set size. This is the local-first developer's final mile: from > `INSERT` to re-render in a single digit millisecond count, with no > polling, no diffing, and no full query re-execution. See [PLAN_PGLITE.md](plans/ecosystem/PLAN_PGLITE.md) §7 Phase 3 for the full reactive integration design. ### Reactive Bindings (Phase 3) > **In plain terms:** Phase 2 gave PGlite users in-engine IVM. This phase > connects stream table changes to PGlite's `live.changes()` API and > provides framework-specific hooks — `useStreamTable()` for React, > `useStreamTable()` for Vue — so UI components automatically re-render > when the underlying data changes. For local-first apps like collaborative > editors, dashboards, and offline-capable tools, this is the last mile > between incremental SQL and reactive UI. | Item | Description | Effort | Ref | |------|-------------|--------|-----| | PGL-3-1 | **`live.changes()` bridge.** Emit INSERT/UPDATE/DELETE change events from stream table delta application to PGlite's live query system. Keyed by `__pgt_row_id`. | 3–5d | [PLAN_PGLITE.md](plans/ecosystem/PLAN_PGLITE.md) §7 Phase 3 | | PGL-3-2 | **React hooks.** `useStreamTable(query)` hook that subscribes to stream table changes and returns reactive state. Handles mount/unmount lifecycle. | 3–5d | — | | PGL-3-3 | **Vue composable.** `useStreamTable(query)` composable with equivalent functionality. | 2–3d | — | | PGL-3-4 | **Documentation and examples.** Local-first app patterns: collaborative todo list, real-time dashboard, offline-first inventory tracker. Published as `@pgtrickle/pglite` docs. | 2–3d | — | | PGL-3-5 | **Performance benchmarks.** End-to-end latency from `INSERT` to React re-render. Compare against `live.incrementalQuery()` for complex queries (3-table join + aggregate). | 1–2d | — | > **Phase 3 subtotal: ~2–3 weeks** ### Correctness | ID | Title | Effort | Priority | |----|-------|--------|----------| | CORR-1 | Change event fidelity vs stream table state | M | P0 | | CORR-2 | Multi-row DML atomicity in reactive stream | S | P0 | | CORR-3 | Hook state consistency after rapid mutations | M | P1 | | CORR-4 | DELETE/re-INSERT identity stability | S | P1 | **CORR-1 — Change event fidelity vs stream table state** > **In plain terms:** The `live.changes()` bridge emits INSERT/UPDATE/DELETE > events derived from the IMMEDIATE mode delta application. If an event is > missed, duplicated, or misclassified (e.g., an UPDATE emitted as DELETE + > INSERT), the React/Vue state will diverge from the actual stream table > contents. For every DML operation on every DVM operator type, assert that > the sequence of change events, when applied to an empty accumulator, > produces a set identical to `SELECT * FROM stream_table`. Verify: integration test replaying 1,000 random DML operations across all operator types; final accumulator state matches `SELECT *`. Any divergence is a hard failure. Dependencies: PGL-3-1. Schema change: No. **CORR-2 — Multi-row DML atomicity in reactive stream** > **In plain terms:** A single `INSERT INTO source SELECT ... FROM > generate_series(1, 100)` inserts 100 rows and triggers IMMEDIATE mode > delta application. The `live.changes()` bridge must emit all 100 change > events as a single batch — not trickle them one-by-one — so that React > performs a single re-render, not 100. If events leak across batch > boundaries, the UI shows intermediate states that never existed in the > database. Verify: test with 100-row INSERT; assert `useStreamTable()` callback fires exactly once with all 100 rows. Intermediate renders counted via React profiler must be ≤ 1. Dependencies: PGL-3-1, PGL-3-2. Schema change: No. **CORR-3 — Hook state consistency after rapid mutations** > **In plain terms:** If a user performs INSERT → DELETE → INSERT on the > same row within 10 ms (e.g., optimistic UI with undo), the hook must > resolve to the correct final state. Race conditions between the > `live.changes()` event stream and React's asynchronous render cycle > could show stale data. The hook must use a monotonic sequence number > (from the bridge's event stream) to discard stale updates. Verify: stress test with 50 rapid mutations on the same row at 1 ms intervals; final hook state matches `SELECT *`. Test on both React 18 (concurrent mode) and React 19. Dependencies: PGL-3-1, PGL-3-2. Schema change: No. **CORR-4 — DELETE/re-INSERT identity stability** > **In plain terms:** When a row is deleted and a new row with the same PK > is inserted, the `__pgt_row_id` changes but the PK doesn't. The change > bridge must emit a DELETE for the old `__pgt_row_id` and an INSERT for > the new one — not an UPDATE — so that React's reconciler correctly > unmounts and remounts the component (not just re-renders it). Wrong > identity semantics cause stale closures and event handler leaks. Verify: test DELETE + INSERT with same PK; verify React component lifecycle (unmount + mount, not just update). Use React DevTools profiler. Dependencies: PGL-3-1, PGL-3-2. Schema change: No. ### Stability | ID | Title | Effort | Priority | |----|-------|--------|----------| | STAB-1 | Memory leak prevention in long-lived hooks | M | P0 | | STAB-2 | Subscription cleanup on component unmount | S | P0 | | STAB-3 | Error boundary integration for hook failures | S | P0 | | STAB-4 | Native extension upgrade path (0.25 → 0.26) | S | P0 | | STAB-5 | Framework version compatibility matrix | S | P1 | **STAB-1 — Memory leak prevention in long-lived hooks** > **In plain terms:** A `useStreamTable()` hook in a long-lived component > (e.g., a dashboard that runs for hours) accumulates change events via > the `live.changes()` subscription. If the bridge or hook retains > references to processed events, memory grows unboundedly. Implement a > bounded event buffer (configurable, default 1,000 events) that discards > processed events after they are applied to the hook's state snapshot. > After the buffer fills, old entries are garbage-collected. Verify: 4-hour soak test with continuous 1 row/sec mutations. Heap snapshot at 1h and 4h shows < 10% growth. No detached DOM nodes or leaked closures. Dependencies: PGL-3-1, PGL-3-2. Schema change: No. **STAB-2 — Subscription cleanup on component unmount** > **In plain terms:** When a React component using `useStreamTable()` is > unmounted (e.g., route change), the `live.changes()` subscription must > be cancelled immediately. Failing to clean up causes: (a) memory leaks > from the change listener, (b) "setState on unmounted component" warnings, > (c) stale event processing after the component is gone. Use > `useEffect()` cleanup function with an AbortController pattern. Verify: mount/unmount cycle test (100 cycles); zero console warnings, zero leaked subscriptions (verified via PGlite connection subscription count). Dependencies: PGL-3-2. Schema change: No. **STAB-3 — Error boundary integration for hook failures** > **In plain terms:** If the `live.changes()` bridge throws (e.g., stream > table was dropped while the hook is active), the hook must propagate the > error to React's error boundary / Vue's `onErrorCaptured` — not swallow > it silently or crash the app. Provide an `onError` callback option and > a default that throws to the nearest error boundary. Verify: test dropping a stream table while `useStreamTable()` is active; assert error boundary catches the error with an actionable message. Dependencies: PGL-3-2, PGL-3-3. Schema change: No. **STAB-4 — Native extension upgrade path (0.29 → 0.30)** > **In plain terms:** v0.31.0 adds reactive bindings at the TypeScript/npm > layer only. The native PostgreSQL extension and PGlite WASM extension > must continue to work unchanged. The upgrade migration from 0.29.0 to > 0.30.0 must leave existing stream tables and the `@pgtrickle/pglite` > WASM extension intact. Verify: upgrade E2E test confirms stream tables survive and refresh correctly after `0.29.0 -> 0.30.0`. TypeScript API backward compatibility verified. Dependencies: None. Schema change: No. **STAB-5 — Framework version compatibility matrix** > **In plain terms:** Test `useStreamTable()` against: React 18.x, React > 19.x, Vue 3.4+. Document which framework versions are supported. Future > consideration: Svelte 5 (runes), SolidJS, Angular signals — document > these as "community-contributed" integration points, not first-party. Verify: CI matrix testing React 18, React 19, Vue 3.4. Published compatibility table in npm README. Dependencies: PGL-3-2, PGL-3-3. Schema change: No. ### Performance | ID | Title | Effort | Priority | |----|-------|--------|----------| | PERF-1 | INSERT-to-render latency benchmark | M | P0 | | PERF-2 | Batch rendering efficiency (single re-render) | S | P0 | | PERF-3 | Bridge overhead vs raw `live.changes()` | S | P1 | **PERF-1 — INSERT-to-render latency benchmark** > **In plain terms:** Measure the end-to-end latency from `INSERT INTO > source_table` to the React component's DOM update. The target is > < 50% of `live.incrementalQuery()` latency for a 3-table join + > aggregate at 10K rows (per PLAN_PGLITE.md). This is the headline > metric: if pg_trickle's reactive path is not significantly faster than > PGlite's built-in incremental query, the value proposition collapses. Verify: benchmark suite with 5 complexity levels (scan, filter, join, aggregate, window). Publish results as a comparison table against `live.incrementalQuery()`. Target: < 50% latency at 10K rows. Dependencies: PGL-3-1, PGL-3-2, PGL-3-5. Schema change: No. **PERF-2 — Batch rendering efficiency (single re-render)** > **In plain terms:** A bulk INSERT (100 rows) must produce exactly one > React re-render, not 100. The change bridge must batch events emitted > within the same transaction into a single `live.changes()` notification. > Use `queueMicrotask()` or `requestAnimationFrame()` batching in the > TypeScript wrapper to coalesce rapid-fire events. Verify: React profiler shows ≤ 1 render per bulk DML. Test with 1, 10, 100, 1000-row INSERTs; render count is always 1. Dependencies: PGL-3-1, PGL-3-2, CORR-2. Schema change: No. **PERF-3 — Bridge overhead vs raw `live.changes()`** > **In plain terms:** The change bridge adds a translation layer between > the IMMEDIATE mode delta application and PGlite's `live.changes()` API. > Measure the overhead of this translation (serialization, event > construction, key mapping) and ensure it is < 5% of total refresh > latency. If overhead is higher, optimize the bridge's change event > construction (e.g., avoid JSON round-trips, use structured clones). Verify: micro-benchmark isolating bridge overhead from WASM refresh time. Document overhead as percentage of total INSERT-to-event latency. Dependencies: PGL-3-1. Schema change: No. ### Scalability | ID | Title | Effort | Priority | |----|-------|--------|----------| | SCAL-1 | Multiple concurrent subscriptions | S | P1 | | SCAL-2 | Large result set rendering (10K+ rows) | M | P1 | | SCAL-3 | Multi-tab / SharedWorker isolation | S | P2 | **SCAL-1 — Multiple concurrent subscriptions** > **In plain terms:** A dashboard page may render 5-10 `useStreamTable()` > hooks simultaneously, each watching a different stream table. The bridge > must not create per-hook subscriptions to `live.changes()` — instead, > use a single multiplexed subscription that fans out to registered hooks. > Measure performance with 1, 5, 10, 20 concurrent hooks. Verify: benchmark with 20 concurrent `useStreamTable()` hooks; latency degradation < 20% vs single hook. Memory growth linear (not quadratic). Dependencies: PGL-3-1, PGL-3-2. Schema change: No. **SCAL-2 — Large result set rendering (10K+ rows)** > **In plain terms:** A stream table with 10K+ rows produces a large > initial snapshot when `useStreamTable()` mounts. The hook must support > virtualized rendering (integrating with libraries like `react-virtual` > or `tanstack-virtual`) by providing a stable row identity key > (`__pgt_row_id`) and fine-grained change signals (which rows changed, > not just "something changed"). Without this, mounting a 10K-row stream > table would freeze the UI for seconds. Verify: demo app with 10K-row stream table using `@tanstack/react-virtual`. Mount time < 200 ms. Single-row INSERT re-renders only the affected row, not the full list. Dependencies: PGL-3-2, PGL-3-4. Schema change: No. **SCAL-3 — Multi-tab / SharedWorker isolation** > **In plain terms:** In multi-tab apps using PGlite with SharedWorker, > each tab gets its own `useStreamTable()` hooks but shares a single > PGlite instance. The bridge must correctly fan out change events to all > tabs without cross-tab interference or duplicate processing. Document > the SharedWorker architecture and test with 3 concurrent tabs. Verify: 3-tab test with shared PGlite instance via SharedWorker. INSERT in tab 1 causes re-render in all 3 tabs. No duplicate events. No memory leaks across tabs. Dependencies: PGL-3-1. Schema change: No. ### Ease of Use | ID | Title | Effort | Priority | |----|-------|--------|----------| | UX-1 | Local-first app example: collaborative todo | M | P0 | | UX-2 | Real-time dashboard example | M | P0 | | UX-3 | API reference with interactive playground | S | P1 | | UX-4 | Migration guide from `live.incrementalQuery()` | S | P1 | **UX-1 — Local-first app example: collaborative todo** > **In plain terms:** A complete, runnable React app demonstrating > pg_trickle + PGlite for a collaborative todo list: multiple "users" > (simulated in separate components) INSERT/UPDATE/DELETE todos, each > user's view updates reactively via `useStreamTable()`. Published in > the monorepo under `examples/pglite-todo/` with a CodeSandbox link. > This is the primary "show, don't tell" marketing asset. Verify: example app runs in CodeSandbox with zero local setup. README explains every code section. A non-pg_trickle developer can understand it in 5 minutes. Dependencies: PGL-3-2, PGL-3-4. Schema change: No. **UX-2 — Real-time dashboard example** > **In plain terms:** A React dashboard with 3 stream tables: (a) live > order count (aggregate), (b) revenue by region (join + aggregate), (c) > top products (window function + LIMIT). Data is inserted via a simulated > event stream. Each panel updates reactively. Demonstrates the breadth of > SQL operators supported in PGlite, beyond what `live.incrementalQuery()` > can efficiently handle. Verify: example app with 3 panels. INSERT 100 orders; all 3 panels update with a single render each. Published to CodeSandbox. Dependencies: PGL-3-2, PGL-3-4. Schema change: No. **UX-3 — API reference with interactive playground** > **In plain terms:** An interactive documentation page (MDX or Storybook) > where users can type SQL, create a stream table, insert data, and see > the `useStreamTable()` hook update live — all in the browser via PGlite. > This replaces the need for a local install for initial exploration. Verify: playground page loads in < 3 seconds. Users can create a stream table and see reactive updates within 30 seconds of page load. Dependencies: PGL-3-2, UX-1. Schema change: No. **UX-4 — Migration guide from `live.incrementalQuery()`** > **In plain terms:** Users already using PGlite's `live.incrementalQuery()` > need a clear guide showing: (a) when to switch to pg_trickle (complex > queries, high-throughput writes, large result sets), (b) how to migrate > step-by-step (replace `live.incrementalQuery(q)` with > `createStreamTable(q)` + `useStreamTable(name)`), (c) what to expect > (latency improvement, memory trade-off, SQL surface differences). Verify: migration guide published in docs. Includes a before/after code diff and a decision flowchart. Dependencies: PGL-3-4, PERF-1. Schema change: No. ### Test Coverage | ID | Title | Effort | Priority | |----|-------|--------|----------| | TEST-1 | Change event fidelity suite (all operators) | L | P0 | | TEST-2 | React hook lifecycle tests | M | P0 | | TEST-3 | Vue composable lifecycle tests | M | P0 | | TEST-4 | Cross-framework render count assertions | S | P0 | | TEST-5 | Long-running soak test for memory leaks | M | P1 | **TEST-1 — Change event fidelity suite (all operators)** > **In plain terms:** For each of the 23 DVM operators, test that the > `live.changes()` bridge emits the correct change events for INSERT, > UPDATE, and DELETE on the source table. Replay events into an > accumulator and assert it matches `SELECT * FROM stream_table`. This > extends v0.30.0 TEST-1 (operator E2E) by adding the reactive layer. Verify: ≥ 69 tests (23 operators × 3 DML types). Accumulator matches `SELECT *` for every test case. Dependencies: PGL-3-1, v0.30.0 TEST-1. Schema change: No. **TEST-2 — React hook lifecycle tests** > **In plain terms:** Test the full lifecycle of `useStreamTable()`: > (a) initial mount returns current stream table state, (b) INSERT on > source triggers re-render with new data, (c) unmount cancels > subscription, (d) remount re-subscribes and returns current state, > (e) rapid mount/unmount (100 cycles) has no leaks. Use React Testing > Library with `renderHook()`. Verify: ≥ 15 tests covering mount, update, unmount, remount, error, and stress scenarios. Zero console warnings in test output. Dependencies: PGL-3-2. Schema change: No. **TEST-3 — Vue composable lifecycle tests** > **In plain terms:** Equivalent of TEST-2 for Vue: mount, update, unmount, > remount, error handling. Use Vue Test Utils with `mount()` and > `wrapper.unmount()`. Test with both Options API and Composition API > usage patterns. Verify: ≥ 10 tests covering Vue lifecycle. Zero console warnings. Dependencies: PGL-3-3. Schema change: No. **TEST-4 — Cross-framework render count assertions** > **In plain terms:** For each framework (React, Vue), verify that a bulk > INSERT (100 rows) triggers exactly 1 render, not 100. This is the > batching correctness test. Use framework-specific profiling APIs (React > Profiler, Vue DevTools perf hooks) to count renders. Verify: render count = 1 for 100-row bulk INSERT in both React and Vue. CI assertion. Dependencies: PGL-3-2, PGL-3-3, PERF-2. Schema change: No. **TEST-5 — Long-running soak test for memory leaks** > **In plain terms:** Run a React app with `useStreamTable()` for 4 hours > with 1 mutation/second. Take heap snapshots at 0h, 1h, 2h, 4h. Assert > heap growth < 10%. Check for detached DOM nodes, leaked event listeners, > and orphaned closures. This validates STAB-1 under real conditions. Verify: soak test runs in CI (with a 30-min abbreviated version for PR CI). Full 4-hour version runs in nightly CI. Heap growth < 10%. Dependencies: STAB-1, PGL-3-2. Schema change: No. ### Conflicts & Risks 1. **`live.changes()` API stability.** PGlite's `live.changes()` is relatively new and its event format may change between PGlite releases. Pin the PGlite version and add an adapter layer so the bridge can accommodate event format changes without rewriting the React/Vue hooks. If PGlite deprecates `live.changes()` before v0.28.0 ships, fall back to `LISTEN/NOTIFY` with a custom channel. 2. **CORR-2 (batch atomicity) and PERF-2 (single re-render) are coupled.** The batching mechanism must ensure correctness (all-or-nothing event delivery) AND performance (single render). Using `queueMicrotask()` for batching risks splitting a transaction's events across two microtasks if the event stream straddles a microtask boundary. Consider explicit transaction-boundary markers in the bridge's event protocol. 3. **React concurrent mode complicates CORR-3 (rapid mutations).** React 18/19 concurrent features (`startTransition`, `useDeferredValue`) may delay or re-order state updates from `useStreamTable()`. The hook must use `useSyncExternalStore()` (React 18+) to ensure tearing-free reads. This is non-negotiable for correctness. 4. **SCAL-2 (large result set rendering) requires external library integration.** The `useStreamTable()` hook should not bundle a virtualization library — instead, expose stable row keys and fine-grained change signals that integrate with `@tanstack/react-virtual` or similar. Document the pattern but do not create a hard dependency. 5. **SCAL-3 (SharedWorker) is exploratory.** PGlite's SharedWorker support has known limitations (no concurrent transactions). Mark SCAL-3 as P2 and scope it to documentation + a proof-of-concept, not production-grade support. 6. **No native extension changes in v0.28.0.** This release is entirely in the TypeScript/npm layer. Any temptation to add native features (e.g., `LISTEN/NOTIFY` bridge, WebSocket push) should be deferred to post-1.0. Keep the scope tight: reactive bindings + examples + docs. > **v1.5.0 total: ~2–3 weeks (bridge + hooks) + ~1–2 weeks (examples + testing + polish)** **Exit criteria:** - [ ] PGL-3-1: Stream table changes appear in `live.changes()` event stream - [ ] PGL-3-2: React `useStreamTable()` hook re-renders on stream table changes - [ ] PGL-3-3: Vue `useStreamTable()` composable re-renders on stream table changes - [ ] PGL-3-4: At least 2 example apps published with documentation and CodeSandbox links - [ ] PGL-3-5: End-to-end latency benchmarked and published - [ ] CORR-1: 1,000-operation replay test: accumulator matches `SELECT *` for all operators - [ ] CORR-2: 100-row bulk INSERT triggers exactly 1 re-render - [ ] CORR-3: 50 rapid same-row mutations: final hook state matches `SELECT *` - [ ] CORR-4: DELETE + re-INSERT with same PK: correct unmount/mount lifecycle - [ ] STAB-1: 4-hour soak test: heap growth < 10% - [ ] STAB-2: 100 mount/unmount cycles: zero leaked subscriptions - [ ] STAB-3: Stream table dropped while hook active: error boundary catches - [ ] STAB-4: Extension upgrade path tested (`1.4.0 → 1.5.0`) - [ ] STAB-5: CI matrix passes for React 18, React 19, Vue 3.4+ - [ ] PERF-1: INSERT-to-render latency < 50% of `live.incrementalQuery()` at 10K rows - [ ] PERF-2: Render count = 1 for bulk DML (1, 10, 100, 1000 rows) - [ ] TEST-1: ≥ 69 change event fidelity tests pass (23 operators × 3 DML types) - [ ] TEST-2: ≥ 15 React hook lifecycle tests pass - [ ] TEST-3: ≥ 10 Vue composable lifecycle tests pass - [ ] TEST-4: Cross-framework render count = 1 for bulk DML - [ ] TEST-5: 30-min abbreviated soak test passes in PR CI - [ ] UX-1: Collaborative todo example published to CodeSandbox - [ ] UX-2: Real-time dashboard example published to CodeSandbox - [ ] UX-4: Migration guide from `live.incrementalQuery()` published - [ ] `just check-version-sync` passes (incl. npm package version) ---