Skip to content

Conversation

@KyleAMathews
Copy link
Collaborator

@KyleAMathews KyleAMathews commented Jan 27, 2026

Summary

Fixes isReady tracking for on-demand live queries. This PR addresses two related issues:

  1. Non-ordered queries: Were incorrectly marked as ready before data finished loading
  2. Ordered queries with paging: Could hang indefinitely when multiple live queries share a source collection or when paging with setWindow

Also fixes preload() promises hanging when cleanup occurs before the collection becomes ready.

Root Cause

Issue 1: Non-ordered queries (original fix)

For non-ordered live queries, subscribeChanges is called with includeInitialState: true, which internally calls requestSnapshot({ trackLoadSubsetPromise: false }). This disables the subscription's status tracking—the subscription never transitions to loadingSubset status, so the live query's isReady stayed true during loading.

Issue 2: Ordered paging hang (this session)

requestLimitedSnapshot used limitedSnapshotRowCount to compute offset for offset-based backends. That counter only advanced when the local index produced rows. When loadSubset resolved asynchronously (cached query observer) without local rows at request time, the counter stayed at 0. Subsequent loadNextItems calls kept requesting offset 0, re-writing the same rows and scheduling loadMore again, leading to an infinite loop.

History: Introduced in commit b3b1940 (2025-12-12) when limitedSnapshotRowCount/offset pagination was added; the counter never advanced for async loadSubset results. The hang has been possible since then in on-demand ordered/limited queries (especially with multiple live queries or window moves).

Approach

For non-ordered queries

The fix couldn't simply change trackLoadSubsetPromise to true because that breaks truncate handling (must-refetch scenarios). The truncate buffering logic depends on the existing status behavior.

Instead, the fix tracks loading at the collection level (collection-subscriber.ts): After subscribing, checks if the source collection's isLoadingSubset changed. If so, listens for the loadingSubset:change event and tracks a promise on the live query collection for isReady.

For ordered paging

  1. Keep offset in sync (subscription.ts): Sync limitedSnapshotRowCount with sentKeys.size after callbacks—advances the offset even when async loadSubset results arrive without local index rows

  2. Deduplicate by request parameters (collection-subscriber.ts): Track lastLoadRequestKey (serialized cursor + window) to prevent repeated identical requests while still allowing window moves (different offset/limit)

  3. Track seen keys (collection-subscriber.ts): Use seenKeys to detect genuinely new rows (as opposed to repeated updates for the same keys) and reset the dedup flag when new keys arrive

  4. Pass orderBy/limit to snapshot requests: Ensures each live query with different parameters gets its own cache entry in on-demand source collections

Cleanup handling

  • Calls onFirstReady callbacks during cleanup (lifecycle.ts) so preload() promises resolve instead of hanging
  • Cleans up event listeners on early unsubscribe to prevent memory leaks

Key Invariants

  1. isReady must return false while loadSubset is in progress
  2. preload() must always resolve (either when ready OR during cleanup)
  3. Truncate handling must continue to work correctly (buffering events during must-refetch)
  4. Event listeners must be cleaned up on early unsubscribe to prevent memory leaks
  5. Multiple live queries sharing a source collection must independently track their loading state
  6. Window moves (setWindow) must trigger new data fetches even if cursor hasn't advanced

Regression Tests Added

  • live-query-collection: advances offset after async loadSubset when initial window is empty
  • live-query-collection: window moves across identical orderBy values request new offsets
  • e2e pagination: multiple live queries with distinct windows + paging a second query

Verification

pnpm --filter @tanstack/db exec vitest run
pnpm --filter @tanstack/query-db-collection exec vitest run
pnpm --filter @tanstack/query-db-collection exec vitest run --config vitest.e2e.config.ts

All tests pass:

  • db: 1935 passed (3 skipped)
  • query-db-collection: 187 passed
  • e2e: 112 passed

Files Changed

  • packages/db/src/collection/subscription.ts - Sync offset counter with sentKeys, add onLoadSubsetResult/trackLoadSubsetPromise options
  • packages/db/src/query/live/collection-subscriber.ts - Deduplicate by request parameters, track seen keys, direct load promise tracking
  • packages/db/src/query/live/collection-config-builder.ts - Pass orderBy/limit to snapshot requests
  • packages/db/src/collection/lifecycle.ts - Call onFirstReady callbacks during cleanup

KyleAMathews and others added 2 commits January 27, 2026 09:52
When a live query without orderBy/limit subscribes to an on-demand
collection, the subscription was passing includeInitialState: true to
subscribeChanges, which internally called requestSnapshot with
trackLoadSubsetPromise: false. This prevented the subscription status
from transitioning to 'loadingSubset', causing the live query to be
marked ready before data actually loaded.

The fix changes subscribeToMatchingChanges to manually call
requestSnapshot() after creating the subscription (with default
tracking enabled), ensuring the loadSubset promise is properly
tracked and the live query waits for data before becoming ready.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This follow-up fix addresses edge cases that caused tests to fail after
the initial isReady fix:

1. lifecycle.ts: Call pending onFirstReady callbacks during cleanup
   - Prevents preload() promises from hanging when cleanup happens
   - Ensures clean termination of pending preload operations

2. query.ts: Add fallback cache checks in createQueryFromOpts
   - Check QueryClient cache when observer state is out of sync
   - Handle cases where observer was deleted but data is still cached
   - Prevents hangs during cleanup/recreate cycles

3. Test updates:
   - Updated where clause tests to use synchronous loadSubset
   - These tests verify where clause passing, not async loading behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Jan 27, 2026

🦋 Changeset detected

Latest commit: 133a697

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@tanstack/db Patch
@tanstack/query-db-collection Patch
@tanstack/angular-db Patch
@tanstack/electric-db-collection Patch
@tanstack/offline-transactions Patch
@tanstack/powersync-db-collection Patch
@tanstack/react-db Patch
@tanstack/rxdb-db-collection Patch
@tanstack/solid-db Patch
@tanstack/svelte-db Patch
@tanstack/trailbase-db-collection Patch
@tanstack/vue-db Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 27, 2026

More templates

@tanstack/angular-db

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/electric-db-collection

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: 133a697

@github-actions
Copy link
Contributor

github-actions bot commented Jan 27, 2026

Size Change: +671 B (+0.73%)

Total Size: 92 kB

Filename Size Change
./packages/db/dist/esm/collection/changes.js 1.22 kB +31 B (+2.6%)
./packages/db/dist/esm/collection/lifecycle.js 1.75 kB +70 B (+4.16%)
./packages/db/dist/esm/collection/subscription.js 3.71 kB +69 B (+1.9%)
./packages/db/dist/esm/query/live/collection-config-builder.js 5.43 kB +11 B (+0.2%)
./packages/db/dist/esm/query/live/collection-subscriber.js 2.42 kB +490 B (+25.36%) 🚨
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.39 kB
./packages/db/dist/esm/collection/events.js 388 B
./packages/db/dist/esm/collection/index.js 3.32 kB
./packages/db/dist/esm/collection/indexes.js 1.1 kB
./packages/db/dist/esm/collection/mutations.js 2.34 kB
./packages/db/dist/esm/collection/state.js 3.49 kB
./packages/db/dist/esm/collection/sync.js 2.41 kB
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.7 kB
./packages/db/dist/esm/event-emitter.js 748 B
./packages/db/dist/esm/index.js 2.69 kB
./packages/db/dist/esm/indexes/auto-index.js 742 B
./packages/db/dist/esm/indexes/base-index.js 766 B
./packages/db/dist/esm/indexes/btree-index.js 2.17 kB
./packages/db/dist/esm/indexes/lazy-index.js 1.1 kB
./packages/db/dist/esm/indexes/reverse-index.js 538 B
./packages/db/dist/esm/local-only.js 837 B
./packages/db/dist/esm/local-storage.js 2.1 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 733 B
./packages/db/dist/esm/query/builder/index.js 4.09 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 1.05 kB
./packages/db/dist/esm/query/compiler/evaluators.js 1.42 kB
./packages/db/dist/esm/query/compiler/expressions.js 430 B
./packages/db/dist/esm/query/compiler/group-by.js 1.81 kB
./packages/db/dist/esm/query/compiler/index.js 2.02 kB
./packages/db/dist/esm/query/compiler/joins.js 2.07 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.45 kB
./packages/db/dist/esm/query/compiler/select.js 1.06 kB
./packages/db/dist/esm/query/expression-helpers.js 1.43 kB
./packages/db/dist/esm/query/ir.js 673 B
./packages/db/dist/esm/query/live-query-collection.js 360 B
./packages/db/dist/esm/query/live/collection-registry.js 264 B
./packages/db/dist/esm/query/live/internal.js 145 B
./packages/db/dist/esm/query/optimizer.js 2.56 kB
./packages/db/dist/esm/query/predicate-utils.js 2.97 kB
./packages/db/dist/esm/query/subset-dedupe.js 921 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 2.9 kB
./packages/db/dist/esm/utils.js 924 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 952 B
./packages/db/dist/esm/utils/cursor.js 457 B
./packages/db/dist/esm/utils/index-optimization.js 1.51 kB
./packages/db/dist/esm/utils/type-guards.js 157 B

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

