Skip to content

fix(query-core): stop orphaned streamedQuery chunks from corrupting the cache#11068

Open
a-y-ibrahim wants to merge 4 commits into
TanStack:mainfrom
a-y-ibrahim:fix/streamed-query-orphaned-stream-cache-corruption
Open

fix(query-core): stop orphaned streamedQuery chunks from corrupting the cache#11068
a-y-ibrahim wants to merge 4 commits into
TanStack:mainfrom
a-y-ibrahim:fix/streamed-query-orphaned-stream-cache-corruption

Conversation

@a-y-ibrahim

@a-y-ibrahim a-y-ibrahim commented Jul 16, 2026

Copy link
Copy Markdown

🎯 Changes

When a streamedQuery-backed query is refetched while a previous stream is still flowing, the old fetch is cancelled but its streamFn keeps running unless it explicitly reads context.signal. Many streamFns can't do that (e.g. a third-party SDK with no AbortSignal support), so the orphaned stream kept calling setQueryData after the new fetch had already reset the cache, interleaving stale chunks into what should be fresh data.

This tracks the current stream per Query independently of signal consumption, so an orphaned stream notices it has been superseded and stops writing, regardless of whether its streamFn ever touches the signal.

Reproduced and fixed with a new test (should not let an orphaned stream write to the cache after a refetch when the signal is not consumed) that fails on current main and passes with this change.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed cases where “orphaned” streamed query chunks could overwrite newer cached results after a refetch.
    • Ensured refetches in replace mode don’t commit stale superseded stream results.
  • Tests
    • Added refetch supersession coverage to verify only the latest streamed output is written to the cache after restarts/cancellations.

…he cache

When a streamedQuery-backed query is refetched while a previous stream is
still flowing, the old fetch is cancelled but its streamFn keeps running
unless it explicitly reads context.signal. Many streamFns can't do that
(e.g. a third-party SDK with no AbortSignal support), so the orphaned
stream kept calling setQueryData after the new fetch had already reset
the cache, interleaving stale chunks into what should be fresh data.

Track the current stream per Query independently of signal consumption,
so an orphaned stream notices it has been superseded and stops writing
regardless of whether its streamFn ever touches the signal.
Minor consistency cleanup, no behavior change: query is always Query |
undefined here (Array.prototype.find never returns null), so !!query
and query !== undefined are equivalent; use the same !!query idiom the
file already uses a few lines above instead of mixing two styles.
Copilot AI review requested due to automatic review settings July 16, 2026 12:54
@coderabbitai

coderabbitai Bot commented Jul 16, 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: 9f076e6f-9190-477e-aba7-50db53b6a571

📥 Commits

Reviewing files that changed from the base of the PR and between 2c40b98 and 9497dfa.

📒 Files selected for processing (1)
  • packages/query-core/src/__tests__/streamedQuery.test.tsx

📝 Walkthrough

Walkthrough

streamedQuery now tracks the active invocation per query and prevents superseded streams from writing stale chunks or final replacement data. Regression tests cover reset and replace refetches where the original stream does not consume its abort signal.

Changes

Streamed query supersession

Layer / File(s) Summary
Track active streams and block stale writes
packages/query-core/src/streamedQuery.ts
A per-query WeakMap identifies the active stream, stopping superseded chunk writes and final replacement writes.
Validate orphaned stream behavior
packages/query-core/src/__tests__/streamedQuery.test.tsx, .changeset/stop-orphaned-streamed-query-writes.md
Async tests cover stale writes after reset and replace refetches, and a patch changeset records the fix.

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

Suggested labels: package: query-core

Suggested reviewers: kobihikri

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main cache-corruption fix in streamedQuery.
Description check ✅ Passed The description includes the required Changes, Checklist, and Release Impact sections with concrete details and a changeset note.
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 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Prevents stale/orphaned streamedQuery() streams from continuing to write chunks into the cache after a refetch cancels and restarts the query, even when the streamFn does not consume context.signal.

Changes:

  • Track the “current” active stream per Query instance using a WeakMap token, and stop writing when a stream has been superseded.
  • Guard both per-chunk cache writes and replace-mode finalization writes against superseded streams.
  • Add a regression test covering refetch cancellation when the stream function never consumes the abort signal, plus a changeset for a patch release.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
packages/query-core/src/streamedQuery.ts Adds per-Query active-stream tracking and prevents superseded streams from writing to cache.
packages/query-core/src/tests/streamedQuery.test.tsx Adds a regression test ensuring orphaned streams can’t corrupt fresh refetch results.
.changeset/stop-orphaned-streamed-query-writes.md Declares a patch release note for the fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@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-core/src/streamedQuery.ts (1)

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

Consider covering the replace-mode finalize guard with a regression test.

The new orphaned-stream test only exercises refetchMode: 'reset', which validates the loop-break guard (line 118) but not this isReplaceRefetch finalization path. Since this guard specifically blocks a stale invocation's accumulated result from overwriting the cache, an analogous test with refetchMode: 'replace' (reusing the pending-promise pattern from the existing test) would lock in this behavior too.

🤖 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-core/src/streamedQuery.ts` at line 133, Add a regression test
for the orphaned-stream scenario using refetchMode: 'replace', reusing the
existing pending-promise setup. Assert that a stale invocation does not finalize
or overwrite the cache with its accumulated result, covering the
isReplaceRefetch && !cancelled && !isSuperseded() guard in the streamed query
flow.
🤖 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-core/src/streamedQuery.ts`:
- Line 133: Add a regression test for the orphaned-stream scenario using
refetchMode: 'replace', reusing the existing pending-promise setup. Assert that
a stale invocation does not finalize or overwrite the cache with its accumulated
result, covering the isReplaceRefetch && !cancelled && !isSuperseded() guard in
the streamed query flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bacb1de2-5ba9-492c-93e6-83dc1336a0ef

📥 Commits

Reviewing files that changed from the base of the PR and between 1d972e7 and 2c40b98.

📒 Files selected for processing (3)
  • .changeset/stop-orphaned-streamed-query-writes.md
  • packages/query-core/src/__tests__/streamedQuery.test.tsx
  • packages/query-core/src/streamedQuery.ts

…uery

Address review feedback: the existing orphaned-stream test only exercised
the loop-break guard (default refetchMode 'reset'), not the separate
isReplaceRefetch finalize-write guard. Add a dedicated test with three
overlapping refetches in refetchMode 'replace', confirming a superseded
stream's accumulated result never overwrites the current correct one.

Verified this test fails (writes a stale empty result over the correct
one) if the isSuperseded() check is removed from the finalize condition,
and passes with it in place.
@a-y-ibrahim

Copy link
Copy Markdown
Author

Addressed the CodeRabbit nitpick in 9497dfa: added a dedicated test covering the refetchMode 'replace' finalize guard (a superseded stream's accumulated result must not overwrite the current one), verified it fails without the isSuperseded() check on that path and passes with it.

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