You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This RFC merges two analyses of the pagination/partitioning/loadSubset issue cluster — an internal codebase survey and an external architectural review — into one problem statement and a concrete PR plan. Scope is deliberately limited to bug fixes, internal consolidation, testing, and documentation of existing behavior. No new user-facing features. Feature proposals surfaced by the analyses (stable pagination, cursor metadata) are recorded in §6 as out of scope. Every load-bearing factual claim below was verified against origin/main (a93abf6), several via red/green test runs.
1. Problem statement
TanStack DB's partial-sync surface grew feature-by-feature: loadSubset began as "trigger a fetch", then accumulated predicates, orderBy/limit hints, cursors, offsets, dedup, truncate-replay, and join key requests. The result is that five concepts exist only implicitly, encoded across booleans, promises, arrays, and adapter-specific conventions:
Demand — what a live query needs (predicate + order + window), as distinct from what we chose to fetch.
Coverage — which regions of remote data are locally materialized, who owns them, and when they can be released.
Order — one total order that the local index, the top-K operator, cursor generation, and SSE admission all agree on.
Result — whether a load exhausted its source, and the region it is authoritative for.
Lifecycle — readiness, degradation, error, and restart as explicit states rather than emergent promise behavior.
Because these are implicit, each bug fix re-derives a fragment of them, and each adapter improvises its own semantics. Making them explicit internally is how we stop the recurring bug classes — without changing what the library does for users.
2. Verified evidence (what's actually true on main today)
#
Claim
Verification
Result
E1
Out-of-window SSE inserts corrupt the ordered window (PR #1555)
PR's regression test run against baseline
RED confirmed (out-of-window item promoted on delete); GREEN with fix
E2
Multi-column orderBy boundary drops ties (PR #1556)
PR's regression tests against baseline
RED confirmed (3/8 fail, incl. wrong item at limit(1)); GREEN with fix
E3
Computed orderBy alias leaks bad pushdown hints (PR #1520, closed as superseded 2026-07-13)
PR's regression test against baseline
No repro — obviated by main's evolved select/ref handling. PR is stale
E4
loadedSubsets/join-key unbounded growth (PR #1554, closed as superseded 2026-07-13)
PR's regression tests against baseline
No repro — superseded by subset-dedupe.ts. PR is stale
E5
Async fetch errors never reach status: 'error' (#1260)
Temp red test: throwing queryFn, retry: false
RED confirmed: collection.status === 'ready' after a failed first fetch; existing test query.test.ts:3389 codifies ready-on-error. The error-handling guide documents error status working — this is a doc/behavior contradiction
E6
Predicate identity is non-canonical JSON
tsx script against real IR
Confirmed: and(A,B) vs and(B,A) and eq(x,1) vs eq(1,x) produce different query keys → two observers, two fetches. query-db has no structural dedup backstop. extractComparisonField assumes ref-first
E7
TrailBase 403 on subscribe('*') → stuck loading forever (#1521)
Code read
Confirmed: await subscribe('*') (trailbase.ts:297) sits outside the try/finally holding the only markReady() (:317); start() fired without .catch() (:343). Affects on-demand mode equally
E8
getNextPageParam is a silent runtime no-op
Code read
Confirmed (declared @deprecated, never invoked)
E9
LoadSubsetFn returns true | Promise<void> — no result signal
types.ts:316 + grep
Confirmed — root of #968: useLiveInfiniteQuery infers hasNextPage purely from locally-materialized rows, so it stalls when the window under-fills even though the source has more data
Electric/TrailBase provide no unloadSubset; dedup tracking grows until truncate
electric.ts return block; trailbase.ts:350-356
Confirmed
E12
Electric progressive resume truncated persisted rows (#1493); PowerSync flushed before diff trigger (#1585); Electric refresh wait unbounded (#1575)
Code read of baselines each PR modifies
Confirmed as described
E13
loadSubset resolves — and readiness (toArrayWhenReady) fires — before fetched rows are applied, whenever any user transaction is persisting; dedup then records the predicate as covered, so retries can't recover (#1662, relates #1017)
PR #1662's repro run against baseline + mechanism read
RED confirmed: control passes, persisting case returns [] (expected [] to deeply equal ['r1','r2']). Guard at state.ts:871 (!hasPersistingTransaction || hasTruncateSync || hasImmediateSync) silently parks committed synced transactions with no signal; commit() returns void; subset-dedupe.ts records coverage on promise resolution. Repro uses only public APIs. Fix direction green-proxied: begin({ immediate: true }) makes both cases pass
Two meta-lessons from verification:
Open PRs are not a reliable bug inventory. Two of four "core window bugs" were already fixed by parallel work. Both analyses initially over-trusted the PR queue.
The error-handling guide already documents that collections report error status; the implementation contradicts it. This is a bug fix, not new API:
A failed initial load with nothing materialized transitions the collection to the existingerror status (the docs' promise).
A failed incremental subset load on a ready collection keeps data available (today's intent) but stops being silent: the subscription records the failure, setWindow's promise rejects instead of resolving indistinguishably, and the live query surfaces the last subset error instead of console.error-and-forget.
Every .finally()-only / .catch(() => {}) site in subscription.ts and sync.ts is replaced with explicit success/failure handling. Errors scope to the subset that failed — one bad subset must not poison unrelated queries on the same collection.
3.2 Fix infinite-query stalling with a minimal exhaustion signal (fixes #968, E8, E9)
#968 cannot be fixed without some way for the sync layer to say "the source is exhausted for this scope" — today hasNextPage is inferred purely from local rows. The minimal change:
void resolution keeps today's behavior exactly (peek-ahead fallback), so every existing adapter continues to work unchanged. Adapters that already know exhaustion (query-db when the queryFn returns fewer rows than requested; TrailBase's paged load) report it; useLiveInfiniteQuery uses it to stop stalling and to skip the wasteful +1 peek fetch when known. This is the smallest possible repair of broken behavior — cursors, totalCount, checkpoints, and page metadata APIs are out of scope (§6). Both analyses independently rejected #863's mutable session-keyed side channel; if metadata ever grows beyond hasMore, it must travel with the result, not through collection-level mutable state.
getNextPageParam (E8): remove the dead option (or hard-deprecate with a runtime warning) and fix the doc examples that still showcase it. No replacement is introduced.
3.3 One TotalOrder, one WindowState (fixes the E1/E2 bug class)
The verified bugs E1 and E2 are both order-disagreement bugs: the BTree pages by (first_col, key) while D2 ranks by the full comparator; SSE admission filtered by a different boundary than the window's. Fix the class, not the instances — an internal refactor with zero API change:
TotalOrder: compiled order terms + explicit null ordering + an always-appended primary-key tie-breaker. The same comparator drives local index bounds, top-K maintenance, cursor predicate generation (whereFrom/whereCurrent), SSE insert admission, and boundary refill. Boundary = complete sort tuple, not first-column-plus-expansion special cases.
WindowState: one module owning window membership (sentKeys), boundary tuple, candidate admission, cursor computation, and load-request dedup — extracted from the fragments in subscription.ts, collection-subscriber.ts, live/utils.ts.
Property harness (fast-check, already a devDep): random multi-column orderBy specs × random interleavings of {insert, delete, sort-key update, loadNextItems response, truncate, setWindow}; invariant: materialized window ≡ top-K of a naive full sort. E1 and E2 both fall out of this harness; future variants will too. PR fix(db): emit change on order-only reorder in ordered live queries #1601 (order-only reorders swallowed by the value-diff when the sort field isn't projected) is a third instance of the class — the harness invariant must therefore check emitted order, not just window membership.
3.4 Coverage registry, internal only (fixes #836, E11)
Two post-analysis reports are live evidence this machinery is failing in the field: #1656 (query-db ownership metadata clobbered — setPersistedOwners before write — so a later query's baseline hydration drops the first query's ownership and its GC deletes rows other live queries still need) and #1631 (eager collections wiped to size = 0 while still reporting ready, from the hasListeners-authoritative cleanup path colliding with the eager-mode refcount).
Consolidate today's scattered tracking (DeduplicatedLoadSubset state, per-subscription loadedSubsets, query-db's row/query maps and refcounts) into one internal registry where coverage entries — not rows, not queries — are the owned, refcounted resources. Queries lease coverage; the data provider's lifetime is tied to the entry's refcount, not to whichever query fetched first — this is the structural answer to #836's stale-dedup problem. unloadSubset already exists in the public contract; this makes it actually work uniformly:
Electric/TrailBase get bounded tracking: last-unload removes the predicate from dedup state (recompute via existing unionWherePredicates/minusWherePredicates) even if rows are retained until truncate, fixing the accumulate-forever behavior (E11) without changing observable row semantics.
Antichain, not append-only history: keep only maximal coverage under the subset relation.
Bounded algebra: exact subsumption only for key sets, scalar intervals, ordered prefixes; conservatively refetch otherwise. A redundant request is preferable to a false claim of coverage — already isPredicateSubset's philosophy.
Destructive diffs scoped to their coverage: a load may only delete rows previously claimed by its coverage entry — the fix for the warm-start class where one empty disjoint query wipes another query's hydrated rows.
v1 is a strangler: wrap existing DeduplicatedLoadSubset semantics behind the registry interface; migrate Electric and query-db first. No public API changes.
3.5 Canonical predicate identity (fixes E6)
Small and immediate, independent of the registry: canonicalize before serializing (sort commutative and/or operands structurally, normalize ref op val argument order), fix extractComparisonField's ref-first assumption, and give query-db-collection the structural dedup backstop it currently lacks. Today and(A,B) vs and(B,A) literally double-fetches.
3.6 Lifecycle hardening (fixes the E7/E12 class)
Epoch counter on collection sync sessions (generalizing subset-dedupe.ts's existing generation); every async completion checks its epoch; every readiness barrier settles exactly once; a preload() issued in any state attaches to a barrier that cleanup/stop must settle — closing the preload-after-cleanup hang class (preload after cleanup on a collection hangs forever #1576) structurally.
No renames, no new states: the existing status enum stays as-is.
3.7 Tests and docs for what already exists
Conformance suite: one shared test suite run against core + every adapter, generated traces over overlapping predicates, multi-column order with duplicates/nulls, growing windows, failed/hanging loads, cleanup/restart/GC, late completions. Invariants: visible rows ≡ reference result when coverage complete; old-epoch completions never mutate current state; every promise settles exactly once; fresh covered demand issues no request; tracking stays bounded. Land chore: Rewrite the tests for the loadSubset to cover more clauses and operators #1426's expansion as part of this.
Warnings, not silence: where an adapter can't honor a pushdown (TrailBase drops composite where; PowerSync ignores orderBy/limit), warn specifically and once. This uses a small internal capability flag set — not a public API.
Docs: Electric and TrailBase syncMode documentation (currently zero mention despite shipped behavior), plus a cross-cutting "Partial sync" guide documenting the existing LoadSubsetOptions contract, dedup behavior, unload semantics, and the per-adapter differences table. Documenting existing behavior is in scope; the guide is also what makes the contract reviewable.
3.8 loadSubset resolution must mean "applied" (fixes E13 / #1662, relates #1017)
Verified: when any user transaction on a collection is persisting, commitPendingTransactions() (state.ts:871) silently parks committed synced transactions, yet commit() returns void, loadSubset resolves, the live query marks ready, and reads return []. Worse, DeduplicatedLoadSubset records the predicate as covered on resolution, so retries are deduped to true — only the mutation settling recovers the rows. Offline-first apps (in-flight background writes) hit this as silently empty reads.
Preferred: loadSubset must not resolve until its synced transaction(s) are actually applied — give commit() an applied-signal (an internal whenApplied() barrier that parked transactions resolve on flush) and have the subset path await it. This also fixes the coverage-poisoning structurally, since dedup tracking runs on resolution.
Tactical fallback (green-proxied in verification): commit subset snapshots with begin({ immediate: true }), which bypasses the guard today. Caveat: an immediate commit flushes all parked committed sync transactions (intentional causal ordering), so it applies unrelated parked stream data ahead of the persisting transaction's confirmation — acceptable for authoritative server rows, but the preferred fix avoids the question.
Either way, the readiness barrier (§3.6) must count committed-but-parked transactions as "not yet ready" for scopes they cover.
4. PR plan
4.1 Phase 0 scope rule
Phase 0 is now deliberately narrower than the original queue sweep. An item belongs here only when all of the following are true:
it restores already-settled/documented behavior;
it is a narrow regression fix rather than a new policy choice;
it has no unresolved lifecycle, degradation, readiness, or ownership design dependency; and
it does not prematurely implement the Phase 2 abstractions.
Sharing a failure boundary with partial sync does not make another RFC's design decision part of this RFC. Cross-cutting items are listed below with a primary owner and an explicit dependency instead of being pulled into Phase 0.
Phase 0 — bounded queue hygiene and settled correctness fixes
PR-0a — ordered-result correctness (settled behavior, patches still need work)
fix(db): filter out-of-window SSE inserts in collection-subscriber #1555: keep as a verified point fix, but do not merge the current patch. Review found six existing ordered-window regressions because it filters legitimate later-window/refill inserts. Restrict admission filtering to unsolicited live/SSE changes and add movement/refill, offset, limit: 0, duplicate-boundary, and event-order coverage.
These remain separate reviewable PRs, using the same ordered-window acceptance matrix. They are fixed points for PR-4, not substitutes for TotalOrder/WindowState.
Ref count misassumption #1631: reproduced on current main. An eager collection becomes ready with size === 0 after its final subscriber leaves and Query GC runs. Land only the narrow invariant that an eager collection-lifetime lease is not destructively released by subscriber GC; defer the general ownership model to PR-5.
Valid Electric persisted-resume data-loss fix, but persistence/resume is #1659's domain. #1657 depends on its regression invariant; it does not own the merge.
Chooses local-first readiness after hydration. This is user-observable policy, not queue cleanup; do not advance it under #1657 until #1659 resolves the decision.
A real fallback requires deciding which failures degrade, polling semantics, mutation confirmation without SSE, and cleanup behavior. Current patch is not a polling fallback and is not merge-ready.
The 250 ms bound and late-completion treatment are lifecycle policy. Current patch reviewed as safe with minor test follow-up, but it should land under the lifecycle owner rather than Phase 0.
Cancellation affects cached-observer reuse, retry, final-owner release, and immediate remount. Current patch retains canceled observers and is not merge-ready.
The startup-order bug is real and the fix direction is sound, but trigger/tracking setup, cleanup races, and restart semantics are lifecycle-barrier work. Preserve its red/green test as a PR-6 fixed point.
Each phase is independently shippable; nothing is a flag-day rewrite. External dependencies such as #1493/#1615 are tracked by outcome and regression invariant, not duplicated implementation ownership.
5. Adjudicated positions (external review vs. this RFC)
Topic
External position
This RFC
Pagination metadata
rich result envelope, not side channel
Minimal hasMore only (bug fix for #968); richer envelope out of scope, but if ever added it rides the result
Coverage ownership
coverage leases, not per-query refcounts
Agree, internal-only, strangler over existing dedup
Moot — #315 was superseded by the includes work; not pursued
Sequencing
architecture-first
Point fixes first — two of their cited PRs were already stale; merge the verified ones, refactor behind their tests
6. Explicitly out of scope (recorded, not planned)
These came out of the analyses and are worth keeping on file, but they are features and this effort does not introduce features:
Stable pagination / deferred updates (Support Stable Pagination and Deferred Update Handling for Large Query Results #21) — correctly decomposes into a consistency axis (live/anchored/snapshot, capability-negotiated) × a presentation axis (immediate/deferred + hasPendingChanges). Requires the result channel to carry snapshot tokens; revisit after §3.2 ships.
Public adapter capability negotiation and explainLoading() devtools — the internal warning flags (§3.7) are the no-feature version; a public planner/explain surface is future work.
(Partitioned collections, #315, previously appeared here — it was superseded by the includes work and is dropped from this analysis entirely.)
Status: draft · Date: 2026-07-08 (updated 2026-07-13: Phase 0 queue review and scope correction)
This RFC merges two analyses of the pagination/partitioning/loadSubset issue cluster — an internal codebase survey and an external architectural review — into one problem statement and a concrete PR plan. Scope is deliberately limited to bug fixes, internal consolidation, testing, and documentation of existing behavior. No new user-facing features. Feature proposals surfaced by the analyses (stable pagination, cursor metadata) are recorded in §6 as out of scope. Every load-bearing factual claim below was verified against
origin/main(a93abf6), several via red/green test runs.1. Problem statement
TanStack DB's partial-sync surface grew feature-by-feature:
loadSubsetbegan as "trigger a fetch", then accumulated predicates, orderBy/limit hints, cursors, offsets, dedup, truncate-replay, and join key requests. The result is that five concepts exist only implicitly, encoded across booleans, promises, arrays, and adapter-specific conventions:Because these are implicit, each bug fix re-derives a fragment of them, and each adapter improvises its own semantics. Making them explicit internally is how we stop the recurring bug classes — without changing what the library does for users.
2. Verified evidence (what's actually true on main today)
limit(1)); GREEN with fixloadedSubsets/join-key unbounded growth (PR #1554, closed as superseded 2026-07-13)subset-dedupe.ts. PR is stalestatus: 'error'(#1260)queryFn,retry: falsecollection.status === 'ready'after a failed first fetch; existing testquery.test.ts:3389codifies ready-on-error. The error-handling guide documents error status working — this is a doc/behavior contradictionand(A,B)vsand(B,A)andeq(x,1)vseq(1,x)produce different query keys → two observers, two fetches. query-db has no structural dedup backstop.extractComparisonFieldassumes ref-firstsubscribe('*')→ stuck loading forever (#1521)await subscribe('*')(trailbase.ts:297) sits outside the try/finally holding the onlymarkReady()(:317);start()fired without.catch()(:343). Affects on-demand mode equallygetNextPageParamis a silent runtime no-op@deprecated, never invoked)LoadSubsetFnreturnstrue | Promise<void>— no result signaluseLiveInfiniteQueryinfershasNextPagepurely from locally-materialized rows, so it stalls when the window under-fills even though the source has more data.finally()/.catch(()=>{}))unloadSubset; dedup tracking grows until truncateloadSubsetresolves — and readiness (toArrayWhenReady) fires — before fetched rows are applied, whenever any user transaction ispersisting; dedup then records the predicate as covered, so retries can't recover (#1662, relates #1017)[](expected [] to deeply equal ['r1','r2']). Guard atstate.ts:871(!hasPersistingTransaction || hasTruncateSync || hasImmediateSync) silently parks committed synced transactions with no signal;commit()returns void;subset-dedupe.tsrecords coverage on promise resolution. Repro uses only public APIs. Fix direction green-proxied:begin({ immediate: true })makes both cases passTwo meta-lessons from verification:
3. Fixes and hardening
3.1 Restore documented error behavior (fixes #1260, E5, E10)
The error-handling guide already documents that collections report error status; the implementation contradicts it. This is a bug fix, not new API:
errorstatus (the docs' promise).setWindow's promise rejects instead of resolving indistinguishably, and the live query surfaces the last subset error instead ofconsole.error-and-forget..finally()-only /.catch(() => {})site insubscription.tsandsync.tsis replaced with explicit success/failure handling. Errors scope to the subset that failed — one bad subset must not poison unrelated queries on the same collection.3.2 Fix infinite-query stalling with a minimal exhaustion signal (fixes #968, E8, E9)
#968 cannot be fixed without some way for the sync layer to say "the source is exhausted for this scope" — today
hasNextPageis inferred purely from local rows. The minimal change:voidresolution keeps today's behavior exactly (peek-ahead fallback), so every existing adapter continues to work unchanged. Adapters that already know exhaustion (query-db when thequeryFnreturns fewer rows than requested; TrailBase's pagedload) report it;useLiveInfiniteQueryuses it to stop stalling and to skip the wasteful+1peek fetch when known. This is the smallest possible repair of broken behavior — cursors, totalCount, checkpoints, and page metadata APIs are out of scope (§6). Both analyses independently rejected #863's mutable session-keyed side channel; if metadata ever grows beyondhasMore, it must travel with the result, not through collection-level mutable state.getNextPageParam(E8): remove the dead option (or hard-deprecate with a runtime warning) and fix the doc examples that still showcase it. No replacement is introduced.3.3 One TotalOrder, one WindowState (fixes the E1/E2 bug class)
The verified bugs E1 and E2 are both order-disagreement bugs: the BTree pages by
(first_col, key)while D2 ranks by the full comparator; SSE admission filtered by a different boundary than the window's. Fix the class, not the instances — an internal refactor with zero API change:TotalOrder: compiled order terms + explicit null ordering + an always-appended primary-key tie-breaker. The same comparator drives local index bounds, top-K maintenance, cursor predicate generation (whereFrom/whereCurrent), SSE insert admission, and boundary refill. Boundary = complete sort tuple, not first-column-plus-expansion special cases.WindowState: one module owning window membership (sentKeys), boundary tuple, candidate admission, cursor computation, and load-request dedup — extracted from the fragments insubscription.ts,collection-subscriber.ts,live/utils.ts.3.4 Coverage registry, internal only (fixes #836, E11)
Two post-analysis reports are live evidence this machinery is failing in the field: #1656 (query-db ownership metadata clobbered —
setPersistedOwnersbeforewrite— so a later query's baseline hydration drops the first query's ownership and its GC deletes rows other live queries still need) and #1631 (eager collections wiped tosize = 0while still reportingready, from thehasListeners-authoritative cleanup path colliding with the eager-mode refcount).Consolidate today's scattered tracking (
DeduplicatedLoadSubsetstate, per-subscriptionloadedSubsets, query-db's row/query maps and refcounts) into one internal registry where coverage entries — not rows, not queries — are the owned, refcounted resources. Queries lease coverage; the data provider's lifetime is tied to the entry's refcount, not to whichever query fetched first — this is the structural answer to #836's stale-dedup problem.unloadSubsetalready exists in the public contract; this makes it actually work uniformly:unionWherePredicates/minusWherePredicates) even if rows are retained until truncate, fixing the accumulate-forever behavior (E11) without changing observable row semantics.isPredicateSubset's philosophy.DeduplicatedLoadSubsetsemantics behind the registry interface; migrate Electric and query-db first. No public API changes.3.5 Canonical predicate identity (fixes E6)
Small and immediate, independent of the registry: canonicalize before serializing (sort commutative
and/oroperands structurally, normalizeref op valargument order), fixextractComparisonField's ref-first assumption, and give query-db-collection the structural dedup backstop it currently lacks. Todayand(A,B)vsand(B,A)literally double-fetches.3.6 Lifecycle hardening (fixes the E7/E12 class)
subset-dedupe.ts's existinggeneration); every async completion checks its epoch; every readiness barrier settles exactly once; apreload()issued in any state attaches to a barrier that cleanup/stop must settle — closing the preload-after-cleanup hang class (preloadaftercleanupon a collection hangs forever #1576) structurally.markReady()reachable on every path (TrailBase E7), tracking/trigger existence guaranteed before flushes (PowerSync fix(powersync): Don't flush records when diff trigger isn't setup yet when usingon-demandmode. #1585), resume state distinguished from initial sync (Electric fix(electric-db-collection): preserve persisted rows on progressive resume #1493), and waits bounded (fix(electric): bound refresh wait for on-demand subsets #1575).3.7 Tests and docs for what already exists
where; PowerSync ignores orderBy/limit), warn specifically and once. This uses a small internal capability flag set — not a public API.syncModedocumentation (currently zero mention despite shipped behavior), plus a cross-cutting "Partial sync" guide documenting the existingLoadSubsetOptionscontract, dedup behavior, unload semantics, and the per-adapter differences table. Documenting existing behavior is in scope; the guide is also what makes the contract reviewable.3.8 loadSubset resolution must mean "applied" (fixes E13 / #1662, relates #1017)
Verified: when any user transaction on a collection is
persisting,commitPendingTransactions()(state.ts:871) silently parks committed synced transactions, yetcommit()returns void,loadSubsetresolves, the live query marks ready, and reads return[]. Worse,DeduplicatedLoadSubsetrecords the predicate as covered on resolution, so retries are deduped totrue— only the mutation settling recovers the rows. Offline-first apps (in-flight background writes) hit this as silently empty reads.Fix (credit @TomasGonzalez, #1662, which contributes the red test):
loadSubsetmust not resolve until its synced transaction(s) are actually applied — givecommit()an applied-signal (an internalwhenApplied()barrier that parked transactions resolve on flush) and have the subset path await it. This also fixes the coverage-poisoning structurally, since dedup tracking runs on resolution.begin({ immediate: true }), which bypasses the guard today. Caveat: an immediate commit flushes all parked committed sync transactions (intentional causal ordering), so it applies unrelated parked stream data ahead of the persisting transaction's confirmation — acceptable for authoritative server rows, but the preferred fix avoids the question.4. PR plan
4.1 Phase 0 scope rule
Phase 0 is now deliberately narrower than the original queue sweep. An item belongs here only when all of the following are true:
Sharing a failure boundary with partial sync does not make another RFC's design decision part of this RFC. Cross-cutting items are listed below with a primary owner and an explicit dependency instead of being pulled into Phase 0.
Phase 0 — bounded queue hygiene and settled correctness fixes
Completed queue cleanup (2026-07-13)
DeduplicatedLoadSubset. Its existing tests should not be transplanted unchanged; any follow-up must directly assert bounded backend requests.PR-0a — ordered-result correctness (settled behavior, patches still need work)
limit: 0, duplicate-boundary, and event-order coverage.These remain separate reviewable PRs, using the same ordered-window acceptance matrix. They are fixed points for PR-4, not substitutes for
TotalOrder/WindowState.PR-0b — narrow configuration/lifetime violations
gcTime: 0with nullish rather than truthy defaulting. Review found the code correct; normal GitHub workflows still require approval/rerun before merge.readywithsize === 0after its final subscriber leaves and Query GC runs. Land only the narrow invariant that an eager collection-lifetime lease is not destructively released by subscriber GC; defer the general ownership model to PR-5.Test-only harvesting
findOne, offset, join-loading, or other unresolved behavior as an implied contract through TODO tests.Items removed from Phase 0
Phase 1 — contract fixes (small, independent)
hasMoreexhaustion signal +getNextPageParamremoval/deprecation per §3.2. Fixes useLiveInfiniteQuery doesn't fetch more data when collection has no more items #968, eliminates the wasteful peek fetch where exhaustion is known.loadSubsetresolution ⇒ rows applied, per §3.8. Fixes Failing repro: loadSubset resolves & live query marks ready while synced rows are parked behind a persisting transaction #1662 (whose repro is the red test); relates Synced data does not appear in a derived (live query) collection while there is a pending optimistic mutation. #1017. Maintainer-led as part of this coordinated effort — a proper fix at the contract level, not a one-off patch.Phase 2 — internal consolidation
TotalOrder+WindowStateextraction + fast-check property harness per §3.3. Inherit fix(db): filter out-of-window SSE inserts in collection-subscriber #1555/fix(db): boundary expansion for multi-column orderBy pagination #1556/fix(db): emit change on order-only reorder in ordered live queries #1601 regression tests as fixed points.unloadSubsetacross adapters per §3.4. Fixes SupportloadSubsetdeduplication in query collection #836 and owns the structural follow-up to Ref count misassumption #1631/Fix/live query record drop on subset unmount #1656/fix(query-db-collection): cancel idle on-demand queries #1573.on-demandmode. #1585; coordinates cancellation/remount behavior with PR-5 for fix(query-db-collection): cancel idle on-demand queries #1573.Phase 3 — tests and docs
Each phase is independently shippable; nothing is a flag-day rewrite. External dependencies such as #1493/#1615 are tracked by outcome and regression invariant, not duplicated implementation ownership.
5. Adjudicated positions (external review vs. this RFC)
hasMoreonly (bug fix for #968); richer envelope out of scope, but if ever added it rides the resultstop()/reset()/dispose()renameerrorbehavior + expose last subset error; nothing more6. Explicitly out of scope (recorded, not planned)
These came out of the analyses and are worth keeping on file, but they are features and this effort does not introduce features:
hasPendingChanges). Requires the result channel to carry snapshot tokens; revisit after §3.2 ships.hasMore) — endCursor/totalCount/pageInfo surfaces, cursor-modeuseLiveInfiniteQuery.explainLoading()devtools — the internal warning flags (§3.7) are the no-feature version; a public planner/explain surface is future work.(Partitioned collections, #315, previously appeared here — it was superseded by the includes work and is dropped from this analysis entirely.)