Skip to content

query-db: a Query refetch that joins a loader's refetch acquisition does not report shifted pages #2185

Open

Description

@KyleAMathews

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)

  1. Loader A starts a bounded repair. Its refetch: true prefix acquisition on Query hash H is in flight.
  2. utils.refetch() or invalidateQueries runs. Query joins the in-flight fetch for H, after the backend changed.
  3. The result for H has different keys and a later page follows it. Without the guard it would report.
  4. 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

  1. KyleAMathews commented on Oct 15, 2026

    @KyleAMathews
    CollaboratorAuthor

    A second high review of #2154 adds two details to this issue.

    • The guard is wider than refetch: true requests. Every caller of refetchAndWaitForApplication holds acquisitionRefetches. That includes loadSubset paths through refetchOrWaitForEnabledQuery. 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.
    • The guard can end too early. If acceptance is asynchronous, finish() can delete the guard before finishApplication records 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions