Contents
See also: 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: fromINSERTto re-render in a single digit millisecond count, with no polling, no diffing, and no full query re-execution.
See 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 §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 toSELECT * 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. Thelive.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_idchanges but the PK doesn’t. The change bridge must emit a DELETE for the old__pgt_row_idand 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 thelive.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), thelive.changes()subscription must be cancelled immediately. Failing to clean up causes: (a) memory leaks from the change listener, (b) “setState on unmounted component” warnings, © stale event processing after the component is gone. UseuseEffect()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’sonErrorCaptured— not swallow it silently or crash the app. Provide anonErrorcallback 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/pgliteWASM 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_tableto the React component’s DOM update. The target is < 50% oflive.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. UsequeueMicrotask()orrequestAnimationFrame()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 tolive.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 likereact-virtualortanstack-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 underexamples/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), © 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 (replacelive.incrementalQuery(q)withcreateStreamTable(q)+useStreamTable(name)), © 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 matchesSELECT * 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, © unmount cancels subscription, (d) remount re-subscribes and returns current state, (e) rapid mount/unmount (100 cycles) has no leaks. Use React Testing Library withrenderHook().
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()andwrapper.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
live.changes()API stability. PGlite’slive.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 deprecateslive.changes()before v0.28.0 ships, fall back toLISTEN/NOTIFYwith a custom channel.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.React concurrent mode complicates CORR-3 (rapid mutations). React 18/19 concurrent features (
startTransition,useDeferredValue) may delay or re-order state updates fromuseStreamTable(). The hook must useuseSyncExternalStore()(React 18+) to ensure tearing-free reads. This is non-negotiable for correctness.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-virtualor similar. Document the pattern but do not create a hard dependency.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.
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/NOTIFYbridge, 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)