Skip to content

fix: invalidate stale predicate-scoped cache entries on manual-sync writes#1497

Open
marbemac wants to merge 4 commits into
TanStack:mainfrom
marbemac:mbm/manual-sync-bug
Open

fix: invalidate stale predicate-scoped cache entries on manual-sync writes#1497
marbemac wants to merge 4 commits into
TanStack:mainfrom
marbemac:mbm/manual-sync-bug

Conversation

@marbemac

@marbemac marbemac commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Manual-sync writes in on-demand Query Collections now preserve predicate-, order-, and pagination-scoped Query cache results. Active queries revalidate from their source, while inactive or disabled entries are removed so later subscriptions fetch fresh data.

Root cause

Each on-demand Query cache entry belongs to one queryFn result and may encode a distinct where, orderBy, limit, or offset. Manual writes replaced every entry under the collection's base key with the full normalized collection snapshot. That changed the shape and membership of scoped cache entries.

An inactive poisoned entry could later be treated as a valid cache hit. Its rows would be reconciled through the wrong query ownership scope, which surfaced in production as “not found” results for rows that still existed in Postgres. Active limited or ordered queries had the same flaw: patching them from the normalized snapshot could widen the cache or fail to replenish a deleted row.

Approach

  • Keep eager behavior unchanged: eager collections patch their full-result cache in place.
  • Never write the full normalized snapshot into on-demand cache entries.
  • Refetch active, fetchable observers directly. This also revalidates queries with staleTime: 'static'.
  • Remove inactive and disabled entries in one cache operation so their next subscription runs queryFn.
  • Ignore success notifications that still carry the exact pre-write dataUpdateCount, then accept the next authoritative result.
  • Clear the notification marker on observer cleanup and Query reset or recreation paths.
  • Wait for an optimistic transaction's deferred-refresh barrier before refetching, and revalidate after either barrier outcome.

Key invariants

  • An on-demand Query cache entry only contains data shaped by its own queryFn.
  • A direct write remains visible in the synced store immediately.
  • Fetch and invalidation notifications cannot replace that write with unchanged cached data.
  • A later authoritative result always reconciles the collection, including after Query state resets.

Non-goals and trade-offs

This does not change eager cache updates or the direct-write API. Active on-demand queries now incur a network refetch after a manual write; that cost is required because a normalized collection snapshot cannot reconstruct ordered, limited, paginated, or predicate-scoped results.

Verification

pnpm exec vitest run --config packages/query-db-collection/vitest.config.ts packages/query-db-collection/tests/query.test.ts --maxWorkers=2
pnpm exec tsc --noEmit -p packages/query-db-collection/tsconfig.json
pnpm --filter @tanstack/query-db-collection build
pnpm exec eslint packages/query-db-collection/src/query.ts packages/query-db-collection/tests/query.test.ts
pnpm test:docs

The package suite passes all 146 tests. New regressions cover inactive cache hits, overlapping scopes, active limited and static queries, disabled entries, in-flight initial loads, Query resets, and rejected deferred-refresh barriers.

Files changed

  • packages/query-db-collection/src/query.ts: revalidate or remove on-demand cache entries and guard stale notifications.
  • packages/query-db-collection/tests/query.test.ts: add red/green coverage for scoped-cache and lifecycle cases.
  • docs/collections/query-collection.md: document eager and on-demand direct-write behavior.
  • docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md: update generated API reference text.
  • .changeset/fix-cache-poisoning-on-manual-writes.md: add the patch release note.

Release impact

  • This change affects published code and includes a changeset.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed manual writes in on-demand collections so they no longer overwrite filtered, sorted, or paginated cache results with a full snapshot.
    • Active queries now refresh with current data, while inactive or disabled entries are removed.
    • Prevented stale or incorrect results after direct inserts, updates, and deletes.
  • Documentation

    • Clarified cache behavior for direct writes across eager and on-demand collections.
    • Added guidance for scoped queries, persistence handlers, and deferred refreshes.

@pkg-pr-new

pkg-pr-new Bot commented Apr 26, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-db

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

@tanstack/browser-db-sqlite-persistence

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

@tanstack/capacitor-db-sqlite-persistence

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

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

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/db-sqlite-persistence-core

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

@tanstack/electric-db-collection

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

@tanstack/electron-db-sqlite-persistence

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

@tanstack/expo-db-sqlite-persistence

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

@tanstack/node-db-sqlite-persistence

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

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

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/tauri-db-sqlite-persistence

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: f55784a

@andyfelder16

Copy link
Copy Markdown

any news on this? we hit the same bug :( — for now we are avoiding the write* utils entirely on on-demand collections and reconciling with refetch() instead, so it would be great to see this land

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Direct writes now patch eager caches but revalidate active scoped on-demand queries and remove inactive or disabled entries. Query observer tracking prevents duplicate reconciliation, while tests cover query-key variants, predicates, pagination, deferred refreshes, and preload behavior.

Changes

Manual-write cache invalidation

Layer / File(s) Summary
Cache behavior contracts
.changeset/..., docs/collections/query-collection.md, docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md, packages/query-db-collection/src/query.ts
Documentation and release notes describe sync-mode-specific direct-write behavior and scoped on-demand revalidation.
Scoped cache update flow
packages/query-db-collection/src/query.ts
On-demand writes refetch active observers, remove non-refetching entries, and track observer update counts; eager writes retain full-cache updates.
Invalidation behavior validation
packages/query-db-collection/tests/query.test.ts
Tests cover refetching, predicate-scoped cache behavior, disabled entries, limited queries, deferred refreshes, and in-flight preloads.

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

Sequence Diagram(s)

sequenceDiagram
  participant DirectWrite
  participant updateCacheData
  participant QueryObserver
  participant queryFn
  DirectWrite->>updateCacheData: update cache after synced-store write
  updateCacheData->>QueryObserver: refetch active scoped observers
  QueryObserver->>queryFn: request scoped results
  queryFn-->>QueryObserver: return refreshed data
Loading

Possibly related PRs

  • TanStack/db#1655: Adds related tests for refetch and cache invalidation behavior in query.test.ts.

Suggested reviewers: kyleamathews, kevin-dp

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title is concise and directly matches the main fix: preventing stale predicate-scoped cache entries from manual-sync writes.
Description check ✅ Passed The description covers the change, rationale, verification, files changed, and release impact, even though it doesn't follow the template headings exactly.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@KyleAMathews

Copy link
Copy Markdown
Collaborator

@marbemac @andyfelder16 could you test this against my latest pushes? I merged in origin/main & did a few fixes

@KyleAMathews
KyleAMathews requested a review from kevin-dp July 21, 2026 19:54

@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 (1)
packages/query-db-collection/tests/query.test.ts (1)

8676-8677: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the double setQueryData with an identity updater.

Two back-to-back identity setQueryData calls are non-obvious; they appear intended to advance dataUpdateCount so the later resetQueries exercises the stale-count path. A one-line comment explaining why the count is bumped twice would help future readers.

🤖 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/query-db-collection/tests/query.test.ts` around lines 8676 - 8677,
Add a concise comment immediately before the duplicate
customQueryClient.setQueryData calls explaining that the identity updates
intentionally increment dataUpdateCount twice so the subsequent resetQueries
test covers the stale-count path; leave both calls unchanged.
🤖 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/query-db-collection/tests/query.test.ts`:
- Around line 8676-8677: Add a concise comment immediately before the duplicate
customQueryClient.setQueryData calls explaining that the identity updates
intentionally increment dataUpdateCount twice so the subsequent resetQueries
test covers the stale-count path; leave both calls unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: abffe3a9-0247-453a-9e5c-a6a3fe9b8dcc

📥 Commits

Reviewing files that changed from the base of the PR and between f42db5c and f55784a.

📒 Files selected for processing (5)
  • .changeset/fix-cache-poisoning-on-manual-writes.md
  • docs/collections/query-collection.md
  • docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md
  • packages/query-db-collection/src/query.ts
  • packages/query-db-collection/tests/query.test.ts

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