Skip to content

refactor(db): extract shared live-query adapter helpers (RFC #1623 step 2)#1641

Open
kevin-dp wants to merge 1 commit into
mainfrom
refactor/extract-adapter-helpers
Open

refactor(db): extract shared live-query adapter helpers (RFC #1623 step 2)#1641
kevin-dp wants to merge 1 commit into
mainfrom
refactor/extract-adapter-helpers

Conversation

@kevin-dp

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

Copy link
Copy Markdown
Contributor

The first real deduplication slice of RFC #1623 — the first PR that touches adapter src/ and shrinks copy-paste. Follows the conformance suite (#1636), which guards this refactor.

Base: stacked on test/live-query-conformance-suite (#1636) so the conformance suite verifies the refactored adapters. Re-target to main once #1636 lands.

What's extracted

Three helpers added to @tanstack/db (packages/db/src/live-query-adapter.ts), and all five adapters migrated to them:

  • isCollection(value) — one structural, multi-realm-safe collection guard. Replaces the duck-typed check copy-pasted in React/Vue/Svelte/Angular and Solid's instanceof CollectionImpl. instanceof gives false negatives across dual-package/multi-realm boundaries — the exact hazard the conformance suite hit — so consolidating on duck typing is also a correctness win.
  • isSingleResultCollection(collection) — shared findOne cardinality check (was config.singleResult, five times).
  • getLiveQueryStatusFlags(status)status → { isLoading, isReady, isIdle, isError, isCleanedUp }.

Honest scope note

This slice is smaller than the RFC's "7 duplicated items" might suggest — and that's a real finding. Only the reactivity-free logic lifts cleanly into plain functions:

  • getLiveQueryStatusFlags fits React's snapshot path, but the reactive adapters (Vue/Svelte/Solid/Angular) each expose every flag as its own computed/signal, so an object-returning helper doesn't fit them — forcing it would add code.
  • Input/disabled classification, status derivation, and change→state projection are all coupled to each framework's reactivity. They can't be lifted into plain helpers without the observer.

So this PR takes the clean, universal wins now (notably unifying isCollection and killing the instanceof divergence). The larger dedup is exactly what the observer (step 3) exists to do — and per the design decision, that observer will carry both a snapshot and the ChangeMessage[] so granular adapters (Vue/Svelte) keep fine-grained updates while others consume the snapshot.

Verification

No behavior change. All five adapter suites green (including the conformance suite that guards this), and @tanstack/db (2456 tests) green. Minor changeset for @tanstack/db (new exports) + patch for the adapters.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added shared live-query helpers for detecting collections, handling single-result collections, and mapping query status flags.
    • Framework adapters now use the same shared logic for live queries, improving consistency across React, Vue, Svelte, Solid, and Angular.
  • Bug Fixes

    • Improved collection detection in cases where objects come from different runtime contexts.
    • Single-result live queries now return the expected item more reliably across adapters.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Shared live-query adapter helper functions (isCollection, isSingleResultCollection, getLiveQueryStatusFlags) are added to @tanstack/db and re-exported. React, Vue, Svelte, Solid, and Angular useLiveQuery/injectLiveQuery implementations are migrated to use these shared helpers, replacing per-adapter duck-typing and instanceof checks.

Changes

Live-query adapter helper extraction and migration

Layer / File(s) Summary
Shared helper functions and re-export
packages/db/src/live-query-adapter.ts, packages/db/src/index.ts, .changeset/extract-adapter-helpers.md
Adds isCollection, isSingleResultCollection, LiveQueryStatusFlags, and getLiveQueryStatusFlags, re-exports them from the package, and records the change in a changeset with version bumps.
React adapter migration
packages/react-db/src/useLiveQuery.ts
Replaces CollectionImpl instanceof/manual checks with isCollection, uses isSingleResultCollection for snapshot single-result detection, and derives status flags via getLiveQueryStatusFlags.
Solid adapter migration
packages/solid-db/src/useLiveQuery.ts
Replaces instanceof CollectionImpl checks with isCollection and switches single-result detection in getData() to isSingleResultCollection.
Svelte adapter migration
packages/svelte-db/src/useLiveQuery.svelte.ts
Replaces local structural collection checks with isCollection and single-result config checks with isSingleResultCollection.
Vue adapter migration
packages/vue-db/src/useLiveQuery.ts
Replaces manual method-based collection detection with isCollection and single-result config checks with isSingleResultCollection.
Angular adapter migration
packages/angular-db/src/index.ts
Replaces manual pre-created collection detection with isCollection, single-result config check with isSingleResultCollection, and casts the returned collection signal to Signal<any>.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: samwillis

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main refactor and matches the changes in the PR.
Description check ✅ Passed The description covers the extracted helpers, rationale, and verification, though it does not follow the template's exact checklist/release-impact sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/extract-adapter-helpers

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@1641

@tanstack/browser-db-sqlite-persistence

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

@tanstack/capacitor-db-sqlite-persistence

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

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

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/db-sqlite-persistence-core

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

@tanstack/electric-db-collection

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

@tanstack/electron-db-sqlite-persistence

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

@tanstack/expo-db-sqlite-persistence

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

@tanstack/node-db-sqlite-persistence

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

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

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/tauri-db-sqlite-persistence

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: 4cf25ae

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Size Change: +383 B (+0.31%)

Total Size: 125 kB

📦 View Changed
Filename Size Change
packages/db/dist/esm/index.js 3.16 kB +65 B (+2.1%)
packages/db/dist/esm/live-query-adapter.js 318 B +318 B (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/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.26 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.37 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 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/extract-adapter-helpers branch from 84895f6 to 84f4e93 Compare July 7, 2026 09:55
Base automatically changed from test/live-query-conformance-suite to main July 7, 2026 10:20
Add isCollection, isSingleResultCollection, and getLiveQueryStatusFlags to
@tanstack/db and migrate all five adapters to use them.

- isCollection: one structural, multi-realm-safe collection guard replacing the
  per-adapter duck-typing (React/Vue/Svelte/Angular) and Solid's
  `instanceof CollectionImpl` (which gives false negatives across dual-package
  boundaries — the same hazard the conformance suite hit).
- isSingleResultCollection: shared findOne cardinality check.
- getLiveQueryStatusFlags: status → {isLoading,isReady,isIdle,isError,isCleanedUp};
  used by React's snapshot path (the reactive adapters derive each flag as its own
  signal/computed, so a shared object-returning helper doesn't fit them — that
  duplication is reactivity-coupled and belongs to the observer step).

No behavior change; guarded by the conformance suite. First slice of the RFC
#1623 extraction (step 2). Status derivation, input/disabled classification, and
change→state projection remain — they are reactivity-coupled and land with the
observer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@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 marked this pull request as ready for review July 7, 2026 12:47
@kevin-dp kevin-dp requested review from KyleAMathews and samwillis and removed request for KyleAMathews July 7, 2026 12:47

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

🧹 Nitpick comments (2)
packages/angular-db/src/index.ts (1)

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

Prefer a narrower cast than Signal<any> for collection.

Casting to Signal<any> discards all type information from the declared InjectLiveQueryResult/InjectLiveQueryResultWithSingleResultCollection return shapes (Signal<Collection<...> | null>), so the implementation body is no longer checked against either overload's collection field. 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."

♻️ Suggested refactor
-    // Loosely typed so the impl return stays compatible with every overload
-    // (the shared `isCollection` guard narrows the computed to `Collection | null`).
-    collection: collection as Signal<any>,
+    // Loosely typed so the impl return stays compatible with every overload
+    // (the shared `isCollection` guard narrows the computed to `Collection | null`).
+    collection: collection as Signal<Collection<any, any, any> | null>,
🤖 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/angular-db/src/index.ts` around lines 281 - 283, The `collection`
field in the `injectLiveQuery` return object is cast too broadly to
`Signal<any>`, which erases the intended
`InjectLiveQueryResult`/`InjectLiveQueryResultWithSingleResultCollection`
typing. Update the implementation to use a narrower, specific signal type that
matches the computed `Collection<...> | null` shape (or a shared
union/unknown-based annotation if needed) so the overload contract stays
checked. Locate the change in `injectLiveQuery` where `collection` is assigned
and replace the `Signal<any>` cast with a properly typed return annotation or
assertion.

Source: Coding guidelines

packages/db/src/live-query-adapter.ts (1)

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

Avoid any inside this shared guard. Cast value once to Record<string, unknown> after the object check, then probe the properties from that typed reference.

🤖 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-adapter.ts` around lines 22 - 32, The shared type
guard in isCollection currently uses repeated any casts to inspect properties;
replace those with a single post-null object cast to Record<string, unknown> and
read subscribeChanges, startSyncImmediate, and id from that typed reference.
Keep the same runtime checks, but ensure the guard no longer relies on any
inside the predicate.

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.

Nitpick comments:
In `@packages/angular-db/src/index.ts`:
- Around line 281-283: The `collection` field in the `injectLiveQuery` return
object is cast too broadly to `Signal<any>`, which erases the intended
`InjectLiveQueryResult`/`InjectLiveQueryResultWithSingleResultCollection`
typing. Update the implementation to use a narrower, specific signal type that
matches the computed `Collection<...> | null` shape (or a shared
union/unknown-based annotation if needed) so the overload contract stays
checked. Locate the change in `injectLiveQuery` where `collection` is assigned
and replace the `Signal<any>` cast with a properly typed return annotation or
assertion.

In `@packages/db/src/live-query-adapter.ts`:
- Around line 22-32: The shared type guard in isCollection currently uses
repeated any casts to inspect properties; replace those with a single post-null
object cast to Record<string, unknown> and read subscribeChanges,
startSyncImmediate, and id from that typed reference. Keep the same runtime
checks, but ensure the guard no longer relies on any inside the predicate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f92418ce-9388-43da-b94f-d8cbe3d645d6

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec3e7a and 4cf25ae.

📒 Files selected for processing (8)
  • .changeset/extract-adapter-helpers.md
  • packages/angular-db/src/index.ts
  • packages/db/src/index.ts
  • packages/db/src/live-query-adapter.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

@KyleAMathews KyleAMathews left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

:shipit:

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