Skip to content

feat(db): shared live-query observer + migrate all five adapters (RFC #1623 step 3)#1642

Open
kevin-dp wants to merge 4 commits into
mainfrom
refactor/live-query-observer
Open

feat(db): shared live-query observer + migrate all five adapters (RFC #1623 step 3)#1642
kevin-dp wants to merge 4 commits into
mainfrom
refactor/live-query-observer

Conversation

@kevin-dp

@kevin-dp kevin-dp commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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 (or null for disabled), it owns everything the five adapters used to each re-implement:

  • start sync
  • subscribe to changes (with includeInitialState, so consumers get initial rows + deltas through one aligned channel)
  • the already-ready / loading→ready notify race (onFirstReady, de-duplicated)
  • a stable per-revision snapshot for wholesale consumers (getSnapshot() — reference-stable for useSyncExternalStore)
  • delivery of the raw ChangeMessage[] to subscribers for granular consumers

The 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:

Adapter Consumer kind How it materializes
React wholesale useSyncExternalStore(observer.subscribe, observer.getSnapshot) — opts into deferInitialNotify
Vue granular applies ChangeMessage[] deltas to its reactive map
Svelte granular applies deltas to its SvelteMap (runes)
Solid granular applies deltas to its ReactiveMap; keeps createResource/Suspense + reconcile
Angular wholesale re-reads the collection into signals on each notify

Two contract refinements the migrations forced (both interesting)

  • includeInitialState is required, not optional. Seeding a granular adapter's map from getSnapshot() 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.
  • deferInitialNotify is per-consumer. React's useSyncExternalStore must not get a synchronous notify during subscribe, so React opts into deferring the initial notify to a microtask. Effect/watcher adapters (Svelte reads synchronously after flushSync) want it synchronous, which is the default.

Bonus

Angular's config-object-input conformance 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/db 2461, 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

Summary by CodeRabbit

  • New Features
    • Added a shared live-query observer API (createLiveQueryObserver) exposed from the main DB package.
    • Updated React, Vue, Svelte, Solid, and Angular live-query hooks to use the shared observer for consistent snapshot and subscription behavior.
  • Bug Fixes
    • Improved live-query lifecycle handling, including readiness transitions and disabled-query behavior.
    • Strengthened unsubscription/disposal cleanup to ensure updates stop reliably.
  • Tests
    • Added coverage for the new observer and React StrictMode behavior.
    • Updated Angular conformance expectations to no longer treat config-object input as a known gap.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da163040-80cc-4f4d-b166-0e0e25c12c1e

📥 Commits

Reviewing files that changed from the base of the PR and between c05a726 and fd3c87d.

📒 Files selected for processing (2)
  • packages/react-db/src/useLiveQuery.ts
  • packages/react-db/tests/useLiveQuery.strictmode.test.tsx

📝 Walkthrough

Walkthrough

This PR adds a shared LiveQueryObserver in @tanstack/db, re-exports it publicly, and updates React, Vue, Svelte, Solid, and Angular live-query hooks to use it. It also adds observer tests, updates Angular conformance gaps, and records the change in a changeset.

Changes

Shared live-query observer migration

Layer / File(s) Summary
LiveQueryObserver implementation, exports, and tests
packages/db/src/live-query-observer.ts, packages/db/src/index.ts, packages/db/tests/live-query-observer.test.ts
Adds LiveQuerySnapshot, LiveQueryObserverListener, LiveQueryObserver, createLiveQueryObserver, and LiveQueryObserverImpl with cached snapshot versioning, lazy state/data computation, listener-managed attach/detach, deferred initial notify, and idempotent dispose; re-exports from the package index; adds Vitest coverage for readiness, deltas, unsubscribe, disabled source, deferred notify, and duplicate-ready guards.
React useLiveQuery migration
packages/react-db/src/useLiveQuery.ts, packages/react-db/tests/useLiveQuery.strictmode.test.tsx
Replaces local snapshot/version bookkeeping and manual subscribeChanges handling with an observerRef feeding useSyncExternalStore via observer.getSnapshot(), and adds a StrictMode regression test.
Vue useLiveQuery migration
packages/vue-db/src/useLiveQuery.ts
Replaces subscribeChanges, onFirstReady, nextTick, and idle preload handling with a currentObserver-driven flow updating state, internalData, and status from observer snapshots.
Svelte and Solid useLiveQuery migration
packages/svelte-db/src/useLiveQuery.svelte.ts, packages/solid-db/src/useLiveQuery.ts
Replaces manual collection subscriptions with createLiveQueryObserver, applying change batches to local state and disposing the observer on cleanup.
Angular useLiveQuery migration and conformance update
packages/angular-db/src/index.ts, packages/angular-db/tests/conformance.test.ts
Replaces direct sync/subscribe hooks in injectLiveQuery with createLiveQueryObserver; removes config-object-input from Angular's knownGaps.
Changeset version bump documentation
.changeset/live-query-observer.md
Documents version bumps and the new shared observer for @tanstack/db and the framework adapters.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • TanStack/db#1594: Both PRs modify packages/react-db/src/useLiveQuery.ts to change how React subscription notifications are deferred.
  • TanStack/db#1636: This PR updates packages/angular-db/tests/conformance.test.ts knownGaps, which aligns with the same Angular conformance area.
  • TanStack/db#1641: Both PRs update framework live-query hooks to rely on shared @tanstack/db helper logic.

Suggested reviewers: samwillis, KyleAmathews

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the shared live-query observer and adapter migration.
Description check ✅ Passed The description covers the changes, motivation, verification, and release impact, though it does not use the repo's exact checklist template.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/live-query-observer

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1642

@tanstack/browser-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/browser-db-sqlite-persistence@1642

@tanstack/capacitor-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/capacitor-db-sqlite-persistence@1642

@tanstack/cloudflare-durable-objects-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/cloudflare-durable-objects-db-sqlite-persistence@1642

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1642

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1642

@tanstack/db-sqlite-persistence-core

npm i https://pkg.pr.new/@tanstack/db-sqlite-persistence-core@1642

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1642

@tanstack/electron-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/electron-db-sqlite-persistence@1642

@tanstack/expo-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/expo-db-sqlite-persistence@1642

@tanstack/node-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/node-db-sqlite-persistence@1642

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1642

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1642

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1642

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1642

@tanstack/react-native-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/react-native-db-sqlite-persistence@1642

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1642

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1642

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1642

@tanstack/tauri-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/tauri-db-sqlite-persistence@1642

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1642

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1642

commit: fd3c87d

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Size Change: +1.06 kB (+0.85%)

Total Size: 126 kB

📦 View Changed
Filename Size Change
packages/db/dist/esm/index.js 3.19 kB +24 B (+0.76%)
packages/db/dist/esm/live-query-observer.js 1.04 kB +1.04 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size
packages/db/dist/esm/collection/change-events.js 1.43 kB
packages/db/dist/esm/collection/changes.js 1.38 kB
packages/db/dist/esm/collection/cleanup-queue.js 810 B
packages/db/dist/esm/collection/events.js 434 B
packages/db/dist/esm/collection/index.js 3.62 kB
packages/db/dist/esm/collection/indexes.js 1.99 kB
packages/db/dist/esm/collection/lifecycle.js 1.69 kB
packages/db/dist/esm/collection/mutations.js 2.47 kB
packages/db/dist/esm/collection/state.js 5.4 kB
packages/db/dist/esm/collection/subscription.js 3.74 kB
packages/db/dist/esm/collection/sync.js 2.88 kB
packages/db/dist/esm/collection/transaction-metadata.js 144 B
packages/db/dist/esm/deferred.js 207 B
packages/db/dist/esm/errors.js 5.1 kB
packages/db/dist/esm/event-emitter.js 748 B
packages/db/dist/esm/indexes/auto-index.js 829 B
packages/db/dist/esm/indexes/base-index.js 767 B
packages/db/dist/esm/indexes/basic-index.js 2.06 kB
packages/db/dist/esm/indexes/btree-index.js 2.19 kB
packages/db/dist/esm/indexes/index-registry.js 820 B
packages/db/dist/esm/indexes/reverse-index.js 557 B
packages/db/dist/esm/live-query-adapter.js 318 B
packages/db/dist/esm/local-only.js 916 B
packages/db/dist/esm/local-storage.js 2.12 kB
packages/db/dist/esm/optimistic-action.js 359 B
packages/db/dist/esm/paced-mutations.js 496 B
packages/db/dist/esm/proxy.js 3.75 kB
packages/db/dist/esm/query/builder/functions.js 1.47 kB
packages/db/dist/esm/query/builder/index.js 5.84 kB
packages/db/dist/esm/query/builder/ref-proxy.js 1.24 kB
packages/db/dist/esm/query/compiler/evaluators.js 1.89 kB
packages/db/dist/esm/query/compiler/expressions.js 430 B
packages/db/dist/esm/query/compiler/group-by.js 3.56 kB
packages/db/dist/esm/query/compiler/index.js 6.67 kB
packages/db/dist/esm/query/compiler/joins.js 2.5 kB
packages/db/dist/esm/query/compiler/lazy-targets.js 923 B
packages/db/dist/esm/query/compiler/order-by.js 1.74 kB
packages/db/dist/esm/query/compiler/select.js 1.53 kB
packages/db/dist/esm/query/effect.js 4.77 kB
packages/db/dist/esm/query/expression-helpers.js 1.43 kB
packages/db/dist/esm/query/ir.js 1.25 kB
packages/db/dist/esm/query/live-query-collection.js 360 B
packages/db/dist/esm/query/live/collection-config-builder.js 9.1 kB
packages/db/dist/esm/query/live/collection-registry.js 264 B
packages/db/dist/esm/query/live/collection-subscriber.js 1.93 kB
packages/db/dist/esm/query/live/internal.js 145 B
packages/db/dist/esm/query/live/utils.js 1.81 kB
packages/db/dist/esm/query/optimizer.js 2.92 kB
packages/db/dist/esm/query/predicate-utils.js 2.97 kB
packages/db/dist/esm/query/query-once.js 359 B
packages/db/dist/esm/query/subset-dedupe.js 960 B
packages/db/dist/esm/scheduler.js 1.3 kB
packages/db/dist/esm/SortedMap.js 1.3 kB
packages/db/dist/esm/strategies/debounceStrategy.js 247 B
packages/db/dist/esm/strategies/queueStrategy.js 428 B
packages/db/dist/esm/strategies/throttleStrategy.js 246 B
packages/db/dist/esm/transactions.js 3.04 kB
packages/db/dist/esm/utils.js 927 B
packages/db/dist/esm/utils/array-utils.js 273 B
packages/db/dist/esm/utils/browser-polyfills.js 304 B
packages/db/dist/esm/utils/btree.js 5.61 kB
packages/db/dist/esm/utils/comparison.js 1.11 kB
packages/db/dist/esm/utils/cursor.js 457 B
packages/db/dist/esm/utils/index-optimization.js 2.39 kB
packages/db/dist/esm/utils/type-guards.js 157 B
packages/db/dist/esm/utils/uuid.js 449 B
packages/db/dist/esm/virtual-props.js 360 B

compressed-size-action::db-package-size

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 4.22 kB

ℹ️ View Unchanged
Filename Size
packages/react-db/dist/esm/index.js 249 B
packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.32 kB
packages/react-db/dist/esm/useLiveQuery.js 1.33 kB
packages/react-db/dist/esm/useLiveQueryEffect.js 355 B
packages/react-db/dist/esm/useLiveSuspenseQuery.js 567 B
packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

@kevin-dp kevin-dp changed the title feat(db): shared live-query observer + React migration (RFC #1623 step 3) feat(db): shared live-query observer + migrate all five adapters (RFC #1623 step 3) Jul 2, 2026
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.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove this comment, it only makes sense within this PR but in main it won't make sense anymore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in cf92efd — the comment was scoped to this PR and wouldn't make sense on main.

@kevin-dp kevin-dp force-pushed the refactor/extract-adapter-helpers branch from 92f19fa to 84895f6 Compare July 7, 2026 08:22
@kevin-dp kevin-dp force-pushed the refactor/live-query-observer branch from 4c86c7f to a7e3c5e Compare July 7, 2026 08:22
@kevin-dp kevin-dp force-pushed the refactor/extract-adapter-helpers branch from 84895f6 to 84f4e93 Compare July 7, 2026 09:55
@kevin-dp kevin-dp force-pushed the refactor/live-query-observer branch 2 times, most recently from 3b3c92e to cf92efd Compare July 7, 2026 10:05
@kevin-dp kevin-dp force-pushed the refactor/extract-adapter-helpers branch from 84f4e93 to 4cf25ae Compare July 7, 2026 10:24
@kevin-dp kevin-dp force-pushed the refactor/live-query-observer branch from cf92efd to 68ea1ca Compare July 7, 2026 10:24
@KyleAMathews

Copy link
Copy Markdown
Collaborator

Code review

Found 1 issue:

  1. onFirstReady registration is not detach-safe. LiveQueryObserverImpl.attach() registers a callback every time the observer gets its first listener while the collection is not ready, but detach() only unsubscribes the change subscription. Since CollectionLifecycleManager.onFirstReady() only pushes into an array and does not return an unsubscribe, a subscribe → unsubscribe before ready → subscribe again sequence leaves two callbacks queued; when markReady() drains them, both call notify(undefined) against the current listener.

// Catch a *later* loading→ready transition that carries no change events
// (e.g. `markReady()` with no rows). Skip when already ready — the initial
// state batch above already covers that, and `onFirstReady` would fire an
// immediate duplicate.
if (collection.status !== `ready`) {
collection.onFirstReady(() => notify(undefined))
}

public onFirstReady(callback: () => void): void {
// If already ready, call immediately
if (this.hasBeenReady) {
callback()
return
}
this.onFirstReadyCallbacks.push(callback)

const callbacks = [...this.onFirstReadyCallbacks]
this.onFirstReadyCallbacks = []
callbacks.forEach((callback) => callback())

I red-tested this locally with a temporary observer test on this branch: after subscribing, unsubscribing before first ready, subscribing again, and then calling markReady, the listener saw 2 synthetic ready notifications where the expected value was 1.

Suggested fix: either make onFirstReady return an unsubscribe and dispose it with collectionUnsub, or arm only one ready callback per observer/collection and guard it with a generation token so stale callbacks no-op after detach/dispose.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@kevin-dp

kevin-dp commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — reproduced it exactly (subscribe → unsubscribe-before-ready → subscribe → markReady gave 2 synthetic ready notifications instead of 1). Fixed in 8ee8e8e with your option B: an attach-generation token, so a callback left behind by a superseded attach no-ops instead of notifying. Went with the observer-local guard rather than changing onFirstReady's signature to keep the fix contained. Added a regression test for the sequence.

Base automatically changed from refactor/extract-adapter-helpers to main July 9, 2026 07:29
@kevin-dp kevin-dp marked this pull request as ready for review July 9, 2026 07:29
kevin-dp and others added 2 commits July 9, 2026 09:30
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>
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>
@kevin-dp kevin-dp force-pushed the refactor/live-query-observer branch from 8ee8e8e to e957121 Compare July 9, 2026 07:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
packages/db/src/live-query-observer.ts (1)

24-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tighten the collection generic instead of exposing any.

The observer API can use Collection<T, TKey> here; the default utility generic keeps the API precise without leaking any.

Proposed typing cleanup
-  collection: Collection<T, TKey, any> | undefined
+  collection: Collection<T, TKey> | undefined
...
-  private readonly collection: Collection<T, TKey, any> | null
+  private readonly collection: Collection<T, TKey> | null
...
-    collection: Collection<T, TKey, any> | null,
+    collection: Collection<T, TKey> | null,
...
-  collection: Collection<T, TKey, any> | null | undefined,
+  collection: Collection<T, TKey> | null | undefined,

As per coding guidelines, “Avoid using any types; use unknown instead when the type is truly unknown, and provide proper type annotations for return values.”

Also applies to: 86-100, 248-253

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/live-query-observer.ts` at line 24, The observer typings are
leaking `any` through the collection generic, so tighten the `LiveQueryObserver`
API to use `Collection<T, TKey>` (or the existing default utility generic)
instead of `Collection<T, TKey, any>`. Update the `collection` field and the
related observer methods/types in `live-query-observer` that reference this
shape, including the other affected spots noted in the diff, so the generic
stays precise without exposing `any`.

Source: Coding guidelines

packages/db/tests/live-query-observer.test.ts (1)

45-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the as any casts from observer tests.

These casts hide whether the new public factory accepts the collection types it is expected to support.

As per coding guidelines, “Avoid using any types; use unknown instead when the type is truly unknown.”

Also applies to: 60-60, 92-92, 121-123, 138-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/tests/live-query-observer.test.ts` at line 45, The observer tests
are masking the real type compatibility of createLiveQueryObserver by casting
sources to any; update the affected test cases to pass properly typed
collection/source values instead of any so the public factory is exercised
against its supported collection types. Use the existing test helpers and types
around createLiveQueryObserver, makeSource, and the other affected observer
assertions to infer the correct typings, and replace any remaining any casts in
the referenced test blocks with explicit, appropriate types.

Source: Coding guidelines

packages/react-db/src/useLiveQuery.ts (1)

435-439: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid erasing the hook return type with any.

The new as any bypasses the overload contracts at the return boundary. Please cast to the hook’s concrete result type or add a typed adapter from LiveQuerySnapshot to the existing public return shape. As per coding guidelines, “Avoid using any types” and “Always provide the most precise return type annotation.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-db/src/useLiveQuery.ts` around lines 435 - 439, The return in
useLiveQuery is erasing the hook’s type contract with as any, which bypasses the
overload guarantees. Update the useSyncExternalStore result to preserve the
concrete hook return type by casting to the existing public snapshot/result type
or introducing a typed adapter from LiveQuerySnapshot, and keep the return
aligned with the function’s declared overloads instead of using any.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/db/src/live-query-observer.ts`:
- Around line 112-115: The snapshot cache in `LiveQueryObserver.getSnapshot()`
can stay stale when `preload()` or a status-only transition makes the collection
ready without bumping `version`. Update the cache invalidation logic so
`preload()` and any readiness/status changes also force a cached snapshot
refresh, not just version changes. Use the existing `cachedVersion`, `version`,
and `getSnapshot()` flow, and make sure the invalidation also covers the related
status transition path referenced in the observer.
- Around line 199-204: The deferred initial notification flush in
LiveQueryObserver can still emit stale changes after a
subscribe/unsubscribe/resubscribe cycle. Update the deferInitialNotify path in
live-query-observer’s attach logic to associate the queued microtask with the
specific attach instance (for example via an incrementing attach token or
generation) and skip flushing if a newer attach has superseded it or the
observer was detached. Ensure the guard is checked before emitting deferred
changes in the queueMicrotask callback.

In `@packages/db/tests/live-query-observer.test.ts`:
- Around line 120-155: Add regression coverage for the remaining lifecycle races
in createLiveQueryObserver: extend the existing observer tests to reproduce a
deferInitialNotify flow where a subscription is unsubscribed and then
resubscribed before the microtask flush, and verify only the latest subscription
receives the deferred notify. Also add a test around getSnapshot() and preload()
that calls getSnapshot() before preload(), then asserts a ready snapshot is
returned after preload() completes. Use the existing createLiveQueryObserver,
subscribe, getSnapshot, and preload behavior to keep the tests aligned with the
bug scenarios.

In `@packages/react-db/src/useLiveQuery.ts`:
- Around line 414-423: Move the observer teardown out of the render path in
useLiveQuery: the needsNewCollection branch should not call
observerRef.current?.dispose() during render, because that can tear down the
committed subscription before React finishes updating. Update useLiveQuery to
perform observer disposal/recreation in a commit-phase effect or cleanup, and
add separate state/flag tracking for initialization so disabled queries do not
recreate the observer on every render when collectionRef.current remains null.

In `@packages/solid-db/src/useLiveQuery.ts`:
- Around line 393-397: Clear the existing Solid state before wiring up the new
observer in useLiveQuery so stale rows from the previous collection are removed
when collection() changes. Add the reset immediately before
createLiveQueryObserver/currentCollection subscription setup, and keep the logic
localized around observer.subscribe so includeInitialState only repopulates the
new collection instead of leaving old keys behind.

---

Nitpick comments:
In `@packages/db/src/live-query-observer.ts`:
- Line 24: The observer typings are leaking `any` through the collection
generic, so tighten the `LiveQueryObserver` API to use `Collection<T, TKey>` (or
the existing default utility generic) instead of `Collection<T, TKey, any>`.
Update the `collection` field and the related observer methods/types in
`live-query-observer` that reference this shape, including the other affected
spots noted in the diff, so the generic stays precise without exposing `any`.

In `@packages/db/tests/live-query-observer.test.ts`:
- Line 45: The observer tests are masking the real type compatibility of
createLiveQueryObserver by casting sources to any; update the affected test
cases to pass properly typed collection/source values instead of any so the
public factory is exercised against its supported collection types. Use the
existing test helpers and types around createLiveQueryObserver, makeSource, and
the other affected observer assertions to infer the correct typings, and replace
any remaining any casts in the referenced test blocks with explicit, appropriate
types.

In `@packages/react-db/src/useLiveQuery.ts`:
- Around line 435-439: The return in useLiveQuery is erasing the hook’s type
contract with as any, which bypasses the overload guarantees. Update the
useSyncExternalStore result to preserve the concrete hook return type by casting
to the existing public snapshot/result type or introducing a typed adapter from
LiveQuerySnapshot, and keep the return aligned with the function’s declared
overloads instead of using any.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a2f170d5-edf2-466d-b219-bdc647b51052

📥 Commits

Reviewing files that changed from the base of the PR and between 6d4c096 and e957121.

📒 Files selected for processing (10)
  • .changeset/live-query-observer.md
  • packages/angular-db/src/index.ts
  • packages/angular-db/tests/conformance.test.ts
  • packages/db/src/index.ts
  • packages/db/src/live-query-observer.ts
  • packages/db/tests/live-query-observer.test.ts
  • packages/react-db/src/useLiveQuery.ts
  • packages/solid-db/src/useLiveQuery.ts
  • packages/svelte-db/src/useLiveQuery.svelte.ts
  • packages/vue-db/src/useLiveQuery.ts

Comment thread packages/db/src/live-query-observer.ts Outdated
Comment thread packages/db/src/live-query-observer.ts
Comment thread packages/db/tests/live-query-observer.test.ts
Comment thread packages/react-db/src/useLiveQuery.ts Outdated
Comment on lines +393 to +397

// The shared observer owns subscription, the ready-race, and status; Solid
// materializes into its keyed ReactiveMap (granular) + reconciled store.
const observer = createLiveQueryObserver(currentCollection)
const unsubscribe = observer.subscribe(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear state before subscribing to a replacement collection.

includeInitialState only inserts current rows; it does not delete keys from the previous collection. Without a clear here, state can expose stale rows after collection() changes.

Proposed fix
     // The shared observer owns subscription, the ready-race, and status; Solid
     // materializes into its keyed ReactiveMap (granular) + reconciled store.
     const observer = createLiveQueryObserver(currentCollection)
+    state.clear()
     const unsubscribe = observer.subscribe(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The shared observer owns subscription, the ready-race, and status; Solid
// materializes into its keyed ReactiveMap (granular) + reconciled store.
const observer = createLiveQueryObserver(currentCollection)
const unsubscribe = observer.subscribe(
// The shared observer owns subscription, the ready-race, and status; Solid
// materializes into its keyed ReactiveMap (granular) + reconciled store.
const observer = createLiveQueryObserver(currentCollection)
state.clear()
const unsubscribe = observer.subscribe(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/solid-db/src/useLiveQuery.ts` around lines 393 - 397, Clear the
existing Solid state before wiring up the new observer in useLiveQuery so stale
rows from the previous collection are removed when collection() changes. Add the
reset immediately before createLiveQueryObserver/currentCollection subscription
setup, and keep the logic localized around observer.subscribe so
includeInitialState only repopulates the new collection instead of leaving old
keys behind.

- observer: getSnapshot() rebuilds when collection.status changes without a
  version bump (status-only loading→ready / preload with no active subscription),
  so a cached snapshot can't go stale.
- observer: guard the deferred initial-notify microtask with the attach
  generation + listener count, so a superseded attach can't flush a stale
  initial batch to a later listener.
- react: don't dispose the previous observer during render (unsafe under
  concurrent rendering) — useSyncExternalStore detaches it when the subscribe
  changes; dispose the current observer in an unmount effect instead.
- tests: regressions for the deferred-notify race and the status-only snapshot
  refresh (both verified red before the fixes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react-db/src/useLiveQuery.ts`:
- Around line 428-434: The unmount cleanup in useLiveQuery’s observer lifecycle
is disposing the shared observer too early, which can leave observerRef.current
pointing to a disposed instance during StrictMode/offscreen replay. Update the
useEffect cleanup to avoid disposing the observer there, or ensure the observer
is recreated before reuse when attach() is called on a disposed instance. Keep
the fix localized to useLiveQuery and the observerRef/useSyncExternalStore
subscription path so the next subscribe always gets a live observer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a7b1d806-2a37-4fb2-b0ff-7b3a757303ed

📥 Commits

Reviewing files that changed from the base of the PR and between e957121 and c05a726.

📒 Files selected for processing (3)
  • packages/db/src/live-query-observer.ts
  • packages/db/tests/live-query-observer.test.ts
  • packages/react-db/src/useLiveQuery.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/db/src/live-query-observer.ts

Comment thread packages/react-db/src/useLiveQuery.ts Outdated
…tMode)

The unmount-effect dispose could run during StrictMode/offscreen effect replay
(mount → cleanup → mount) without a re-render, leaving observerRef pointing at a
disposed observer; the next subscribe hit attach()'s disposed guard and the
store stopped resubscribing. Remove the explicit dispose — useSyncExternalStore
already detaches the observer on unsubscribe/unmount, so the collection
subscription is torn down and the observer is GC'd. Adds a StrictMode regression
test (verified red before the fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants