Skip to content

fix(db): useLiveInfiniteQuery pagination with async on-demand loadSubset#1209

Open
KyleAMathews wants to merge 9 commits intomainfrom
claude/investigate-live-infinite-query-bug-aWea8
Open

fix(db): useLiveInfiniteQuery pagination with async on-demand loadSubset#1209
KyleAMathews wants to merge 9 commits intomainfrom
claude/investigate-live-infinite-query-bug-aWea8

Conversation

@KyleAMathews
Copy link
Collaborator

@KyleAMathews KyleAMathews commented Feb 2, 2026

Summary

Fixes useLiveInfiniteQuery not fetching subsequent pages when used with an on-demand collection that has an async loadSubset implementation (like Electric).

Two bugs were identified and fixed:

  1. Peek-ahead detection: The initial query used .limit(pageSize) instead of .limit(pageSize + 1), so hasNextPage was always false

  2. Async pagination: When setWindow() was called for the next page, loadMoreIfNeeded was never triggered because the graph had no pending work

Changes

  • useLiveInfiniteQuery.ts: Use pageSize + 1 in initial query for peek-ahead
  • collection-config-builder.ts: Call the graph callback at least once even when there's no pending work, so setWindow() can trigger lazy loading
  • Added comprehensive tests for on-demand collection pagination (sync and async)

This test documents a bug where useLiveInfiniteQuery doesn't request
pageSize+1 items from loadSubset for hasNextPage peek-ahead detection.

The bug causes hasNextPage to always return false when using on-demand
sync mode with Electric collections, because:

1. useLiveInfiniteQuery calls setWindow({ limit: pageSize + 1 }) in useEffect
2. But subscribeToOrderedChanges calls requestLimitedSnapshot BEFORE the
   useEffect runs, using the original compiled limit (pageSize)
3. The loadSubset function receives limit=pageSize instead of limit=pageSize+1
4. This prevents the peek-ahead strategy from working correctly

Related: Discord bug report about useLiveInfiniteQuery + Electric on-demand

https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
@changeset-bot
Copy link

changeset-bot bot commented Feb 2, 2026

🦋 Changeset detected

Latest commit: 2186630

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

This PR includes changesets to release 1 package
Name Type
@tanstack/react-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 Feb 2, 2026

More templates

@tanstack/angular-db

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/electric-db-collection

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: 2186630

@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

Size Change: +10 B (+0.01%)

Total Size: 92 kB

Filename Size Change
./packages/db/dist/esm/query/live/collection-config-builder.js 5.44 kB +10 B (+0.18%)
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.39 kB
./packages/db/dist/esm/collection/changes.js 1.22 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/lifecycle.js 1.75 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/subscription.js 3.71 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/collection-subscriber.js 2.42 kB
./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 Feb 2, 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

claude and others added 4 commits February 2, 2026 08:46
The initial query was using `.limit(pageSize)` but `setWindow` expects
`pageSize + 1` for peek-ahead detection. This caused a race condition
where the first `requestLimitedSnapshot` was called with `limit = pageSize`
before `setWindow` could adjust it to `pageSize + 1`.

The fix uses `pageSize + 1` from the start so the compiled query includes
the peek-ahead limit, ensuring `loadSubset` receives the correct limit
for `hasNextPage` detection.

https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
- Fix unused parameter lint warnings (allPages -> _allPages)
- Simplify test logic using .find() instead of .filter()[0]
- Condense redundant comments

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

@samwillis samwillis left a comment

Choose a reason for hiding this comment

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

I think this looks good, I believe the bug was that the current tests don't trigger a pushdown to load more data from the server. So while we are calling fetchNextPage to paginate, and are checking the behaviour of hasNextPage, it's not checked against on-demand collections.

I would suggest one thing, the new test is checking for a specific internal detail (we query for offset+), but really the tests should be asserting that an on demand collection as the source with incremental sync does work with useLiveInfiniteQuery. Maybe ask Claude to add those tests.

Replaces the previous implementation-detail test with a proper e2e test
that verifies the actual behavior of useLiveInfiniteQuery with on-demand
collections:

- Initial page loads correctly with hasNextPage=true
- fetchNextPage() actually loads more data via loadSubset
- Multiple pages can be fetched with correct items
- hasNextPage correctly reflects when no more data exists

This test catches bugs where the incremental sync doesn't properly
fetch data from the backend when paginating.

https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
This test reproduces a bug where useLiveInfiniteQuery doesn't fetch
subsequent pages when loadSubset returns a Promise (async mode).

Root cause identified in collection-subscriber.ts:
- When loadSubset returns a Promise, pendingOrderedLoadPromise is set
- loadMoreIfNeeded returns early while the promise is pending
- When the promise resolves, pendingOrderedLoadPromise is cleared
- BUT loadMoreIfNeeded is NOT re-triggered to check if more data is needed

This affects Electric on-demand mode where all data comes from async
loadSubset calls. The initial page loads correctly, but fetchNextPage
fails to trigger additional loadSubset calls.

https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
@KyleAMathews KyleAMathews changed the title test: add peek-ahead limit detection test for useLiveInfiniteQuery fix(db): useLiveInfiniteQuery pagination with async on-demand loadSubset Feb 2, 2026
When useLiveInfiniteQuery uses an on-demand collection with async
loadSubset, the second page was never loaded because:

1. When setWindow() was called for the next page, maybeRunGraph's
   callback was never called because the graph had no pending work

This fix ensures the graph run callback is called at least once even
when there's no pending work, so setWindow() can trigger loadMoreIfNeeded
for lazy loading scenarios.

https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
@KyleAMathews KyleAMathews force-pushed the claude/investigate-live-infinite-query-bug-aWea8 branch from 62e4bc3 to 2186630 Compare February 2, 2026 10:13
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.

3 participants