@github-actions
Copy link
Contributor

github-actions bot commented Jan 27, 2026

Size Change: 0 B

Total Size: 3.7 kB

ℹ️ View Unchanged
Filename Size
./packages/react-db/dist/esm/index.js 225 B
./packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.17 kB
./packages/react-db/dist/esm/useLiveQuery.js 1.34 kB
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 559 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
autofix-ci bot and others added 3 commits January 27, 2026 18:49
- Fix potential memory leak in trackCollectionLoading by registering
  cleanup callback for early unsubscribe scenarios
- Add error handling for onFirstReady callbacks during cleanup to
  ensure all callbacks are attempted even if one throws

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, when multiple live queries subscribed to the same source
collection, only the first would correctly track loading state due to
the `!wasLoadingBefore` guard. This caused subsequent live queries to
incorrectly report `isReady` before data finished loading.

The fix removes this guard since each live query needs its own loading
state tracking regardless of whether another query already triggered
loading.

Also adds a regression test for this scenario.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@chicojasl
Copy link

Verified this locally. Working great! 👍

Copy link
Contributor

@kevin-dp kevin-dp left a comment

Choose a reason for hiding this comment

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

Code quality looks good to me. Just left a small comment.
There are quite some changes to subtle parts of the code, not sure i understand all of them. The changes may have some important implications and complicate this already complicated code a bit more by introducing yet some more state variables loadRequestedForCurrentCursor, lastLoadRequestKey, seenKeys. We know that previous bugs have been caused by all these state variables and some code paths not handling/resetting them properly. So we need to be careful about that.

// The onStatusChange listener will catch the transition to 'ready'.
if (subscription.status === `loadingSubset`) {
trackLoadPromise(subscription)
if (!this.subscriptionLoadingPromises.has(subscription)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This code is duplicated on line 93. It used to call trackLoadPromise. We should extract this duplicate code into a function (if trackLoadPromise isn't the right one to use).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good call — working on adding a helper to make the state changes cleaner/less bug prone

KyleAMathews and others added 3 commits January 29, 2026 09:16
- Remove redundant has() check before Set.add() (idempotent operation)
- Replace nested ternary with clearer if-statement for minValues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@KyleAMathews KyleAMathews merged commit 284ebcc into main Jan 29, 2026
7 checks passed
@KyleAMathews KyleAMathews deleted the is-ready-redux branch January 29, 2026 16:47
@github-actions github-actions bot mentioned this pull request Jan 29, 2026
@github-actions
Copy link
Contributor

🎉 This PR has been released!

Thank you for your contribution!

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.

4 participants