query-db: a Query refetch that joins a loader's refetch acquisition does not report shifted pages #2185
Description
Summary
Query DB does not report invalidateLoadedSubsets() for a Query refetch that joins a running refetch: true loader acquisition on the same Query hash. If that refetch shifts the page, other ordered live queries that rely on the page can keep a #2136-style gap.
Where
packages/query-db-collection/src/query.ts, recordOrderedPageResult. The gate skips a result while acquisitionRefetches holds its hash. This loop guard stops a repair's own result from starting another repair. Since #2154 (commit 0216c6026), the guard counts overlapping acquisitions, but it still cannot tell a joined user refetch from the acquisition's own result.
Shape of a trace (not yet reproduced)
- Loader A starts a bounded repair. Its
refetch: trueprefix acquisition on Query hash H is in flight. utils.refetch()orinvalidateQueriesruns. Query joins the in-flight fetch for H, after the backend changed.- The result for H has different keys and a later page follows it. Without the guard it would report.
- The guard suppresses the report. Loader A repairs its own window from the result, but loader B, whose window also used H's page, gets no report.
Status
No history in the query-db refetch oracle reaches this. The oracle's limits section and the coverage map record it.
Suggested witness
A named replay in the query-db refetch oracle with two live queries over the same pages. Hold one loader's repair acquisition, change the backend, run a Query refetch, then release. The coverage law should fail for the second live query if the defect is real. A possible fix: record the fetch that the acquisition started, not the hash, and report other fetches' results.
This issue was written by Isaac.
Activity
KyleAMathews commentedon Oct 15, 2026
A second high review of #2154 adds two details to this issue.
refetch: truerequests. Every caller ofrefetchAndWaitForApplicationholdsacquisitionRefetches. That includesloadSubsetpaths throughrefetchOrWaitForEnabledQuery. The guard lasts for the whole acquisition, which can be long behind a persisting optimistic transaction, so a refetch of that hash during that time is not reported.finish()can delete the guard beforefinishApplicationrecords the page. Then the loader's own result reports and starts one more repair. It cannot loop: with an unchanged backend the next result keeps its keys. But it costs a repair in every paging live query.A fix that records the fetch the acquisition started, not the hash, would address both.
This comment was written by Isaac.