feat(db): shared live-query observer + migrate all five adapters (RFC #1623 step 3)#1642
feat(db): shared live-query observer + migrate all five adapters (RFC #1623 step 3)#1642kevin-dp wants to merge 2 commits into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
More templates
@tanstack/angular-db
@tanstack/browser-db-sqlite-persistence
@tanstack/capacitor-db-sqlite-persistence
@tanstack/cloudflare-durable-objects-db-sqlite-persistence
@tanstack/db
@tanstack/db-ivm
@tanstack/db-sqlite-persistence-core
@tanstack/electric-db-collection
@tanstack/electron-db-sqlite-persistence
@tanstack/expo-db-sqlite-persistence
@tanstack/node-db-sqlite-persistence
@tanstack/offline-transactions
@tanstack/powersync-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/react-native-db-sqlite-persistence
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/tauri-db-sqlite-persistence
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
|
Size Change: +1.04 kB (+0.83%) Total Size: 126 kB 📦 View Changed
ℹ️ View Unchanged
|
|
Size Change: 0 B Total Size: 4.22 kB ℹ️ View Unchanged
|
| knownGaps: [`config-object-input`], | ||
| // The config-object gap is now closed here: the observer starts sync on the | ||
| // resolved collection regardless of the config path, so a bare `{ query }` | ||
| // syncs. (PR #1638 also fixes the source path directly.) |
There was a problem hiding this comment.
This comment is not needed. It only makes sense with the scope of this PR but no longer makes sense when we will merge it into main.
There was a problem hiding this comment.
Removed in cf92efd — the comment was scoped to this PR and wouldn't make sense on main.
| knownGaps: [`config-object-input`], | ||
| // The config-object gap is now closed here: the observer starts sync on the | ||
| // resolved collection regardless of the config path, so a bare `{ query }` | ||
| // syncs. (PR #1638 also fixes the source path directly.) |
There was a problem hiding this comment.
let's remove this comment, it only makes sense within this PR but in main it won't make sense anymore.
There was a problem hiding this comment.
Removed in cf92efd — the comment was scoped to this PR and wouldn't make sense on main.
92f19fa to
84895f6
Compare
4c86c7f to
a7e3c5e
Compare
84895f6 to
84f4e93
Compare
3b3c92e to
cf92efd
Compare
Add createLiveQueryObserver to @tanstack/db. Given a resolved collection (or null for disabled), it owns the shared lifecycle: start sync, subscribe with initial state, the loading→ready notify, a stable per-revision snapshot for wholesale consumers, and delivery of the raw ChangeMessage[] for granular consumers (deferInitialNotify defers the initial notify for useSyncExternalStore consumers like React). React, Vue, Svelte, Solid, and Angular all materialize from the observer, removing their duplicated subscribe/status/ready-race plumbing while keeping native reactivity: Vue/Svelte/Solid apply the change deltas granularly to their reactive maps; React/Angular consume the snapshot wholesale. Observer unit tests cover the wholesale and granular paths, disabled, deferred-notify, and dispose. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84f4e93 to
4cf25ae
Compare
cf92efd to
68ea1ca
Compare
Code reviewFound 1 issue:
db/packages/db/src/live-query-observer.ts Lines 178 to 184 in 68ea1ca db/packages/db/src/collection/lifecycle.ts Lines 282 to 289 in 68ea1ca db/packages/db/src/collection/lifecycle.ts Lines 149 to 151 in 68ea1ca I red-tested this locally with a temporary observer test on this branch: after subscribing, unsubscribing before first ready, subscribing again, and then calling Suggested fix: either make 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
onFirstReady returns no unsubscribe and detach() couldn't remove it, so a subscribe → unsubscribe-before-ready → subscribe sequence left a stale ready callback that also fired on markReady — the current listener saw two synthetic ready notifications instead of one. Guard the callback with an attach-generation token so only the current attachment's callback notifies. Adds a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good catch — reproduced it exactly (subscribe → unsubscribe-before-ready → subscribe → |
RFC #1623 step 3 — the shared observer that unifies the live-query lifecycle, now adopted by all five adapters. This is where the duplicated lifecycle across adapters actually collapses.
Base: stacked on
refactor/extract-adapter-helpers(#1641, step 2) → conformance suite (#1636). Re-target down the stack as each lands.The observer
createLiveQueryObserver(collection | null, { deferInitialNotify? })in@tanstack/db. Given a resolved live-query collection (ornullfor disabled), it owns everything the five adapters used to each re-implement:includeInitialState, so consumers get initial rows + deltas through one aligned channel)onFirstReady, de-duplicated)getSnapshot()— reference-stable foruseSyncExternalStore)ChangeMessage[]to subscribers for granular consumersThe last two are the item-6 decision made concrete: the observer carries both a snapshot and the change set, so Vue/Svelte/Solid keep fine-grained keyed-map updates while React/Angular consume the snapshot.
Input resolution stays in each adapter (query fn / config / collection / disabled) — it's framework-reactive and fixed separately (#1637/#1638). The observer owns everything after the input resolves.
All five adapters migrated
Each keeps its native reactivity; the duplicated subscribe/
onFirstReady/status/ready-race plumbing is gone:useSyncExternalStore(observer.subscribe, observer.getSnapshot)— opts intodeferInitialNotifyChangeMessage[]deltas to itsreactivemapSvelteMap(runes)ReactiveMap; keepscreateResource/Suspense +reconcileTwo contract refinements the migrations forced (both interesting)
includeInitialStateis required, not optional. Seeding a granular adapter's map fromgetSnapshot()and subscribing without initial state desyncs the collection's per-subscriber change stream — deletes arrived as empty batches. The observer must subscribe with initial state so initial rows + deltas flow through one channel.deferInitialNotifyis per-consumer. React'suseSyncExternalStoremust not get a synchronous notify duringsubscribe, so React opts into deferring the initial notify to a microtask. Effect/watcher adapters (Svelte reads synchronously afterflushSync) want it synchronous, which is the default.Bonus
Angular's
config-object-inputconformance gap closes here as a side effect — the observer starts sync on the resolved collection regardless of the config path. (PR #1638 still fixes the source path directly.)Verification
All green:
@tanstack/db2461, react 119, vue 54, svelte 54, solid 62, angular 50 (+1 todo) — including the cross-adapter conformance suite that guards every migration. Observer unit tests cover both wholesale and granular paths, disabled, deferred-notify, and dispose. Minor changeset for db, patch for all five adapters.🤖 Generated with Claude Code