From 0e5a5234b9fe4a26244a0e52d3862916a7f4ce7e Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 30 Oct 2026 17:05:11 -0600 Subject: [PATCH 1/6] fix(db): retain failed demand during sibling replay --- .changeset/retain-loading-sibling-replay.md | 5 + docs/contributing/oracle-coverage.md | 20 ++ .../2026-10-30-release-replay-retention.md | 154 ++++++++ .../tests/multi-tab-electric-oracle.test.ts | 59 +++- packages/db/src/collection/subscription.ts | 2 +- packages/db/src/query/live/ARCHITECTURE.md | 7 +- .../query/live/collection-config-builder.ts | 6 +- ...ad-subset-replay-refinement-oracle.test.ts | 330 +++++++++++++++++- 8 files changed, 555 insertions(+), 28 deletions(-) create mode 100644 .changeset/retain-loading-sibling-replay.md create mode 100644 docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md diff --git a/.changeset/retain-loading-sibling-replay.md b/.changeset/retain-loading-sibling-replay.md new file mode 100644 index 0000000000..64368eee0d --- /dev/null +++ b/.changeset/retain-loading-sibling-replay.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Keep the last complete live-query result when a new include demand fails while a sibling subscription is still replaying after truncate. A successful sibling load cannot publish an incomplete replacement. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 6e5ba7182e..0821c9962e 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -6304,3 +6304,23 @@ useful extension: the hash identity oracle owns those semantic distinctions, but its helper-level evidence does not by itself exercise distinct storage. The separate Index structural-collision obligation above remains open. See the appended [review evidence](oracle-reviews/2026-10-30-distinct-collisions.md). + +## Loading sibling replay and final catch-up reach (2026-10-30) + +The primary load-subset replay owner now constructs 48 finite histories where +an include with no demand at truncate starts under its sibling's loading replay. +It measures held failure (44 histories), failure retirement (33), and later own +coverage (12), through the existing public retention and recovery comparisons. +The original guard fails the quiet retention check. Subscription-scoped verdict +and successful-coverage calibrations remain in that primary owner. + +The browser multi-tab Electric owner preserves its stop prefix and arbitrary +tail and constructs an abandoned reservation immediately before final catch-up. +This removes dependence on sampling that boundary without changing its model, +80-run budgets, comparisons or reach minima. + +See [the versioned experiment record](oracle-reviews/2026-10-30-release-replay-retention.md) +for authority, RED, reach, mutant, grammar controls, historical gate limits and +receiving-path limits. This is core Collection publication evidence and a Node +SQLite/coordinator receiving witness with controlled Electric streams. It does +not claim Effect, provider, framework, deeper include, or browser-process closure. diff --git a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md new file mode 100644 index 0000000000..7917412d03 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md @@ -0,0 +1,154 @@ +# Release replay retention and generated final catch-up + +The release workflow for `e75f8455a78bc33c43376288218e9912dc2a3f4c` +failed before publication. Its two failures have different causes. +The DB property exposed a production retention defect. The browser property +passed its behavior comparisons but missed a required state. Neither failure +establishes a regression in the callback-metadata size refactor: the DB +counterexample also fails on the exact release source. + +## Law and the first divergent observation + +Authority: live-query `ARCHITECTURE.md`, **Replay participants and failure**, +and normative law 9. A query's subscriptions to one source share publication. +Every acquisition started under that replay participates. A failed held demand +keeps the last complete result. Sibling success cannot decide its verdict. +Demand retirement or successful own replacement are the neighboring controls. + +The original `replay.ordered-failed-replay` counterexample uses seed +`-533188721`, path `428:3:3:6`. Its commands hold loads, truncate markers, +settle the two oldest loads successfully, fail the newest load, then remove +marker 1. The scenario has descending order, offset 1, limit 2, no ties or +peer, an anti-join and an inline include over on-demand markers. At quiet +loading, the expected retained snapshot is empty; production publishes row 1. + +The adapter receipts distinguish production failure from demand retirement. +The include held no demand at truncate. It later acquires marker key 1 while +the sibling join replay is loading. That include acquisition fails and remains +held and un-aborted. Its sibling later succeeds. Only cleanup unloads the +failed acquisition, after the mismatching observation. The old guard looked +only for a failed sibling replay. The repair also recognizes a loading replay +that still withholds publication. Ordinary failures outside replay retain their +existing request-error behavior. + +## Primary owner and experiment order + +`load-subset-replay-refinement-oracle.test.ts` remains the primary owner. +The change retains the subscription-scoped verdict and successful-coverage +corrections previously reviewed on PR 2312. Those corrections concern the +model, not a new production promise: shared publication does not merge replay +verdicts. Their two finite calibrations remain in the same owner. + +Before the runtime edit, a new constructive finite grammar produced the +intended RED on unchanged main. The failing assertion reports one measured new +sibling failure and the same empty-snapshot/row-1 mismatch. The first reach +recorder incorrectly excluded retired historical acquisitions; the corrected +pre-edit run measures acquisitions actually held at truncate. Both logs remain. + +The final grammar has 48 histories: two order directions, three widths, one or +two successful pre-failure settlements, and four later actions (none, marker +change, rank move, or another marker replay). Every history uses the existing +model, driver, retention comparison after each command, quiet checkpoint, and +final authoritative recovery. No expected row set was added to the histories. + +The initial repair trial passed all retention comparisons but failed the newly +declared own-coverage reach. The final recorder observes same-subscription key +coverage, rather than predicate object identity: a later demand may construct +new request data. The grammar also includes an explicit later replay control. +This post-trial reach work does not retroactively establish a pre-edit recovery +reach gate. The original retention RED and the receiving path were obtained +before production changed. + +Final measured reach: 44 histories with a first-demand failure under a loading +sibling; 33 with retirement of such failed demand; 12 with later successful +coverage by its own subscription. The three states belong to this constructive +grammar. Adding them to the shared result type initially made the unconstrained +campaign demand them by accident; the explicit owner assignment corrects that +without removing any pre-existing reach obligation. + +The original-guard mutant fails at the intended quiet retention comparison, +with the same one measured new sibling failure. It was restored in a `finally` +block and the repaired source hash was checked. Setup errors are recorded +separately and are not RED or mutant rejection evidence. + +## Browser grammar repair + +`multi-tab-electric-oracle.test.ts` owns the existing five browser laws. +The failed 80-history random campaign missed **reset-abandoned source recovered +during final catch-up**. Its fixed campaign reached that state four times. +A previous stop/peer prefix constructs other required states, but this state +still depended on an unconstrained 8–24-step tail. + +The repair keeps that prefix, tail, 80-run budgets, model, comparisons and reach +minimum. A suffix creates a fresh handle, recovers and catches up every tab, +delivers a new change to one tab without committing it, then resets its +reservation. The existing final delivery must observe the abandoned commit's +failure before restarting and checking convergence. Two/three tabs, source tab, +key and recovery method vary. Removing the partial delivery or reset loses the +premise; committing before final catch-up moves the failure to a different cut. +The original named final-delivery replays remain unchanged. + +## Review and limits + +This is an author self-review. The earlier independent diagnosis supplied the +receipt trace; it is not an independent review of the final implementation. + +- ORC-001/002/003/005/009: accepted architecture law, existing independent + row/retention model, literate grammar and real Collection sync boundary; + subscription identities and requested keys are adapter receipts. +- ORC-004/013/015: finite direction/width/settlement/change grammar reconstructs + the reported witness and adjacent controls. Absent replay is the ordinary + failure neighbor; obsolete or retired requests cannot count as held failures. + Browser suffix ablations lose its stated premise. Reach is measured. +- ORC-006/010: original guard rejected at the retention assertion; original + failure, initial reach-only failures and setup errors remain distinguishable. + Existing fixture cleanup remains in place. +- ORC-007: existing fixed/random properties and direct seed/path interface are + unchanged. The new 48-cell grammar is finite enumeration, not a random + property. Browser fixed/random campaigns retain the same 80-run grammar. +- ORC-008: no new semantic model state. New receipt fields serve reach only. + Retained subscription distinction is required because sibling success and + own success permit different verdicts. +- ORC-011: no new shared semantic fault hypothesis. Independent adapter trace + rejected the alternative retirement explanation of this failure. +- ORC-012/014/016: this record states the bounded claim and the unchanged + controlled premises. Histories use the shared comparison, with no unit-test + expected answers. + +Core evidence is limited to live-query Collection publication with one ordered +source, a one-level inline include, and the controlled core sync adapter. This +change does not claim Effect failure disposal, provider delivery, framework +scheduling, arbitrary joins or deeper recursion. Those paths have separate +owners; the modified builder failure path serves live-query Collections. +Browser evidence uses real SQLite, BroadcastChannel and coordinator code in one +Node process with controlled Electric streams. It does not establish installed +SDK/HTTP, real browser multi-process timing, on-demand row contents or existing +known-gap closure. No laws, assertions, run budgets or reach minima are weakened. + +## Final commands, results and source binding + +Local runs use saved configs under `/private/tmp/callback-metadata/release-replay-repair` to resolve the exact worktree source with cached dependencies. Every command sets `pnpm_config_verify_deps_before_run=false`; Vitest uses one worker. + +| Check | Command or selector | Result | +| --- | --- | --- | +| Pre-edit primary RED | `pnpm exec vitest run --config /vitest.config.mjs -t 'keeps new include failures behind a loading sibling replay'` | Intended quiet retention mismatch; measured first-demand sibling failure 1. | +| Final finite grammar | Same command | PASS, 48 histories, failure/retirement/own-recovery reach 44/33/12. | +| Original guard mutant | Same command with only the old guard restored | Intended quiet retention mismatch, measured failure 1. Repaired source restored and hash verified. | +| Full primary owner | `pnpm exec vitest run --config /vitest.config.mjs` | 43 passed, two accidental new-reach obligations failed. After explicit ownership correction, both affected fixed/random campaigns passed. All 45 tests pass across those runs. | +| Core neighbors | Saved `neighbors.config.mjs` | 21 tests in three files pass, covering replay refinement, failure boundaries and request errors. | +| Browser fixed/random campaigns | `pnpm exec vitest run --config /browser.config.mjs -t 'keeps reserved commits lawful'` | Both pass. Final catch-up recovery reached 80/80 histories in each campaign. | +| Browser final-delivery controls | Same config with the two existing final-delivery selectors | Both existing shared-model replays pass. | +| Focused TypeScript | `pnpm exec tsc --noEmit -p /db-types.json` and `browser-types.json` | Both pass. | + +The browser campaign retains 80 runs for each seed mode. Its expected injected stream errors are diagnostics, not comparison failures. The first two browser setup failures, the initial reach-only failures, and the intermediate TypeScript literal-inference error remain separate from RED/GREEN evidence. + +The following SHA256 fingerprints bind the executable evidence to this change. The commit containing this record supplies its immutable Git identity. + +| File | SHA256 | +| --- | --- | +| `packages/db/src/query/live/collection-config-builder.ts` | `bb1b62aaf00aed1dc5eaa5e537671d92bb99def7a53a6e951ef38f73b00f5088` | +| `packages/db/src/collection/subscription.ts` | `bd171bdc97b38a79ebbc81d7fb605267787308559ad4c0646dff1e072a43798a` | +| `packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts` | `e5ce4a77fe8d16f4b0d26f4c71fdadb53b2779c8e7e8313a7eb32d2de3ebdd38` | +| `packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts` | `67c16d284522417f1a77e0a36efcc552b718f858ea8ec00771f001f001160782` | + +Focused ESLint reports zero errors and two existing `no-shadow` warnings. Prettier, whitespace, and `pnpm lint:changed` pass. No run budget decreased or disappeared. diff --git a/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts b/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts index f109c45c08..341715569a 100644 --- a/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts +++ b/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts @@ -989,24 +989,53 @@ describe(`multi-tab Electric over one persisted database`, () => { }), steps: fc.array(historyStep, { minLength: 8, maxLength: 24 }), }) - .map(({ tabCount, source, key, recovery, stopOrder, steps }) => { - const tab = source % tabCount - const peer = (tab + 1) % tabCount - return { + .map( + ({ tabCount, - steps: [ - ...stopOrder.flatMap((pick): Array => [ + source, + key, + recovery, + stopOrder, + steps, + }): GeneratedHistory => { + const tab = source % tabCount + const peer = (tab + 1) % tabCount + return { + tabCount, + steps: [ + ...stopOrder.flatMap((pick): Array => [ + { kind: `server change`, key, pick: 0 }, + { kind: `advance`, tab, pick: 0 }, + { kind: `stop`, tab, pick }, + { kind: `catch up`, tab: peer, pick: 0 }, + { kind: `recover`, tab, pick: recovery }, + { kind: `catch up`, tab, pick: 0 }, + ]), + ...steps, + // Final catch-up must encounter an abandoned reservation in every + // generated history. Start a fresh handle, recover every tab, and + // deliver one new change without committing it. Reset then abandons + // that reservation. No later history step commits or recovers it; + // the existing final-delivery comparison must observe its failure + // before the existing recovery establishes convergence (Laws 2/5). + // The source tab, key and two/three-tab host vary with this grammar. + // Removing advance or reset loses that premise; catching up here + // would move the failure out of the final observation cut. + { kind: `deploy` }, + ...Array.from( + { length: tabCount }, + (_, index): Array => [ + { kind: `recover`, tab: index, pick: recovery }, + { kind: `catch up`, tab: index, pick: 0 }, + ], + ).flat(), { kind: `server change`, key, pick: 0 }, { kind: `advance`, tab, pick: 0 }, - { kind: `stop`, tab, pick }, - { kind: `catch up`, tab: peer, pick: 0 }, - { kind: `recover`, tab, pick: recovery }, - { kind: `catch up`, tab, pick: 0 }, - ]), - ...steps, - ], - } - }) + { kind: `peer reset` }, + ], + } + }, + ) type GeneratedHistory = { tabCount: number; steps: Array } type ServerLog = { diff --git a/packages/db/src/collection/subscription.ts b/packages/db/src/collection/subscription.ts index 428dd2fac8..949e0eb4d4 100644 --- a/packages/db/src/collection/subscription.ts +++ b/packages/db/src/collection/subscription.ts @@ -871,7 +871,7 @@ export class CollectionSubscription /** * A lazy demand that failed outside this subscription's replay, while a - * failed replay of another of the graph's subscriptions to this collection + * replay of another of the graph's subscriptions to this collection still * holds publication, fails this subscription's replacement too. It keeps * the last publication until every demand load that joins this replay * loads, a later replay succeeds, or this subscription's last demand diff --git a/packages/db/src/query/live/ARCHITECTURE.md b/packages/db/src/query/live/ARCHITECTURE.md index 6dc031ae33..a3a07c71d7 100644 --- a/packages/db/src/query/live/ARCHITECTURE.md +++ b/packages/db/src/query/live/ARCHITECTURE.md @@ -1532,10 +1532,11 @@ A live query's subscriptions to one collection, such as a join and an include of the same source, share one publication barrier. None of their replays publishes, and none whose last demand retired closes, while another of them still has replay work, so demand that any of them starts meanwhile joins its -own open replay. While a failed replay of any of them holds publication, a +own open replay. While a replay of any of them still holds publication, a lazy demand of any of them that fails after initial-query readiness fails its -own subscription's replacement too, so retiring the other replay's failed -demand cannot publish the window without this demand's rows. It holds until +own subscription's replacement too. This includes a subscription that held no +demand at truncate. Settling a sibling's replay or retiring its failed demand +cannot publish the window without this demand's rows. It holds until every demand load that joins it loads, a later replay of the source succeeds, or the subscription's last demand retires. The query retries the failed demand at once and the retry joins the hold, so the hold ends only after the failed diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index ee60295643..0089c5a7be 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -596,8 +596,8 @@ export class CollectionConfigBuilder< if (this.liveQueryCollection?._lifecycle.status === `ready`) { // The failure settles the demand: the request errors of its keys show // it, and ordered work must not wait for a load that already ended. - // Output held for this demand publishes now, unless a failed replay of - // this source holds publication: the failure then holds this + // Output held for this demand publishes now, unless a replay of this + // source still holds publication: the failure then holds this // subscription's publication too, so retiring the other replay's // failed demand cannot publish without this demand's rows. demand.settled = true @@ -605,7 +605,7 @@ export class CollectionConfigBuilder< this.collectionSources.some( (source) => source.collection === collection && - this.subscriptions[source.sourceId]?.hasFailedTruncateReplacement, + this.subscriptions[source.sourceId]?.hasPendingTruncateReplacement, ) ) subscription.failReplacement(normalized) diff --git a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts index 8a11da8ce3..6f94d276e3 100644 --- a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts @@ -165,7 +165,8 @@ import type { LoadSubsetOptions, SyncConfig } from '../../src/types.js' * node may list a failure, and the replay-error identity law skips peer * histories. The replay-error identity law judges only rejections with a failure of a replay * the move waited on; a move that resumes into another failed replay is not - * judged. + * judged. Each subscription owns its replay verdict, even when publication + * is shared with a sibling. Error-order comparisons keep that distinction. */ type OrderedFailureState = `initiating-keyed-request-failed` | `keyed-reacquisition-failed` @@ -316,6 +317,12 @@ type OrderedReach = { // Deferred window moves that rejected with a failure of a replay they // waited on. deferredMoveRejections: number + ownReplayFailureComparisons: number + siblingReplayFailureComparisons: number + siblingSuccessfulCoverage: number + newSiblingReplayFailures: number + newSiblingReplayRetirements: number + newSiblingReplayRecoveries: number replayLoads: number abortRejections: number preloadChecks: number @@ -463,6 +470,11 @@ async function checkOrderedReplay( error?: Error // Its source, the keys it asked for, and whether it is an ordered read. source?: BarrierSource + // Publication is shared, but each subscription owns its replay verdict. + subscription: LoadSubsetOptions[`subscription`] + // This subscription held no acquisition at truncate, then started demand + // during its sibling replay. Measured only from adapter receipts. + newSiblingReplay?: boolean keys?: Set ordered?: boolean } @@ -485,6 +497,8 @@ async function checkOrderedReplay( ambiguous: boolean pending: Map own: Set + // Acquisition receipts held at truncate, used only for declared reach. + subscriptionsAtTruncate: Set } const barriers = new Map() // Every barrier this history raised, in raise order. @@ -514,6 +528,18 @@ async function checkOrderedReplay( ambiguous: [...barriers.values()].some(({ ambiguous }) => ambiguous), pending: new Map(barriers.get(source)?.pending), own: new Set(), + subscriptionsAtTruncate: new Set( + [...acquired].flatMap(([options, count]) => + count > 0 && + loads.some( + (load) => + load.source === source && + load.subscription === options.subscription, + ) + ? [options.subscription] + : [], + ), + ), } barriers.set(source, barrier) raisedBarriers.push(barrier) @@ -540,6 +566,7 @@ async function checkOrderedReplay( barrier.own.add(load) return true } + let replayScopeViolation: string | undefined const settleParticipant = ( source: BarrierSource, load: Load, @@ -561,6 +588,13 @@ async function checkOrderedReplay( barrier.own.has(load) && !options.signal?.aborted ) { + if ( + load.newSiblingReplay && + [...barrier.pending.values()].some( + (pending) => pending.subscription !== options.subscription, + ) + ) + reach.newSiblingReplayFailures++ barrier.failures.set(load, options) barrier.history.push(load) } @@ -569,15 +603,33 @@ async function checkOrderedReplay( // failure no longer fails the replay (ARCHITECTURE.md law 17: a failure // counts for a key no later held load loaded). if (ok && barrier?.pending.has(load) && !options.signal?.aborted) { + // Calibration of the replay account: a successful sibling can supply the + // same source keys without deciding another subscription's verdict. + // Preserve those failure identities independently of the mutation below. + const siblingFailures = [...barrier.failures.keys()].filter( + (failed) => failed.subscription !== load.subscription, + ) for (const failed of barrier.failures.keys()) { if ( + failed.subscription !== load.subscription && loads.indexOf(failed) < loads.indexOf(load) && failed.ordered === load.ordered && failed.keys !== undefined && [...failed.keys].every((key) => load.keys?.has(key)) ) + reach.siblingSuccessfulCoverage++ + if ( + loads.indexOf(failed) < loads.indexOf(load) && + failed.subscription === load.subscription && + failed.ordered === load.ordered && + failed.keys !== undefined && + [...failed.keys].every((key) => load.keys?.has(key)) + ) { barrier.failures.delete(failed) + } } + if (siblingFailures.some((failed) => !barrier.failures.has(failed))) + replayScopeViolation ??= `successful sibling removed another subscription's replay failure` } barrier?.pending.delete(load) // A replay completes when its last participant settles, so demand that @@ -594,7 +646,11 @@ async function checkOrderedReplay( const failures = barrier.failures.size for (const owned of [barrier.pending, barrier.failures]) { for (const [load, held] of owned) { - if (held === options) owned.delete(load) + if (held === options) { + if (owned === barrier.failures && load.newSiblingReplay) + reach.newSiblingReplayRetirements++ + owned.delete(load) + } } } // One failed demand retires while another still holds the barrier. @@ -758,6 +814,19 @@ async function checkOrderedReplay( ) ) reach.successWhileSubscriptionOwes++ + // Later successful coverage by this subscription is a recovery control. + // Coverage by a sibling cannot decide this subscription's failure. + if ( + loads.some( + (earlier) => + earlier.newSiblingReplay && + earlier.outcome === `failed` && + earlier.source === source && + earlier.subscription === options.subscription && + [...(earlier.keys ?? [])].every((key) => keys.has(key)), + ) + ) + reach.newSiblingReplayRecoveries++ loadedKeys.push({ source, keys, at: ++requestClock }) } const recordRequestFailure = ( @@ -890,6 +959,12 @@ async function checkOrderedReplay( requestErrorChecks: 0, successWhileSubscriptionOwes: 0, deferredMoveRejections: 0, + ownReplayFailureComparisons: 0, + siblingReplayFailureComparisons: 0, + siblingSuccessfulCoverage: 0, + newSiblingReplayFailures: 0, + newSiblingReplayRetirements: 0, + newSiblingReplayRecoveries: 0, replayLoads: 0, abortRejections: 0, preloadChecks: 0, @@ -942,11 +1017,20 @@ async function checkOrderedReplay( // owner, whose release unloads it. acquired.set(options, (acquired.get(options) ?? 0) + 1) const load: Load = { + subscription: options.subscription, epoch: truncates, outcome: `failed`, source: `markers`, keys: requestedKeys(`markers`, options), } + load.newSiblingReplay = + barriers.get(`markers`) !== undefined && + !barriers + .get(`markers`)! + .subscriptionsAtTruncate.has(options.subscription) && + [...(barriers.get(`markers`)?.pending.values() ?? [])].some( + (pending) => pending.subscription !== options.subscription, + ) loads.push(load) reach.markerLoads++ // The throw happens while the subscription registers the @@ -990,11 +1074,20 @@ async function checkOrderedReplay( const startedReady = allReady() acquired.set(options, (acquired.get(options) ?? 0) + 1) const load: Load = { + subscription: options.subscription, epoch: truncates, outcome: `pending`, source: `markers`, keys: requestedKeys(`markers`, options), } + load.newSiblingReplay = + barriers.get(`markers`) !== undefined && + !barriers + .get(`markers`)! + .subscriptionsAtTruncate.has(options.subscription) && + [...(barriers.get(`markers`)?.pending.values() ?? [])].some( + (pending) => pending.subscription !== options.subscription, + ) loads.push(load) reach.markerLoads++ const participant = joinBarrier(`markers`, load, options) @@ -1146,6 +1239,7 @@ async function checkOrderedReplay( const startedReady = allReady() acquired.set(options, (acquired.get(options) ?? 0) + 1) const load: Load = { + subscription: options.subscription, epoch: truncates, outcome: `pending`, source: `rows`, @@ -1367,7 +1461,12 @@ async function checkOrderedReplay( // failing, as a demand's replacement does, no longer count. A failure of a // load in no waited replay is the move's own window failure after it // resumed, which this law does not judge. + // The source-wide barrier combines subscriptions only for publication. + // Their verdicts remain separate: a sibling can fail while another replay + // still loads. Only failures of the rejecting subscription compete for its + // earliest error. Subscription identity comes from adapter request data. const covers = (later: Load, earlier: Load) => + later.subscription === earlier.subscription && later.ordered === earlier.ordered && later.keys !== undefined && earlier.keys !== undefined && @@ -1392,13 +1491,32 @@ async function checkOrderedReplay( ) if (candidates.length === 0) return reach.deferredMoveRejections++ + const rejected = loads.find((load) => load.error === error)! + for (const barrier of candidates) { + if ( + barrier.history.filter( + (load) => load.subscription === rejected.subscription, + ).length > 1 + ) + reach.ownReplayFailureComparisons++ + if ( + barrier.history.some( + (load) => load.subscription !== rejected.subscription, + ) + ) + reach.siblingReplayFailureComparisons++ + } const earliest = candidates.some((barrier) => barrier.history .slice( 0, barrier.history.findIndex((load) => load.error === error), ) - .every((earlier, at) => !stillDecides(barrier, earlier, at)), + .every( + (earlier, at) => + earlier.subscription !== rejected.subscription || + !stillDecides(barrier, earlier, at), + ), ) if (!earliest) moveErrorViolation ??= `move ${move} rejected with "${error.message}", not its replay's earliest failure that still decides it` @@ -1441,9 +1559,10 @@ async function checkOrderedReplay( const raised = [...barriers.values()] if (raised.some(({ ambiguous }) => ambiguous)) return if (live.status === `error`) return - expect(JSON.parse(JSON.stringify(live.toArray)), label).toEqual( - JSON.parse(retainedRows), - ) + expect( + JSON.parse(JSON.stringify(live.toArray)), + `${label}; new sibling replay failures: ${reach.newSiblingReplayFailures}`, + ).toEqual(JSON.parse(retainedRows)) if (raised.some(({ failures }) => failures.size > 0)) { reach.retainedFailedReplays++ } else reach.heldPublicationChecks++ @@ -1697,6 +1816,8 @@ async function checkOrderedReplay( if (moveOrderViolation) { throw new Error(`newest move wins: ${moveOrderViolation}`) } + if (replayScopeViolation) + throw new Error(`replay account scope: ${replayScopeViolation}`) if (moveErrorViolation) { throw new Error(`replay error identity: ${moveErrorViolation}`) } @@ -2429,6 +2550,190 @@ describe(`Ordered windows across truncate replays`, () => { ) }) + // A window with no initial include demand can activate its first include + // acquisition while a sibling join replay is still loading. Failure of that + // demand belongs to the shared publication barrier (law 9), even though its + // subscription had no replay of its own at the truncate. Adapter receipts + // prove both premises. This bounded grammar varies order, width, prior + // settlements and later demand changes; it reuses every ordinary observation + // and the independent retention account above. A move can retire the failed + // demand; a later truncate can replace it with successful own coverage. + it(`keeps new include failures behind a loading sibling replay`, async () => { + const states = [ + `newSiblingReplayFailures`, + `newSiblingReplayRetirements`, + `newSiblingReplayRecoveries`, + ] as const + const declared = createOracleReach(`replay.new-sibling-demand`, states) + for (const direction of [`desc`, `asc`] as const) { + for (const limit of [2, 1, 3] as const) { + for (const settles of [2, 1]) { + for (const change of [`mark`, `none`, `move`, `replay`] as const) { + declared.beginHistory() + const observed = await checkOrderedReplay( + [ + { type: `hold` }, + { type: `truncateMarkers` }, + ...Array.from({ length: settles }, () => ({ + type: `settle` as const, + ok: true, + keepHolding: true, + newest: false, + })), + { type: `settle`, ok: false, newest: true, keepHolding: true }, + ...(change === `none` + ? [] + : change === `replay` + ? [{ type: `truncateMarkers` as const }] + : change === `mark` + ? [{ type: `mark` as const, id: 1, on: false }] + : [ + { + type: `move` as const, + id: 1, + rank: direction === `desc` ? 60 : 1, + }, + ]), + ], + { + ...ordinaryOrderedScenario, + direction, + offset: 1, + limit, + joined: `none`, + includes: true, + markerMode: `on-demand`, + }, + ) + for (const state of states) { + if (observed[state] > 0) declared.reach(state) + } + } + } + } + } + console.info( + `replay.new-sibling-demand`, + Object.fromEntries(declared.counts()), + ) + declared.assertReached() + }, 120_000) + + // This finite history grammar varies which source replays, the order of + // two load failures, join form, direction and window width. Join plus include + // gives sibling subscriptions; the root replay gives same-subscription + // failures. The same comparison checks both, without an expected error + // copied from production or a source-wide error order. + it(`keeps replay error order within each subscription`, async () => { + const declared = createOracleReach( + `replay.subscription-error-order`, + [ + `ownReplayFailureComparisons`, + `siblingReplayFailureComparisons`, + ] as const, + { minimum: 5 }, + ) + for (const direction of [`asc`, `desc`] as const) { + for (const joined of [false, `some`, `none`, `inner`] as const) { + for (const limit of [1, 2, 3] as const) { + for (const markers of [false, true]) { + for (const newest of [false, true]) { + declared.beginHistory() + const observed = await checkOrderedReplay( + [ + { type: `hold` }, + markers + ? { type: `truncateMarkers` } + : { type: `truncate`, armAfterStart: false }, + { type: `window`, offset: 0, limit }, + { type: `settle`, ok: false, newest }, + { + type: `settle`, + ok: false, + newest: !newest, + keepHolding: false, + }, + ], + { + ...ordinaryOrderedScenario, + direction, + joined, + limit, + markerMode: `on-demand`, + includes: true, + }, + ) + for (const state of [ + `ownReplayFailureComparisons`, + `siblingReplayFailureComparisons`, + ] as const) { + if (observed[state] > 0) declared.reach(state) + } + } + } + } + } + } + console.info( + `replay.subscription-error-order`, + Object.fromEntries(declared.counts()), + ) + declared.assertReached() + }, 120_000) + + // Failure followed by successful coverage is the neighboring regime of + // the two-failure grammar above. Real adapter subscriptions supply identity; + // the shared account comparison requires a sibling's failure to survive. + it(`retains sibling replay failures through successful coverage`, async () => { + const declared = createOracleReach( + `replay.subscription-success-scope`, + [`siblingSuccessfulCoverage`] as const, + { minimum: 1 }, + ) + for (const direction of [`asc`, `desc`] as const) { + for (const joined of [false, `some`, `none`, `inner`] as const) { + for (const limit of [1, 2, 3] as const) { + for (const markers of [false, true]) { + for (const newest of [false, true]) { + declared.beginHistory() + const observed = await checkOrderedReplay( + [ + { type: `hold` }, + markers + ? { type: `truncateMarkers` } + : { type: `truncate`, armAfterStart: false }, + { type: `window`, offset: 0, limit }, + { type: `settle`, ok: false, newest, keepHolding: true }, + { + type: `settle`, + ok: true, + newest: !newest, + keepHolding: false, + }, + ], + { + ...ordinaryOrderedScenario, + direction, + joined, + limit, + markerMode: `on-demand`, + includes: true, + }, + ) + if (observed.siblingSuccessfulCoverage > 0) + declared.reach(`siblingSuccessfulCoverage`) + } + } + } + } + } + console.info( + `replay.subscription-success-scope`, + Object.fromEntries(declared.counts()), + ) + declared.assertReached() + }, 120_000) + it.each(orderedReplayCampaigns)( `keeps the ordered window under load and release faults (%s campaign)`, async (campaign) => { @@ -2455,6 +2760,12 @@ describe(`Ordered windows across truncate replays`, () => { requestErrorChecks: 0, successWhileSubscriptionOwes: 0, deferredMoveRejections: 0, + ownReplayFailureComparisons: 0, + siblingReplayFailureComparisons: 0, + siblingSuccessfulCoverage: 0, + newSiblingReplayFailures: 0, + newSiblingReplayRetirements: 0, + newSiblingReplayRecoveries: 0, replayLoads: 0, abortRejections: 0, preloadChecks: 0, @@ -2493,6 +2804,13 @@ describe(`Ordered windows across truncate replays`, () => { `discardedReplayMoves`, `requestErrorChecks`, `deferredMoveRejections`, + `ownReplayFailureComparisons`, + `siblingReplayFailureComparisons`, + `siblingSuccessfulCoverage`, + // The constructive new-sibling grammar declares these rare premises. + `newSiblingReplayFailures`, + `newSiblingReplayRetirements`, + `newSiblingReplayRecoveries`, `successWhileSubscriptionOwes`, ]) // Counts are histories, rather than repeated events in one trace. From ec25fc4f117c0b20fa0827f33b2a400a6e77a36a Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 30 Oct 2026 17:32:06 -0600 Subject: [PATCH 2/6] Scope replay request-error oracle to each subscription --- docs/contributing/oracle-coverage.md | 14 +++++ .../2026-10-30-release-replay-retention.md | 63 +++++++++++++++++++ ...ad-subset-replay-refinement-oracle.test.ts | 45 ++++++++++--- 3 files changed, 115 insertions(+), 7 deletions(-) diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 0821c9962e..648c33badc 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -6324,3 +6324,17 @@ for authority, RED, reach, mutant, grammar controls, historical gate limits and receiving-path limits. This is core Collection publication evidence and a Node SQLite/coordinator receiving witness with controlled Electric streams. It does not claim Effect, provider, framework, deeper include, or browser-process closure. + + +### Subscription-scoped request-error assertion (2026-10-30) + +The primary load-subset replay owner now retains subscription identity in its +later-request and successful-coverage account. A sibling requesting overlapping +source keys cannot exempt the failed demand's public health error. Its existing +96-history sibling-success grammar measures the actual owed-error assertion in +four histories, alongside four successful-sibling-coverage histories. A targeted +omitted-error observation passes the former source-wide account and fails the +scoped health comparison. All existing comparisons, histories, budgets and +reach minima remain. The controlled core, one-level include and peer limits +remain those of this owner; no provider or framework claim follows. See the +[versioned repair record](oracle-reviews/2026-10-30-release-replay-retention.md#subscription-scoped-request-error-checker-repair). diff --git a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md index 7917412d03..9e1b624ba0 100644 --- a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md +++ b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md @@ -152,3 +152,66 @@ The following SHA256 fingerprints bind the executable evidence to this change. T | `packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts` | `67c16d284522417f1a77e0a36efcc552b718f858ea8ec00771f001f001160782` | Focused ESLint reports zero errors and two existing `no-shadow` warnings. Prettier, whitespace, and `pnpm lint:changed` pass. No run budget decreased or disappeared. + +## Subscription-scoped request-error checker repair + +CodeRabbit review 5719714297 covered repair head +`0e5a5234b9fe4a26244a0e52d3862916a7f4ce7e`. Its one behavioral +finding identified a false-green request-error classification. Architecture +law 16 and the glossary's request-error rule already scope a lazy demand's +error to that demand. A sibling subscription asking for or loading the same +source keys cannot decide it. Shared publication does not merge verdicts. +The existing complete authoritative source-recovery control still decides the +held demands together; an individual sibling verdict does not supply it. + +The old account stored successful keys by source alone. Both its later-request +and later-coverage exemptions omitted subscription identity. The repair keeps +that identity in the recorded successful acquisition and requires it in both +exemptions. This is an oracle enforcement repair. Production is unchanged. + +The existing 96-history sibling-success grammar varies direction, join form, +width, replay source and settlement order. A receipt probe found four histories +with eight observation cuts where the old account exempted an error the scoped +account owed. Production listed the exact injected error at all eight cuts. +The cut concerns a later sibling request; the experiment did not establish a +later successful sibling-coverage cut with an otherwise owed request error. +An initial reach counter for that stronger premise correctly failed at zero. +It remains in the scratch evidence. The final declared reach names the actual +sibling-request error comparison and reaches four histories. The existing +successful-sibling-coverage reach also remains four. + +Calibration supplies a wrong public observation to the same health comparison: +omit the injected error only at those distinguishing cuts. The old account +accepts it. The scoped account rejects it at `after settle: request error`, +with expected true and actual false. A broader temporary production mutant +that suppressed every ready-query replay error already failed the old checker; +it is not evidence for this newly repaired gap. Both temporary sources were +restored. The real production owner passes all 45 tests, including fixed and +random campaigns, with the stronger classifier. The reported seeds and paths +from issues 2294 and 2315 each pass the direct replay interface as well. These +are bounded replay results, not closure of either whole issue or provider +adapter conformance from issue 2118. + +ORC review (author self-review): 001/002/003 use the accepted demand-scoped +law, independently evaluated request keys and explanatory local prose. +004/013 retain all 96 legal histories and their direction/join/width/source/ +settlement neighbors; collapsing subscription identity is the wrong rule. +005/006/015 exercise the real Collection sync path and public health error +identity after each command, measure four distinguishing histories and reject +the targeted omission. 007 retains all fixed/random campaigns, budgets and +seed/path replays. 008/009 retain subscription identity because equal source +keys can have different demand verdicts. 010 retains primary mismatch and +cleanup separation. 011 has no new shared production/model semantic-fault +hypothesis: the defect was an exemption before the assertion. 012 is this +versioned bounded record. 014 keeps the controlled core boundary and makes no +provider/framework handoff claim. 016 adds no unit expectation or standalone +fixture. No law, comparison, budget or prior reach minimum was weakened. + +The checker evidence is stored under +`/private/tmp/callback-metadata/release-replay-repair/review-5719714297`. +Commands use the saved `vitest.config.mjs` for the full primary owner and +`-t 'retains sibling replay failures through successful coverage'` for the +finite calibration, with one worker and dependency verification disabled. +The amended primary owner SHA256 is `a2ab454ff5a77aa47bf57cbd590b67a2a5602d3dd6897af051fb1819b9482ce7` on both branches. The earlier +table binds the original repair evidence. Its runtime and browser hashes remain +unchanged; callback runtime hashes remain bound by its separate port record. diff --git a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts index 6f94d276e3..7ad2fdfd13 100644 --- a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts @@ -320,6 +320,8 @@ type OrderedReach = { ownReplayFailureComparisons: number siblingReplayFailureComparisons: number siblingSuccessfulCoverage: number + // Owed request-error comparisons after another subscription asked for its keys. + siblingRequestErrorChecks: number newSiblingReplayFailures: number newSiblingReplayRetirements: number newSiblingReplayRecoveries: number @@ -749,18 +751,19 @@ async function checkOrderedReplay( // request error on the query's own node. The error stays until one of these // meets it: // - its demand retires, which aborts its request and unloads it; - // - a later load of the source asks for some of its keys: that newer + // - a later load of the same subscription asks for some of its keys: that newer // request of the demand decides them, and keys it no longer asks for // left the demand (ARCHITECTURE.md law 16: a lazy demand's error clears // when that demand settles or is released; a key's latest load decides // it); - // - a later load of the same source covers its keys; + // - a later load of the same subscription covers its keys; // - a replay of its source that rose after the failure completes, since a // replay asks again for every held demand. // A failure that joins a replay waits for its subscription's verdict, // which comes once that subscription's participants settled, so it owes // no error before then. The barrier that holds publication stays shared - // across the query's subscriptions to the source. Keys come from evaluating the request's + // across the query's subscriptions to the source. A sibling's successful + // load cannot decide this demand's error. Keys come from evaluating the request's // `where` over a small key domain, independent of production's bookkeeping. // The account combines the production concepts of a failed request and the // replay verdict that decides it; it names no production structure. @@ -790,6 +793,7 @@ async function checkOrderedReplay( const requestFailures: Array = [] const loadedKeys: Array<{ source: BarrierSource + subscription: LoadSubsetOptions[`subscription`] keys: Set at: number }> = [] @@ -827,7 +831,12 @@ async function checkOrderedReplay( ) ) reach.newSiblingReplayRecoveries++ - loadedKeys.push({ source, keys, at: ++requestClock }) + loadedKeys.push({ + source, + subscription: options.subscription, + keys, + at: ++requestClock, + }) } const recordRequestFailure = ( load: Load, @@ -868,6 +877,7 @@ async function checkOrderedReplay( (later, index) => index > at && later.source === failure.source && + later.subscription === failure.options.subscription && !later.ordered && [...failure.keys].some((key) => later.keys?.has(key)), ) @@ -875,6 +885,7 @@ async function checkOrderedReplay( const covered = loadedKeys.some( (loaded) => loaded.source === failure.source && + loaded.subscription === failure.options.subscription && loaded.at > failure.at && [...failure.keys].every((key) => loaded.keys.has(key)), ) @@ -962,6 +973,7 @@ async function checkOrderedReplay( ownReplayFailureComparisons: 0, siblingReplayFailureComparisons: 0, siblingSuccessfulCoverage: 0, + siblingRequestErrorChecks: 0, newSiblingReplayFailures: 0, newSiblingReplayRetirements: 0, newSiblingReplayRecoveries: 0, @@ -1442,6 +1454,19 @@ async function checkOrderedReplay( if (owed.length === 0) return reach.requestErrorChecks++ for (const failure of owed) { + // Reach is measured at the health assertion, after a sibling asked + // for overlapping keys that cannot decide this subscription's error. + if ( + loads.some( + (later, index) => + index > loads.indexOf(failure.load) && + later.source === failure.source && + later.subscription !== failure.options.subscription && + !later.ordered && + [...failure.keys].some((key) => later.keys?.has(key)), + ) + ) + reach.siblingRequestErrorChecks++ expect( [live, peer].some((query) => query?.health.error?.errors.some( @@ -2687,7 +2712,7 @@ describe(`Ordered windows across truncate replays`, () => { it(`retains sibling replay failures through successful coverage`, async () => { const declared = createOracleReach( `replay.subscription-success-scope`, - [`siblingSuccessfulCoverage`] as const, + [`siblingSuccessfulCoverage`, `siblingRequestErrorChecks`] as const, { minimum: 1 }, ) for (const direction of [`asc`, `desc`] as const) { @@ -2720,8 +2745,12 @@ describe(`Ordered windows across truncate replays`, () => { includes: true, }, ) - if (observed.siblingSuccessfulCoverage > 0) - declared.reach(`siblingSuccessfulCoverage`) + for (const state of [ + `siblingSuccessfulCoverage`, + `siblingRequestErrorChecks`, + ] as const) { + if (observed[state] > 0) declared.reach(state) + } } } } @@ -2763,6 +2792,7 @@ describe(`Ordered windows across truncate replays`, () => { ownReplayFailureComparisons: 0, siblingReplayFailureComparisons: 0, siblingSuccessfulCoverage: 0, + siblingRequestErrorChecks: 0, newSiblingReplayFailures: 0, newSiblingReplayRetirements: 0, newSiblingReplayRecoveries: 0, @@ -2807,6 +2837,7 @@ describe(`Ordered windows across truncate replays`, () => { `ownReplayFailureComparisons`, `siblingReplayFailureComparisons`, `siblingSuccessfulCoverage`, + `siblingRequestErrorChecks`, // The constructive new-sibling grammar declares these rare premises. `newSiblingReplayFailures`, `newSiblingReplayRetirements`, From b73f8e865bdda471df9dc77cc471e927143a41a9 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 2 Nov 2026 07:44:43 -0700 Subject: [PATCH 3/6] fix(db): enroll new demand in sibling replay Law: ARCHITECTURE law9 / Replay participants and failure requires demand started during sibling replay to hold complete publication. Verdicts stay subscription-scoped under Atomic window publication. Reach: 48 new direction/width/order/outcome/waiting-move histories, with24 late active settlements and12 retry starts. Existing receipt reach44/9/12. RED: all48 public retention failures on unchanged repair ec25 and callback a2e75c3 before each production edit. Earlier masked probes are retained. GREEN: each branch passes46 primary,21 neighboring and4 browser tests, including both80-history campaigns and both final-delivery replays. Mutant: admission removal, latest own error, omitted owed error, final restart omission and final commit omission fail intended public comparisons. Limits: controlled core adapters and Node-host SQLite/coordinator streams; finite one-level include histories. Full pnpm test/builds not run. Reach corrections after first runtime trial are explicitly recorded. Preserve arbitrary browser endings with reset/stop terminal regimes and measure actual final-catchup branches. Remove model-only pseudo-calibration, measure sibling reach from adapter receipts, and correct error-scope prose. Lossless review record: seven fixed-now, two accepted-design, no deferrals. --- .changeset/retain-loading-sibling-replay.md | 2 +- docs/contributing/oracle-coverage.md | 34 ++ .../2026-10-30-release-replay-retention.md | 228 +++++++++++++ .../tests/multi-tab-electric-oracle.test.ts | 64 +++- packages/db/src/collection/subscription.ts | 160 +++++---- packages/db/src/query/live/ARCHITECTURE.md | 4 +- .../query/live/collection-config-builder.ts | 4 +- ...ad-subset-replay-refinement-oracle.test.ts | 312 ++++++++++++++---- 8 files changed, 663 insertions(+), 145 deletions(-) diff --git a/.changeset/retain-loading-sibling-replay.md b/.changeset/retain-loading-sibling-replay.md index 64368eee0d..f7991e70b2 100644 --- a/.changeset/retain-loading-sibling-replay.md +++ b/.changeset/retain-loading-sibling-replay.md @@ -2,4 +2,4 @@ '@tanstack/db': patch --- -Keep the last complete live-query result when a new include demand fails while a sibling subscription is still replaying after truncate. A successful sibling load cannot publish an incomplete replacement. +Keep the last complete live-query result while a new include demand loads under a sibling subscription's truncate replay. The demand now joins the publication barrier when it starts, so a sibling that settles first cannot publish an incomplete replacement. A failed held demand keeps that result until its own coverage recovers or its demand retires. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 648c33badc..dc8aee8e72 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -6338,3 +6338,37 @@ scoped health comparison. All existing comparisons, histories, budgets and reach minima remain. The controlled core, one-level include and peer limits remain those of this owner; no provider or framework claim follows. See the [versioned repair record](oracle-reviews/2026-10-30-release-replay-retention.md#subscription-scoped-request-error-checker-repair). + + +### First-demand admission and terminal regimes (2026-11-02) + +The load-subset replay owner adds 48 finite histories crossing sibling/include +settlement order, direction, width, failure/success and a waiting window. A new +parent remains eligible after sibling success. Before admission repair, all 48 +fail the existing public-retention comparison. The repaired driver measures 24 +late active include settlements and 12 retries while a sibling loads, directly +from adapter receipts. The original sibling grammar's receipt reaches are now +44 failures, 9 same-truncate failed-demand unloads during commands and 12 own +recoveries; its earlier model-derived retirement count 33 is historical. All +existing semantic comparisons and required minima stay. The original-admission +mutant fails retention. + +The earliest-error comparison was corrected from source-wide failure order to +subscription-owned verdicts. Authority is the pre-existing maintainer decision +in ARCHITECTURE **Atomic window publication**: a move rejects with *that replay's +own* earliest still-deciding failure. Shared publication does not merge verdicts. +This is an explicit account of the correction, not a claim that the assertion +was unchanged. There is no source-wide race winner promise. The same-subscription +latest-error mutant fails; the actual public owed-error check also rejects an +error omitted after sibling coverage. Moves that resume into a different failed +replay retain the owner's stated limit. A failed move is not promised success +merely because a later retry recovers its demand. + +The multi-tab Electric owner now generates untouched arbitrary, reset-abandoned +and stopped-source endings. Existing final comparisons, 80-run budgets, known +gaps and minima stay; new reach separately measures final stopped-source restart +and lagging-row rewrite. Old grammar reach was 0/46, so only the first was absent. +Final-restart and final-commit omissions fail the real SQLite/coordinator public +comparisons. These are Node-host controlled-stream receiving histories, not real +provider or browser-process claims. The core and browser path limits are unchanged. +See the [nine-item review amendment](oracle-reviews/2026-10-30-release-replay-retention.md#first-demand-admission-and-terminal-grammar-review-2026-11-02). diff --git a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md index 9e1b624ba0..e81ab11d51 100644 --- a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md +++ b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md @@ -215,3 +215,231 @@ finite calibration, with one worker and dependency verification disabled. The amended primary owner SHA256 is `a2ab454ff5a77aa47bf57cbd590b67a2a5602d3dd6897af051fb1819b9482ce7` on both branches. The earlier table binds the original repair evidence. Its runtime and browser hashes remain unchanged; callback runtime hashes remain bound by its separate port record. + + +## First-demand admission and terminal grammar review (2026-11-02) + +This amendment evaluates all nine findings from the human-supplied review of +`ec25fc4f117c0b20fa0827f33b2a400a6e77a36a`. The original review was unverified; +its examples were hypotheses. This is an author self-review. The raw review, +append-only nine-item ledger, source bindings and complete logs are retained in +`/private/tmp/callback-metadata/release-replay-repair/nine-item-review-20261102`. + +### Law, experiment and inference + +**Law and authority.** ARCHITECTURE **Replay participants and failure**, law 9, +requires new demand started under a sibling's replay to share publication. +**Atomic window publication** separately permits a move to reject once a waited +replay fails. The earliest-error promise is that replay's own earliest still +deciding failure, an explicit maintainer decision already present on release +base `e75f8455a78bc33c43376288218e9912dc2a3f4c`; it imposes no global failure order +across independent subscription verdicts. Laws 16/17 scope request errors to +the failed demand. Publication retention and operation success are different +judgments. + +**Experiment before production.** The existing primary owner gained a provider +settlement selector and a 48-history grammar: direction, width, include/sibling +settlement order, success/failure and presence of a waiting move. An unmarked +sixth parent remains eligible after the sibling succeeds. All 48 histories fail +on unchanged ec25 at the existing public retention assertion: row 6 publishes +while the retained result is empty and include work remains held. The mismatch +can occur before include failure, so changing only `failDemand` cannot close +this ordering. Earlier probes without the persistent parent passed; obsolete +speculative includes retired, or other held loads masked the publication cut. +Those passing probes were not treated as refutations. Both 96-history error +campaigns passed before production changed. The real core receiving path is an +ordered live-query Collection, its join and one-level inline include over real +Collections with controlled adapter promises. No new framework/provider promise +requires a different receiving driver for this admission repair. + +**Repair.** First demand now creates its own replay hold when a sibling replay +still loads, before calling the adapter. Setup itself holds publication through +reentrancy, and the existing participant machinery handles settlement, abort, +retirement and retry. Sibling success can no longer remove this subscription's +hold. The failure fallback remains for the existing failure path. + +**Reach.** The final selector chooses active, un-aborted acquisition receipts; +it drains other work while keeping the selected include held. The 48 histories +reach 24 late active settlements and 12 retry starts while a sibling loads. +The selector and retry counters were corrected after the first runtime trial; +the original 48-history public-retention RED remains the pre-edit gate. Do not +claim the corrected reach counters ran before that trial. The first full run +passed all behavior comparisons but had three reach-only failures: one selector +included retired speculative requests, and two unconstrained campaigns inherited +new obligations owned by the constructive grammar. These were corrected without +removing any pre-existing reach or minimum. + +New-sibling classification, failure and retirement reach no longer read model +barriers. The driver snapshots acquisitions at the actual truncate and uses +adapter starts, failures, aborts and unloads. The original 48-cell grammar now +measures 44 failures, 9 same-truncate failed-demand unloads during commands, and +12 later own recoveries. The historical retirement count 33 was model-derived; +it is not the count of this stricter receipt witness. Final cleanup and a newer +truncate's physical-acquisition renewal supply no retirement reach. Semantic +retention, request-error and recovery comparisons remain unchanged. + +**Calibration.** Disabling admission reproduces the intended retention RED. +Reporting the latest own failure instead of the earliest fails error identity. +Omitting an actually observed public error after successful sibling coverage +fails the ordinary owed-request-error assertion. The removed model-only +`replayScopeViolation` assertion proved no production behavior; its replacement +evidence is this public observation calibration, not a second model invariant. +All temporary subjects were restored in `finally` and their hashes verified. + +### Browser terminal regimes + +The old suffix's fixed 80-history run reaches zero stopped-source restarts at +final catch-up. Its existing final comparisons do exercise a lagging-row rewrite +in 46 histories, so the review's assertion that both regimes are absent is too +broad. The missing stopped-source reach is a grammar failure, not a production +mismatch. A terminal dimension now keeps the original arbitrary final state or +constructs a reset-abandoned commit or a stopped source. The 24-step stop prefix, +8–24-step arbitrary tail, 80 accepted histories per campaign, all comparisons, +existing minima and known-gap handling remain. The constructive endings add +`4 + 2 * tabCount` steps (8 or 10); an untouched ending adds none. + +The real SQLite/coordinator driver measures both final branches. Fixed/random +campaigns pass with final stopped-source restart reach 53/42, lagging-row rewrite +58/52 and reset-abandoned final recovery 22/35. Temporary omission of final +restart fails the public source-head comparison; omission of final commit fails +an ordinary durable/public comparison. This is the separate release CI reach +repair described earlier, not evidence about include admission. Sampling the +terminal dimension is bounded evidence, not a universal guarantee of reach for +every possible future random seed. Required minima continue to fail any campaign +that misses a declared state. + +### Lossless item accounting + +| ID | Technical judgment | Action and durable value | +| --- | --- | --- | +| R1 | Confirmed production and history gap; the narrower failure-only fix missed admission. | Fixed first-demand admission and added settlement order with a persistent parent. Original-admission mutant fails retention. | +| R2 | Rejection despite eventual successful retry is allowed by Atomic window publication. An already failed operation need not become successful. | Accepted design. Keep rejection separate from shared retention; the new admission path participates normally rather than depending on synthetic failed completion. Retry reach is explicit. No success-after-retry promise was added. | +| R3 | The self-check follows directly from the model's guarded deletion loop and does not calibrate production. Existing health comparisons have independent sensitivity. | Removed the redundant model self-check. A wrong actual health observation is rejected at the owed-error comparison. | +| R4 | Cross-subscription earliest-error ordering is not promised. The scope change aligns with an existing maintainer decision. Its reporting needed to be explicit. | Accepted design; coverage map now cites the authority and records the comparison correction. A latest-own-error mutant fails. A claim that no assertion changed would be inaccurate: the old source-wide expectation was corrected, not preserved. | +| R5 | Stopped-tab final recovery was absent; lagging-row final rewriting was reached. The suffix still removed arbitrary final states. | Added terminal regimes and actual branch reach. Kept the separate browser release-CI purpose explicit. Final restart/commit mutants fail. | +| R6 | The claim of receipt-only reach was inaccurate because classification read model barriers. | All newly claimed reach comes from actual adapter receipts and truncate/command boundaries. Model-based historical counts are not relabeled as receipt measurements. | +| R7 | Confirmed duplicate classifier. | One classifier serves both marker adapter paths. | +| R8 | Confirmed incomplete explanatory comment. | Comment now names sibling settlement and retirement. Architecture clarifies admission before adapter reentry. | +| R9 | Confirmed missing suffix length. | Prose states prefix, arbitrary tail and optional 8/10-step ending separately. | + +Totals: seven `fixed-now`, two `accepted-design` (R2/R4); no deferred finding. +R4's reporting correction is complete even though its proposed extra product +obligation is not accepted. Every original claim and suggested repair has a +separate ledger entry. No global source-wide error order or automatic success +of an already failed operation was introduced. + +### Reviewer assessment and limits + +Qualified hire. The reviewer found a real admission defect and useful test +integrity, reach and prose problems. The proposed admission location was sound. +The review explicitly labeled its production claims unverified. It still +confused operation rejection with publication retention, missed an explicit +maintainer decision, and overstated the browser reach loss. Executable probes +and authority checks are needed before treating its downstream claims as facts. + +The claim remains bounded by this owner's finite histories, one ordered source, +one-level include, no peer for retention, controlled core adapter and named +public checkpoints. Existing initial-readiness, abort, retirement, request-error, +error-identity and convergence checks remain. This is not Effect disposal, +arbitrary provider, framework, deep-include or multi-process browser closure. +An evaluated review does not establish whole-PR readiness: final pushed CI and +integration with the actual main branch must be assessed separately. Neither +PR is authorized to merge by this review. + + +### Final validation and source binding + +The browser counts above bind the earlier expanded-grammar run in +`browser-expanded.log`. Final runs use the same grammar, comparisons and +original 80-accepted-history budgets. Their measured final branches are: + +| Branch/run | Stopped-source restart | Lagging-row rewrite | Reset-abandoned final recovery | +| --- | ---: | ---: | ---: | +| Repair fixed | 53 | 58 | 22 | +| Repair random | 48 | 52 | 31 | +| Callback fixed | 53 | 58 | 22 | +| Callback random | 46 | 58 | 31 | + +On each branch, one fresh full primary run passes all 46 tests, the three +neighboring files pass 21 tests, and the browser selection passes both +campaigns plus both existing final-delivery replays (four tests). Focused core +and browser TypeScript, ESLint, formatting, whitespace and `pnpm lint:changed` +pass on both branches. ESLint retains two existing shadow warnings. Whole +repository `pnpm test`, builds and installs did not run. Local Vitest is cached +3.2.4; these runs do not replace final-head CI. + +Callback browser setup initially missed Electric, debug and SQLite resolution. +A subsequent scratch mapping separated the mock from the adapter's SDK module, +so no controlled stream started. Those runs are setup failures, not behavioral +RED. The final mapping resolves both SDK imports to the same installed module +and runs the callback branch's actual sources. No repository dependency file +or installation changed. + +The repair is based on `ec25fc4f117c0b20fa0827f33b2a400a6e77a36a`; the port is +based on `a2e75c30402deb815abd01783ea816a2edfe3ea8`, which already integrates +main `85392bd1d349aa3bb77c9c11ffba671f0b2d2a84`. Both final trees share these +SHA256 bindings: + +| Source | SHA256 | +| --- | --- | +| `packages/db/src/collection/subscription.ts` | `5cd5a8759dc38bf70c88ed756b6886e474afef5ad02f4419bd0f5d1ab6b39dc0` | +| `packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts` | `d3c4edf87d4f046d888887ab4dea6e16ed40571a8e4334850427573d92c51271` | +| `packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts` | `3184cd014be5ab6222350525bbd3d09a78b46e49c5e8b17fdde6ffbc417f26c5` | + +The containing commit versions this evidence; final commit IDs and all changed +path hashes are additionally bound in the scratch `final-binding.json` and the +published PR descriptions. The builder differs between branches because the +callback branch retains its plural joined-dependency gate. This review changes +only its explanatory comment. Compiler and Solid source/test paths are +unchanged from each branch's starting head. Earlier runtime fingerprints and +size figures remain historical, because admission intentionally changes runtime. + +### Oracle guide audit for this amendment + +This is author self-review, not an independent external review. + +- ORC-001/002/003: the architecture and existing maintainer decisions supply + authority. The independent source-row/barrier model and server log remain + separate from real drivers. Local prose explains the new grammar and cuts. +- ORC-004/013: direction, widths 1–3, both settlement orders, success/failure and + waiting/no-waiting moves reconstruct the new core witness. Removing the + persistent parent masks it through retirement; removing order variation + misses sibling-first publication. Browser arbitrary/reset/stop endings keep + the prior arbitrary domain and distinguish final recovery branches. Aborting + an acquisition excludes it from the active demand premise. Closing a tab for + good remains outside the existing browser convergence claim. +- ORC-005/006: core compares actual public rows and request errors after commands, + quiet loading and authoritative recovery. SQLite/coordinator compares durable + and public rows/status at existing cuts. Admission, own-error, owed-error, + final-restart and final-commit controls reject at intended assertions, not + setup or timeout. All temporary subjects were restored. +- ORC-007: fixed and random campaigns retain the same generators, comparisons + and budgets. The existing direct seed/path interface and two final-delivery + replays remain. The new 48-cell core grammar is bounded enumeration, so it + does not need a second random copy. Existing related-issue seed/path results + remain bounded historical evidence. +- ORC-008/009: removing model-only calibration state cannot change a promised + result. Acquisition receipt state belongs to the driver and measures reach, + not expected publication. Publication stays source-shared while verdicts stay + subscription-owned. These distinctions use the glossary's demand/acquisition + boundary and the documented barrier abstraction. +- ORC-010: finite enumeration preserves every mismatch in an AggregateError, + with its first failure as cause. Existing cleanup still releases real resources. + Setup and reach failures remain separate from behavioral mismatches. +- ORC-011: the concrete shared-fault hypothesis was model-derived reach. The + repair measures actual adapter receipts independently of model barriers. No + additional shared semantic fault requiring a second model was identified. +- ORC-012/015: this versioned record reports limits, the original public RED, + final GREEN, hostile controls, both branches' hashes and declared reach. New + constructive reach is not imposed on unrelated arbitrary campaigns. +- ORC-014: the claims end at controlled core adapters and Node-host real SQLite/ + coordinator code. Controlled Electric streams and peer-reset messages supply + premises. No real-provider or browser-process handoff claim follows. +- ORC-016: new finite histories use the existing shared model/driver/comparison. + Final-delivery replays carry inputs without separate expected values. No unit + assertion or concrete type assertion was added to an oracle. + +No raw review item is deferred. The remaining limitations are the owners' +explicit controlled and finite boundaries, not a known reachable violation of +the repaired admission law. Final pushed CI and repair integration with actual +main remain separate readiness obligations. No merge authorization follows. diff --git a/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts b/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts index 341715569a..1dc3261503 100644 --- a/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts +++ b/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts @@ -973,7 +973,9 @@ describe(`multi-tab Electric over one persisted database`, () => { // Every stop kind occurs in a generated order, with a distinct running // peer. Recovery and catch-up separate the blocks. Four membership toggles // of an initially absent key restore the original rows before the tail. - // This adds 24 steps to the unchanged 8–24-step arbitrary tail. Removing + // The prefix adds 24 steps to the unchanged 8–24-step arbitrary tail. An + // ending dimension either leaves that final state untouched or adds a + // constructive reset/stop suffix of 4 + 2*tabCount steps (8 or 10). Removing // the open delivery, stop, or peer commit loses the law's premise; recovering // before the peer commit would test a different boundary. Existing pinned // histories still use the same driver without this prefix. @@ -983,6 +985,11 @@ describe(`multi-tab Electric over one persisted database`, () => { source: fc.nat(2), key: fc.constantFrom(3, 4), recovery: fc.nat(1), + ending: fc.constantFrom( + `arbitrary` as const, + `reset` as const, + `stop` as const, + ), stopOrder: fc.shuffledSubarray([0, 1, 2, 3], { minLength: 4, maxLength: 4, @@ -995,6 +1002,7 @@ describe(`multi-tab Electric over one persisted database`, () => { source, key, recovery, + ending, stopOrder, steps, }): GeneratedHistory => { @@ -1012,26 +1020,32 @@ describe(`multi-tab Electric over one persisted database`, () => { { kind: `catch up`, tab, pick: 0 }, ]), ...steps, - // Final catch-up must encounter an abandoned reservation in every - // generated history. Start a fresh handle, recover every tab, and - // deliver one new change without committing it. Reset then abandons - // that reservation. No later history step commits or recovers it; - // the existing final-delivery comparison must observe its failure - // before the existing recovery establishes convergence (Laws 2/5). + // Preserve arbitrary final states as well as constructing the rare + // final-reset and stopped-tab boundaries. The constructive endings + // start a fresh handle, recover every tab, then deliver one change + // without committing it. Reset abandons that reservation, so final + // delivery judges its failure before recovery (Laws 2/5). Stop + // instead leaves final catch-up to restart the stopped source. // The source tab, key and two/three-tab host vary with this grammar. // Removing advance or reset loses that premise; catching up here // would move the failure out of the final observation cut. - { kind: `deploy` }, - ...Array.from( - { length: tabCount }, - (_, index): Array => [ - { kind: `recover`, tab: index, pick: recovery }, - { kind: `catch up`, tab: index, pick: 0 }, - ], - ).flat(), - { kind: `server change`, key, pick: 0 }, - { kind: `advance`, tab, pick: 0 }, - { kind: `peer reset` }, + ...(ending === `arbitrary` + ? [] + : [ + { kind: `deploy` as const }, + ...Array.from( + { length: tabCount }, + (_, index): Array => [ + { kind: `recover`, tab: index, pick: recovery }, + { kind: `catch up`, tab: index, pick: 0 }, + ], + ).flat(), + { kind: `server change` as const, key, pick: 0 }, + { kind: `advance` as const, tab, pick: 0 }, + ending === `reset` + ? { kind: `peer reset` as const } + : { kind: `stop` as const, tab, pick: stopOrder[0]! }, + ]), ], } }, @@ -1526,9 +1540,19 @@ describe(`multi-tab Electric over one persisted database`, () => { models[index]!.stopped !== undefined || models[index]!.failedByReset ) { + generatedReach.reach(`stopped source restarted during final catch-up`) await recover(index, `restart`) } + const laggingBeforeFinal = + rowsOf(durable).some(({ id, label }) => log.rows.get(id) !== label) || + rowsOf(log.rows).some(({ id, label }) => durable.get(id) !== label) await deliver(index, `all`, true) + if ( + laggingBeforeFinal && + !models[index]!.failedByReset && + JSON.stringify(rowsOf(durable)) === JSON.stringify(rowsOf(log.rows)) + ) + generatedReach.reach(`lagging commit rewritten during final catch-up`) // A reset-abandoned transaction fails only when delivery tries to // commit it. Judge Law 5 at that cut before the restart establishes // Law 2's successful catch-up premise. @@ -1716,6 +1740,8 @@ describe(`multi-tab Electric over one persisted database`, () => { ) type GeneratedReach = + | `stopped source restarted during final catch-up` + | `lagging commit rewritten during final catch-up` | `must-refetch delivered` | `peer commit during an open reserved transaction` | `source stopped with an open transaction` @@ -1738,6 +1764,8 @@ describe(`multi-tab Electric over one persisted database`, () => { const createGeneratedReach = () => createOracleReach(`browser.multi-tab-reserved-history`, [ + `stopped source restarted during final catch-up`, + `lagging commit rewritten during final catch-up`, `must-refetch delivered`, `peer commit during an open reserved transaction`, `source stopped with an open transaction`, diff --git a/packages/db/src/collection/subscription.ts b/packages/db/src/collection/subscription.ts index 949e0eb4d4..552c2f96c5 100644 --- a/packages/db/src/collection/subscription.ts +++ b/packages/db/src/collection/subscription.ts @@ -1185,83 +1185,109 @@ export class CollectionSubscription result: LoadSubsetRequestResult started: boolean } { - const demand: SubsetDemand = { - requestOptions, - acquisition: { - options: requestOptions, - syncRunGeneration: this.collection._sync.getSyncRunGeneration(), - }, - acquisitionState: `starting`, - } - // A demand that starts during an open replay joins it, parked or not (law - // 9: a new demand joins the open replay). - const replayState = this.truncateReplayState - const replayAttempt = replayState?.currentAttempt + // Law 9: a first demand joins the shared publication barrier when it + // starts, even if this subscription held no demand at truncate. Waiting + // until failure loses the hold when the sibling settles first. + let joinedReplay: TruncateReplayState | undefined if ( - this.collection.status === `cleaned-up` || - // A deferring subscription starts no acquisition attempt until it resumes. - (this.defersAcquisition && - this.collection.config.syncMode === `on-demand`) || - // Ready/error callbacks can run before sync returns its loader. Idle - // deferred starts still acquire through the sync manager's queue. - (this.collection.config.syncMode === `on-demand` && - (isFailedStatus(this.collection.status) || - (this.collection.status !== `idle` && - this.collection._sync.syncLoadSubsetFn === null))) + !this.truncateReplayState && + this.options.truncateReplayPublication?.siblingReplays() ) { - demand.acquisitionState = `detached` + joinedReplay = this.createTruncateReplayState( + this.collection._sync.getSyncRunGeneration(), + () => new Map(), + ) + this.truncateReplayState = joinedReplay + this.truncateReplacementPending = true + } + const joinedAttempt = joinedReplay?.currentAttempt + try { + const demand: SubsetDemand = { + requestOptions, + acquisition: { + options: requestOptions, + syncRunGeneration: this.collection._sync.getSyncRunGeneration(), + }, + acquisitionState: `starting`, + } + // A demand that starts during an open replay joins it, parked or not (law + // 9: a new demand joins the open replay). + const replayState = this.truncateReplayState + const replayAttempt = replayState?.currentAttempt + if ( + this.collection.status === `cleaned-up` || + // A deferring subscription starts no acquisition attempt until it resumes. + (this.defersAcquisition && + this.collection.config.syncMode === `on-demand`) || + // Ready/error callbacks can run before sync returns its loader. Idle + // deferred starts still acquire through the sync manager's queue. + (this.collection.config.syncMode === `on-demand` && + (isFailedStatus(this.collection.status) || + (this.collection.status !== `idle` && + this.collection._sync.syncLoadSubsetFn === null))) + ) { + demand.acquisitionState = `detached` + this.subsetDemands.push(demand) + const initialResult = createDeferred() + demand.initialResult = initialResult + const abort = () => + initialResult.reject(new LoadSubsetOperationAbortedError()) + requestOptions.signal?.addEventListener(`abort`, abort, { once: true }) + const finish = () => { + requestOptions.signal?.removeEventListener(`abort`, abort) + demand.initialResult = undefined + } + void initialResult.promise.then(finish, finish) + if (replayState && replayAttempt) + demand.parkedParticipant = this.trackTruncateReplayParticipant( + replayState, + replayAttempt, + demand, + initialResult.promise, + requestOptions.signal, + ) + return { demand, result: initialResult.promise, started: false } + } + const acquisition = this.createSubsetAcquisitionRecord(demand) + demand.acquisition = acquisition + const syncRunGeneration = this.collection._sync.getSyncRunGeneration() + // Reentrant release must see the exact acquisition before adapter work + // starts. A load that throws is a rejection: it keeps its owner. this.subsetDemands.push(demand) - const initialResult = createDeferred() - demand.initialResult = initialResult - const abort = () => - initialResult.reject(new LoadSubsetOperationAbortedError()) - requestOptions.signal?.addEventListener(`abort`, abort, { once: true }) - const finish = () => { - requestOptions.signal?.removeEventListener(`abort`, abort) - demand.initialResult = undefined + const result = this.collection._sync.loadSubset(acquisition.options) + + if (!this.isSyncRunGenerationCurrent(syncRunGeneration)) { + const demandIndex = this.subsetDemands.indexOf(demand) + if (demandIndex !== -1) this.subsetDemands.splice(demandIndex, 1) + cancelAcquisition(acquisition) + return { demand, result, started: true } } - void initialResult.promise.then(finish, finish) - if (replayState && replayAttempt) - demand.parkedParticipant = this.trackTruncateReplayParticipant( + + demand.acquisitionState = `active` + if (!this.subsetDemands.includes(demand)) { + this.releaseAcquisition(acquisition) + return { demand, result, started: true } + } + + if (replayState && replayAttempt) { + this.trackTruncateReplayParticipant( replayState, replayAttempt, demand, - initialResult.promise, - requestOptions.signal, + result, + acquisition.options.signal, ) - return { demand, result: initialResult.promise, started: false } - } - const acquisition = this.createSubsetAcquisitionRecord(demand) - demand.acquisition = acquisition - const syncRunGeneration = this.collection._sync.getSyncRunGeneration() - // Reentrant release must see the exact acquisition before adapter work - // starts. A load that throws is a rejection: it keeps its owner. - this.subsetDemands.push(demand) - const result = this.collection._sync.loadSubset(acquisition.options) - - if (!this.isSyncRunGenerationCurrent(syncRunGeneration)) { - const demandIndex = this.subsetDemands.indexOf(demand) - if (demandIndex !== -1) this.subsetDemands.splice(demandIndex, 1) - cancelAcquisition(acquisition) - return { demand, result, started: true } - } - - demand.acquisitionState = `active` - if (!this.subsetDemands.includes(demand)) { - this.releaseAcquisition(acquisition) + } return { demand, result, started: true } + } finally { + // Adapter and status callbacks can reenter before returning a promise. + // Setup holds the replacement until that tentative work is registered. + if (joinedReplay && joinedAttempt) { + joinedReplay.pendingSetups-- + joinedAttempt.setupComplete = true + this.checkTruncateReplayComplete(joinedReplay) + } } - - if (replayState && replayAttempt) { - this.trackTruncateReplayParticipant( - replayState, - replayAttempt, - demand, - result, - acquisition.options.signal, - ) - } - return { demand, result, started: true } } /** Re-check ownership after adapter and event callbacks that may reenter. */ diff --git a/packages/db/src/query/live/ARCHITECTURE.md b/packages/db/src/query/live/ARCHITECTURE.md index a3a07c71d7..45c0412e4b 100644 --- a/packages/db/src/query/live/ARCHITECTURE.md +++ b/packages/db/src/query/live/ARCHITECTURE.md @@ -1535,7 +1535,9 @@ still has replay work, so demand that any of them starts meanwhile joins its own open replay. While a replay of any of them still holds publication, a lazy demand of any of them that fails after initial-query readiness fails its own subscription's replacement too. This includes a subscription that held no -demand at truncate. Settling a sibling's replay or retiring its failed demand +demand at truncate. Such a subscription joins the barrier when its first +acquisition starts, before adapter code can reenter; its current work still +holds publication after the sibling settles. Settling a sibling's replay or retiring its failed demand cannot publish the window without this demand's rows. It holds until every demand load that joins it loads, a later replay of the source succeeds, or the subscription's last demand retires. The query retries the failed demand diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index 0089c5a7be..99f405f429 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -598,8 +598,8 @@ export class CollectionConfigBuilder< // it, and ordered work must not wait for a load that already ended. // Output held for this demand publishes now, unless a replay of this // source still holds publication: the failure then holds this - // subscription's publication too, so retiring the other replay's - // failed demand cannot publish without this demand's rows. + // subscription's publication too. Neither sibling settlement nor + // retirement of its failed demand may publish without these rows. demand.settled = true if ( this.collectionSources.some( diff --git a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts index 7ad2fdfd13..00a7aafafc 100644 --- a/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-replay-refinement-oracle.test.ts @@ -179,7 +179,14 @@ type OrderedCommand = // while the next stays pending. `newest` settles the most recently held // load instead of the oldest, so a later load can settle before an earlier // one, as independent provider requests may. - | { type: `settle`; ok: boolean; keepHolding?: boolean; newest?: boolean } + | { + type: `settle` + ok: boolean + keepHolding?: boolean + newest?: boolean + // Settle a new sibling demand, or drain other adapter work while it stays held. + target?: `new-sibling` | `other` + } | { type: `truncate`; armAfterStart: boolean } | { type: `unloadThrows` } | { type: `window`; offset: 0 | 1; limit: 1 | 2 | 3 } @@ -322,6 +329,8 @@ type OrderedReach = { siblingSuccessfulCoverage: number // Owed request-error comparisons after another subscription asked for its keys. siblingRequestErrorChecks: number + lateSiblingSettlements: number + retryWhileSiblingLoads: number newSiblingReplayFailures: number newSiblingReplayRetirements: number newSiblingReplayRecoveries: number @@ -477,10 +486,12 @@ async function checkOrderedReplay( // This subscription held no acquisition at truncate, then started demand // during its sibling replay. Measured only from adapter receipts. newSiblingReplay?: boolean + markerTruncate?: number keys?: Set ordered?: boolean } const loads: Array = [] + const receiptOptions = new Map() // Retention model. Each source with an open or failed replay has a // barrier. `pending` holds the loads that still hold its publication: the // loads started while it is up, and work an older replay started. `own` @@ -499,8 +510,6 @@ async function checkOrderedReplay( ambiguous: boolean pending: Map own: Set - // Acquisition receipts held at truncate, used only for declared reach. - subscriptionsAtTruncate: Set } const barriers = new Map() // Every barrier this history raised, in raise order. @@ -530,18 +539,6 @@ async function checkOrderedReplay( ambiguous: [...barriers.values()].some(({ ambiguous }) => ambiguous), pending: new Map(barriers.get(source)?.pending), own: new Set(), - subscriptionsAtTruncate: new Set( - [...acquired].flatMap(([options, count]) => - count > 0 && - loads.some( - (load) => - load.source === source && - load.subscription === options.subscription, - ) - ? [options.subscription] - : [], - ), - ), } barriers.set(source, barrier) raisedBarriers.push(barrier) @@ -568,7 +565,6 @@ async function checkOrderedReplay( barrier.own.add(load) return true } - let replayScopeViolation: string | undefined const settleParticipant = ( source: BarrierSource, load: Load, @@ -590,13 +586,6 @@ async function checkOrderedReplay( barrier.own.has(load) && !options.signal?.aborted ) { - if ( - load.newSiblingReplay && - [...barrier.pending.values()].some( - (pending) => pending.subscription !== options.subscription, - ) - ) - reach.newSiblingReplayFailures++ barrier.failures.set(load, options) barrier.history.push(load) } @@ -605,12 +594,9 @@ async function checkOrderedReplay( // failure no longer fails the replay (ARCHITECTURE.md law 17: a failure // counts for a key no later held load loaded). if (ok && barrier?.pending.has(load) && !options.signal?.aborted) { - // Calibration of the replay account: a successful sibling can supply the - // same source keys without deciding another subscription's verdict. - // Preserve those failure identities independently of the mutation below. - const siblingFailures = [...barrier.failures.keys()].filter( - (failed) => failed.subscription !== load.subscription, - ) + // Sibling success cannot decide another subscription's failure. + // The public request-error comparison below enforces that distinction; + // model self-consistency is not production calibration evidence. for (const failed of barrier.failures.keys()) { if ( failed.subscription !== load.subscription && @@ -630,8 +616,6 @@ async function checkOrderedReplay( barrier.failures.delete(failed) } } - if (siblingFailures.some((failed) => !barrier.failures.has(failed))) - replayScopeViolation ??= `successful sibling removed another subscription's replay failure` } barrier?.pending.delete(load) // A replay completes when its last participant settles, so demand that @@ -649,8 +633,6 @@ async function checkOrderedReplay( for (const owned of [barrier.pending, barrier.failures]) { for (const [load, held] of owned) { if (held === options) { - if (owned === barrier.failures && load.newSiblingReplay) - reach.newSiblingReplayRetirements++ owned.delete(load) } } @@ -667,6 +649,46 @@ async function checkOrderedReplay( const oneSubscription = (_source: BarrierSource) => !scenario.peer const acquired = new Map() const held: Array<(ok: boolean) => void> = [] + const heldReceipts = new Map<(ok: boolean) => void, Load>() + const holdLoad = (load: Load) => + new Promise((resolve) => { + held.push(resolve) + heldReceipts.set(resolve, load) + }) + // Driver receipts at the actual truncate, independent of model membership. + // These select legal settlement orders and measure reach only. + let markerSubscriptionsAtTruncate = new Set< + LoadSubsetOptions[`subscription`] + >() + const hasPendingReplaySibling = ( + subscription: LoadSubsetOptions[`subscription`], + ) => + loads.some( + (load) => + load.source === `markers` && + load.outcome === `pending` && + load.subscription !== subscription && + markerSubscriptionsAtTruncate.has(load.subscription) && + !receiptOptions.get(load)?.signal?.aborted && + (acquired.get(receiptOptions.get(load)!) ?? 0) > 0, + ) + const isNewSiblingReplayLoad = (options: LoadSubsetOptions) => + markerTruncates > 0 && + !markerSubscriptionsAtTruncate.has(options.subscription) && + ((markerCommitting && + [...markerSubscriptionsAtTruncate].some( + (subscription) => subscription !== options.subscription, + )) || + hasPendingReplaySibling(options.subscription)) + const noteNewSiblingFailure = (load: Load, options: LoadSubsetOptions) => { + if ( + load.newSiblingReplay && + !options.signal?.aborted && + hasPendingReplaySibling(options.subscription) + ) + reach.newSiblingReplayFailures++ + } + let observingHistory = false let holding = false let truncates = 0 let throwNextUnload = false @@ -974,6 +996,8 @@ async function checkOrderedReplay( siblingReplayFailureComparisons: 0, siblingSuccessfulCoverage: 0, siblingRequestErrorChecks: 0, + lateSiblingSettlements: 0, + retryWhileSiblingLoads: 0, newSiblingReplayFailures: 0, newSiblingReplayRetirements: 0, newSiblingReplayRecoveries: 0, @@ -1035,14 +1059,23 @@ async function checkOrderedReplay( source: `markers`, keys: requestedKeys(`markers`, options), } - load.newSiblingReplay = - barriers.get(`markers`) !== undefined && - !barriers - .get(`markers`)! - .subscriptionsAtTruncate.has(options.subscription) && - [...(barriers.get(`markers`)?.pending.values() ?? [])].some( - (pending) => pending.subscription !== options.subscription, + load.markerTruncate = markerTruncates + load.newSiblingReplay = isNewSiblingReplayLoad(options) + receiptOptions.set(load, options) + // This retry premise is measured when the real adapter receives + // another load of a failed demand while its sibling still loads. + if ( + load.newSiblingReplay && + loads.some( + (earlier) => + earlier.subscription === load.subscription && + earlier.outcome === `failed` && + [...(earlier.keys ?? [])].every((key) => + load.keys?.has(key), + ), ) + ) + reach.retryWhileSiblingLoads++ loads.push(load) reach.markerLoads++ // The throw happens while the subscription registers the @@ -1064,6 +1097,7 @@ async function checkOrderedReplay( const recovery = belongsToRecovery(options, markerTruncates, markerCommitting) || (participant && oneSubscription(`markers`)) + noteNewSiblingFailure(load, options) settleParticipant(`markers`, load, options, false) // A load that threw acquired nothing, so the adapter never sees // its demand retire, and any graph run can retire it. The @@ -1092,14 +1126,23 @@ async function checkOrderedReplay( source: `markers`, keys: requestedKeys(`markers`, options), } - load.newSiblingReplay = - barriers.get(`markers`) !== undefined && - !barriers - .get(`markers`)! - .subscriptionsAtTruncate.has(options.subscription) && - [...(barriers.get(`markers`)?.pending.values() ?? [])].some( - (pending) => pending.subscription !== options.subscription, + load.markerTruncate = markerTruncates + load.newSiblingReplay = isNewSiblingReplayLoad(options) + receiptOptions.set(load, options) + // This retry premise is measured when the real adapter receives + // another load of a failed demand while its sibling still loads. + if ( + load.newSiblingReplay && + loads.some( + (earlier) => + earlier.subscription === load.subscription && + earlier.outcome === `failed` && + [...(earlier.keys ?? [])].every((key) => + load.keys?.has(key), + ), ) + ) + reach.retryWhileSiblingLoads++ loads.push(load) reach.markerLoads++ const participant = joinBarrier(`markers`, load, options) @@ -1119,14 +1162,13 @@ async function checkOrderedReplay( const newDemandDuringReplay = !reload && markerCommitting && isDemand(options) if (holding) { - const ok = await new Promise((resolve) => - held.push(resolve), - ) + const ok = await holdLoad(load) if (!ok) { // The source's replay had already failed and still holds // publication when this load fails. const barrierFailed = (barriers.get(`markers`)?.failures.size ?? 0) > 0 + noteNewSiblingFailure(load, options) const replayLoading = settleParticipant( `markers`, load, @@ -1224,6 +1266,20 @@ async function checkOrderedReplay( })() }, unloadSubset: (options) => { + // During commands, same-truncate unload retires a failed demand; + // final cleanup and a newer replay's acquisition renewal do not + // supply this reach witness. + if ( + observingHistory && + loads.some( + (load) => + receiptOptions.get(load) === options && + load.newSiblingReplay && + load.outcome === `failed` && + load.markerTruncate === markerTruncates, + ) + ) + reach.newSiblingReplayRetirements++ unloaded.add(options) retireParticipants(`markers`, options) const count = acquired.get(options) ?? 0 @@ -1262,9 +1318,7 @@ async function checkOrderedReplay( const participant = joinBarrier(`rows`, load, options) belongsToRecovery(options, truncates, truncating) if (holding) { - const ok = await new Promise((resolve) => - held.push(resolve), - ) + const ok = await holdLoad(load) if (!ok) { const replayLoading = settleParticipant( `rows`, @@ -1600,6 +1654,7 @@ async function checkOrderedReplay( await flushPromises() const next = held.shift() if (!next) return + heldReceipts.delete(next) next(true) } throw new Error(`held loads did not drain`) @@ -1624,6 +1679,19 @@ async function checkOrderedReplay( // An eager marker source rewrites its rows; an on-demand one replays the // demand it retains. const truncateMarkers = async () => { + markerSubscriptionsAtTruncate = new Set( + [...acquired].flatMap(([options, count]) => + count > 0 && + !options.signal?.aborted && + loads.some( + (load) => + load.source === `markers` && + load.subscription === options.subscription, + ) + ? [options.subscription] + : [], + ), + ) installedMarkers.clear() raiseBarrier(`markers`) markerSync.begin() @@ -1652,6 +1720,7 @@ async function checkOrderedReplay( try { if (!scenario.historyDuringPreload) await preload let sawRequestError = false + observingHistory = true for (const command of history) { try { switch (command.type) { @@ -1684,7 +1753,40 @@ async function checkOrderedReplay( break case `settle`: { holding = command.keepHolding === true && holding - ;(command.newest === true ? held.pop() : held.shift())?.(command.ok) + const candidates = held.filter((resolve) => { + const receipt = heldReceipts.get(resolve)! + const activeNewSibling = + receipt.source === `markers` && + !markerSubscriptionsAtTruncate.has(receipt.subscription) && + !receiptOptions.get(receipt)?.signal?.aborted + return ( + command.target === undefined || + (command.target === `new-sibling` + ? activeNewSibling + : !activeNewSibling) + ) + }) + const selected = command.newest ? candidates.at(-1) : candidates[0] + if (selected) { + held.splice(held.indexOf(selected), 1) + const receipt = heldReceipts.get(selected)! + if ( + receipt.source === `markers` && + !markerSubscriptionsAtTruncate.has(receipt.subscription) && + !loads.some( + (other) => + other !== receipt && + other.source === `markers` && + other.subscription !== receipt.subscription && + other.outcome === `pending` && + !receiptOptions.get(other)?.signal?.aborted, + ) && + !receiptOptions.get(receipt)?.signal?.aborted + ) + reach.lateSiblingSettlements++ + heldReceipts.delete(selected) + selected(command.ok) + } break } case `truncate`: @@ -1821,6 +1923,7 @@ async function checkOrderedReplay( sawRequestError = true } if (sawRequestError) reach.requestErrors++ + observingHistory = false holding = false throwNextUnload = false // The drain is the checkpoint, where recovery loads succeed, so an @@ -1841,8 +1944,6 @@ async function checkOrderedReplay( if (moveOrderViolation) { throw new Error(`newest move wins: ${moveOrderViolation}`) } - if (replayScopeViolation) - throw new Error(`replay account scope: ${replayScopeViolation}`) if (moveErrorViolation) { throw new Error(`replay error identity: ${moveErrorViolation}`) } @@ -1899,6 +2000,7 @@ async function checkOrderedReplay( } } } finally { + observingHistory = false holding = false for (const resolve of held.splice(0)) resolve(true) for (const subscription of subscriptions) subscription.unsubscribe() @@ -2644,6 +2746,99 @@ describe(`Ordered windows across truncate replays`, () => { declared.assertReached() }, 120_000) + // Law 9 keeps late include demand private after its sibling settles. + // A waiting move distinguishes publication retention from replay verdicts: + // Atomic window publication allows rejection despite a later retry. + it(`keeps publication across sibling and include settlement orders`, async () => { + const states = [`lateSiblingSettlements`, `retryWhileSiblingLoads`] as const + const declared = createOracleReach( + `replay.sibling-settlement-order`, + states, + ) + const failures: Array = [] + for (const direction of [`asc`, `desc`] as const) { + for (const limit of [1, 2, 3] as const) { + for (const includeFirst of [false, true]) { + for (const ok of [false, true]) { + for (const waitingMove of [false, true]) { + declared.beginHistory() + try { + const receipt = await checkOrderedReplay( + [ + { type: `hold` }, + { type: `truncateMarkers` }, + { type: `move`, id: 6, rank: direction === `asc` ? 60 : 1 }, + { + type: `settle`, + ok: true, + keepHolding: true, + target: `other`, + }, + ...(waitingMove + ? [{ type: `window` as const, offset: 0 as const, limit }] + : []), + ...(includeFirst + ? [ + { + type: `settle` as const, + ok, + keepHolding: true, + target: `new-sibling` as const, + }, + ] + : []), + // Twelve settlements exceed the other receipts in this + // six-row domain; selected new demand stays held throughout. + ...Array.from({ length: 12 }, () => ({ + type: `settle` as const, + ok: true, + keepHolding: true, + target: `other` as const, + })), + ...(!includeFirst + ? [ + { + type: `settle` as const, + ok, + keepHolding: true, + target: `new-sibling` as const, + }, + ] + : []), + ], + { + ...ordinaryOrderedScenario, + direction, + offset: 1, + limit, + joined: `none`, + includes: true, + markerMode: `on-demand`, + }, + ) + for (const state of states) + if (receipt[state] > 0) declared.reach(state) + } catch (error) { + failures.push(error) + } + } + } + } + } + } + console.info( + `replay.sibling-settlement-order`, + Object.fromEntries(declared.counts()), + ) + if (failures.length) + throw new AggregateError( + failures, + `${failures.length} settlement histories violated shared publication`, + { cause: failures[0] }, + ) + declared.assertReached() + }, 120_000) + // This finite history grammar varies which source replays, the order of // two load failures, join form, direction and window width. Join plus include // gives sibling subscriptions; the root replay gives same-subscription @@ -2793,6 +2988,8 @@ describe(`Ordered windows across truncate replays`, () => { siblingReplayFailureComparisons: 0, siblingSuccessfulCoverage: 0, siblingRequestErrorChecks: 0, + lateSiblingSettlements: 0, + retryWhileSiblingLoads: 0, newSiblingReplayFailures: 0, newSiblingReplayRetirements: 0, newSiblingReplayRecoveries: 0, @@ -2838,6 +3035,9 @@ describe(`Ordered windows across truncate replays`, () => { `siblingReplayFailureComparisons`, `siblingSuccessfulCoverage`, `siblingRequestErrorChecks`, + // The constructive settlement-order grammar declares these premises. + `lateSiblingSettlements`, + `retryWhileSiblingLoads`, // The constructive new-sibling grammar declares these rare premises. `newSiblingReplayFailures`, `newSiblingReplayRetirements`, From 688049658d0c12ca4f2cc5f7cdeac57a487be516 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 2 Nov 2026 08:20:33 -0700 Subject: [PATCH 4/6] test: prove final overwrite and deferred replay coverage --- docs/contributing/oracle-coverage.md | 25 +++ .../2026-10-30-release-replay-retention.md | 164 +++++++++++++++ .../tests/multi-tab-electric-oracle.test.ts | 103 +++++++-- ...-query-deferred-acquisition-oracle.test.ts | 195 +++++++++++++++++- 4 files changed, 463 insertions(+), 24 deletions(-) diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index dc8aee8e72..fabcf83c61 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -6372,3 +6372,28 @@ Final-restart and final-commit omissions fail the real SQLite/coordinator public comparisons. These are Node-host controlled-stream receiving histories, not real provider or browser-process claims. The core and browser path limits are unchanged. See the [nine-item review amendment](oracle-reviews/2026-10-30-release-replay-retention.md#first-demand-admission-and-terminal-grammar-review-2026-11-02). + + +### Final-catch-up provenance and deferred sibling resumption (2026-11-02) + +The multi-tab Electric owner now separates durable/server divergence from an +actual older same-key commit overwriting a newer peer commit. The old fixed +campaign measured 58 divergence repairs, of which 57 lacked that overwrite +premise and one had it (81 initiated/80 accepted histories). Earlier final +"lagging rewrite" counts are historical divergence evidence. A fourth terminal +regime constructs the actual overwrite. Original arbitrary/reset/stop regimes, +24-step prefix, 8–24-step arbitrary tail, models, comparisons, known gaps, +80-run budgets and minima remain. New lagging suffix length is 12/15 for two/three +tabs. Final commit omission fails the ordinary durable/public comparison. + +The deferred-acquisition owner adds 32 finite histories over a real same-source +LEFT self-join, crossing subscriber/preload, one/two rows, literal/Promise +acquisitions, and settlement order. Every history requires an actual deferred +joined demand, checks zero acquisitions before its own reader, and checks +readiness and independently projected rows after successful coverage. Existing +production passes; retaining the parked participant makes all 32 fail readiness. +No production cycle was confirmed. Both full owners pass 540 tests. Browser +fixed/random actual overwrite reaches are repair 25/18 and callback 25/25. +These boundaries retain their controlled core and Node-host SQLite/Electric +limits. No HTTP, framework, browser-process, or arbitrary-shape closure follows. +See the [versioned amendment](oracle-reviews/2026-10-30-release-replay-retention.md#final-catch-up-provenance-and-deferred-sibling-resumption-review-2026-11-02). diff --git a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md index e81ab11d51..1a296f8c95 100644 --- a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md +++ b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md @@ -443,3 +443,167 @@ No raw review item is deferred. The remaining limitations are the owners' explicit controlled and finite boundaries, not a known reachable violation of the repaired admission law. Final pushed CI and repair integration with actual main remain separate readiness obligations. No merge authorization follows. + + +## Final-catch-up provenance and deferred sibling resumption review (2026-11-02) + +This amendment evaluates CodeRabbit review **5738453581** on repair head +`b73f8e865bdda471df9dc77cc471e927143a41a9`. Its callback port starts from +`0b53825d4c19446fa3f346c1575820498804da9c`. The incremental run +`9a64a058-a898-4ac9-8d4c-4ecc87fdea30` selected eight files and skipped five +similar files. Selection does not establish eight fresh reviews. This is an +author self-review of the resulting oracle changes. No product law or shipped +source changes in this amendment. + +### Law, experiment, and inference + +**R1: eventual convergence needs an honest reach premise.** Browser owner Law 2 +and the coverage map promise the server head after every open stream catches +up. Durable/server divergence alone does not show that an older tab overwrote +a newer peer commit. Instrumenting the old grammar on unchanged production +recorded 58 final divergence repairs: 57 had no older-over-newer peer overwrite, +and one had it. The fixed campaign accepted 80 histories and initiated 81, +because the existing known-gap classifier discarded one history. These are +diagnostic counts, not a production RED or a new product defect. + +The owner now retains that old observation as `durable divergence repaired +during final catch-up`. Separate provenance records the delivered message's +server-log version, source handle, and committing tab. A differing older +same-key change must overwrite a newer peer commit. The marker remains until +the key reaches server head; a full snapshot resets this provenance. Final +rewrite reach counts only after the existing real durable/public comparison +passes. Provenance is independent model-side instrumentation over delivered +messages, not a production coordinator classification. + +The terminal dimension adds `lagging` beside the existing arbitrary, reset, +and stop endings. It starts a fresh handle, catches up every tab, establishes +the chosen key, catches up every tab again, makes two updates, lets a peer +commit both, and lets the selected tab commit only the older update. Final +catch-up must repair that actual overwrite. The new suffix has `6 + 3*tabCount` +steps (12 or 15). Reset/stop still add `4 + 2*tabCount` (8 or 10), and arbitrary +adds zero. The original 24-step stop prefix, arbitrary 8–24-step tail, model +predictions, comparisons, known gaps, 80-run budgets, and minima remain. + +The omission mutant suppresses only the final up-to-date commit while actual +older-over-newer overwrite evidence remains. It fails the ordinary durable and +public row comparison at `after final delivery to tab A` (v2 instead of v3). +It is an intended assertion failure, not a setup error or timeout. The captured +seed is `952051961`, shrink path `4:0:0:1:1:1:1:1:1:1:1:1:1:1:1:1`. +The shared direct replay interface reproduces that same mismatch. The oracle +file is restored after each temporary calibration. + +**R2: resumed deferred demand must progress after applied coverage.** Authority +is ARCHITECTURE law 15 (no acquisition before an own subscriber/preload), law 9 +(shared replay publication), and Initial demand (readiness after coverage). +The alleged completion cycle is refuted on its actual path. Existing +`startTruncateReplayDemand` removes `parkedParticipant` and decrements its +attempt before starting acquisition. The initial-result waiter can then wait +for completion without participating in the completion it awaits. + +The deferred-acquisition owner adds a finite 32-history grammar over a real +same-source LEFT self-join. It crosses subscriber/preload, one/two rows, +literal/held-Promise root and sibling results, and oldest/newest settlement. +The query starts its sync run without a reader and asserts zero acquisitions. +Its first source acquisition writes synchronously. This creates demand in the +second subscription while that subscription still defers acquisition and a +sibling replacement holds publication. A request-entry spy measures the actual +deferred-to-joined transition, with reach one required in every history. + +Held provider promises keep initial-query readiness pending. After controlled +successes, the query must be ready, any preload must fulfill, and public rows +must equal the independent self-join projection of input IDs. The driver allows +no later truncate, abort, window move, or extra preload to rescue the history. +All 32 pass against unchanged production on both branches. Leaving the parked +participant installed makes all 32 fail the readiness assertion with `loading` +instead of `ready`. Production bytes are restored afterward. The review's +major production repair is unnecessary; its durable value is this coverage. + +### Executed GREEN and calibration evidence + +On each branch, the complete deferred-acquisition owner passes **540 tests**, +including the new 32 histories. Both browser campaigns accept **80 histories** +per branch. The two existing final-delivery replays pass on both branches. +Repair ran its missing second replay separately after the first selector chose +only three tests. Callback ran all four tests in one invocation. + +| Branch/campaign | Final stopped-source restart | Durable divergence repair | Actual older-peer overwrite repair | Reset-abandoned final recovery | +| --- | ---: | ---: | ---: | ---: | +| Repair fixed | 36 | 63 | 25 | 16 | +| Repair random | 34 | 60 | 18 | 25 | +| Callback fixed | 36 | 63 | 25 | 16 | +| Callback random | 38 | 66 | 25 | 18 | + +Earlier records describing 46, 52, or 58-style final counts as lagging rewrites +are historical divergence counters. They do not establish older-over-newer +peer overwrite coverage. This correction preserves their original records and +does not retroactively strengthen that evidence. + +Focused core/browser TypeScript, ESLint, formatting, whitespace, and +`pnpm lint:changed` validate the follow-up. Callback's first browser TypeScript +attempt lacked local module paths. Temporary links to already installed cached +modules repair setup and are removed afterward. No dependency install, manifest +or lockfile change occurs. Local Vitest is cached 3.2.4; CI uses 4.1.12. Full +repository tests and builds are not run for this oracle-only amendment. + +### Grammar controls and oracle self-review + +Reconstruction: the old arbitrary/reset/stop regimes remain reconstructible. +The lagging suffix reconstructs an actual older commit over a newer peer and +leaves it for the final comparison. The deferred grammar reconstructs the +reviewed parked demand during real sibling resumption. Ablation: remove either +update, the peer commit, or the selected older commit and the overwrite premise +is lost. Remove the deferred start or synchronous first delivery and the parked +sibling path is lost. Promise axes distinguish pending readiness from literal +completion; settlement order distinguishes the two active acquisition gates. + +Range: browser histories retain two/three tabs, keys 1–3, every existing stop +kind and tail action, plus all four endings. Deferred histories bound one/two +rows and two source subscriptions with both public initiating paths. Zero rows +cannot create the reported keyed sibling demand and is outside this lane; the +existing broad owner retains empty/no-provider regimes. Exclusion: the suffix +commits an update only after establishing its key; it never fabricates an +update to a missing row. The deferred lane never starts provider work before +the first reader and never settles an acquisition that was not issued. + +| Requirement | Outcome for this amendment | +| --- | --- | +| ORC-001 | Existing Law 2 and ARCHITECTURE laws 9/10/15 authorize results. Limits below remain explicit. | +| ORC-002 | Input-ID self-join recomputation and server-message commit provenance use no production semantic classifier. | +| ORC-003 | Contract, model, histories, driver, and checkpoints remain visible beside executable code. | +| ORC-004 | Reconstruction, ablation, range, and exclusion appear above. | +| ORC-005 | Real core live-query Collection and real SQLite/coordinator paths compare readiness, rows, and durable/public state. Supplied messages contain the source facts needed at each cut. | +| ORC-006 | Parked-participant and actual-final-commit omissions fail their intended assertions; neither is a setup failure. | +| ORC-007 | Browser fixed/random campaigns use the same property, grammar, recorder, comparison, and 80-run budget. Direct seed/path mutant replay reproduces the row mismatch. The 32-case finite enumeration needs no random campaign. | +| ORC-008 | Provenance is reach instrumentation, not a change to expected product state. Equal row divergence can have different commit origins, so provenance distinguishes which coverage claim applies. | +| ORC-009 | Comments map provenance to delivered message position/tab/handle and distinguish demand from acquisition and completion. | +| ORC-010 | Existing browser failure/cleanup handling remains. Calibration scripts restore exact bytes in finally. The deferred driver restores its spy, settles held gates, and cleans Collections. | +| ORC-011 | No shared semantic-fault hypothesis needs a second formulation. The independent self-join result and explicit commit provenance address the two named hypotheses. | +| ORC-012 | This versioned record binds the reviewed parent heads, evidence, dispositions, and every applicable requirement. | +| ORC-013 | Divergence without an older overwrite is the neighboring false-positive case. Literal versus held results and both settlement orders distinguish readiness boundaries. | +| ORC-014 | Claims stop at core controlled acquisition and Node SQLite/coordinator controlled Electric messages. No HTTP provider, framework, or browser-process handoff claim follows. | +| ORC-015 | Browser reach requires each retained minimum plus honest divergence and actual overwrite states. Every deferred history requires one actual deferred joined demand. | +| ORC-016 | Histories share independent projection/comparison or the existing generated driver. No standalone fixed expected rows or type assertions enter an oracle. | + +### Loss audit, reviewer assessment, and readiness + +The two inline findings are fully accounted: **one fixed-now** (R1 test +integrity), **one refuted** (R2 production cycle), with its missing receiving +history added. No deferral or open in-scope finding remains from this review. +The raw audit also retains summary/risk duplicates, five related-issue links, +the informational docstring metric (0%, one function, four files, four +unsupported), run metadata, and generator/autofix suggestions. No boilerplate +command runs. Related links do not establish issue closure. + +The reviewer earns a qualified hire recommendation for oracle scrutiny. R1 +correctly distinguishes a classifier's premise from ordinary convergence. R2 +identifies a useful lifecycle intersection but overlooks the existing participant +removal, so its major severity and proposed production fix lack support. Use +runtime witnesses before relying on this reviewer for lifecycle merge decisions. + +Evidence covers finite core resumption and controlled-stream Node persistence +histories at named cuts. It does not prove arbitrary provider failures, +framework schedules, browser processes, every join shape, or the existing +excluded/pinned known gaps. No new product behavior or shipped source changes. +Current-head CI after the oracle commits and actual-main repair integration +remain separate readiness requirements. No merge or publication is authorized +by this review. diff --git a/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts b/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts index 1dc3261503..11c87c0109 100644 --- a/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts +++ b/packages/browser-db-sqlite-persistence/tests/multi-tab-electric-oracle.test.ts @@ -975,7 +975,8 @@ describe(`multi-tab Electric over one persisted database`, () => { // of an initially absent key restore the original rows before the tail. // The prefix adds 24 steps to the unchanged 8–24-step arbitrary tail. An // ending dimension either leaves that final state untouched or adds a - // constructive reset/stop suffix of 4 + 2*tabCount steps (8 or 10). Removing + // reset/stop suffix of 4 + 2*tabCount steps (8 or 10), or a lagging-commit + // suffix of 6 + 3*tabCount steps (12 or 15). Removing // the open delivery, stop, or peer commit loses the law's premise; recovering // before the peer commit would test a different boundary. Existing pinned // histories still use the same driver without this prefix. @@ -989,6 +990,7 @@ describe(`multi-tab Electric over one persisted database`, () => { `arbitrary` as const, `reset` as const, `stop` as const, + `lagging` as const, ), stopOrder: fc.shuffledSubarray([0, 1, 2, 3], { minLength: 4, @@ -1026,8 +1028,13 @@ describe(`multi-tab Electric over one persisted database`, () => { // without committing it. Reset abandons that reservation, so final // delivery judges its failure before recovery (Laws 2/5). Stop // instead leaves final catch-up to restart the stopped source. - // The source tab, key and two/three-tab host vary with this grammar. - // Removing advance or reset loses that premise; catching up here + // The lagging ending first establishes the key, then a peer commits + // two updates and the selected tab commits only the older one. It + // leaves that actual overwrite for final catch-up to repair. A + // server change alone establishes only durable/server divergence. + // Tab roles, key and two/three-tab host vary with this grammar. + // Removing either update or the partial commit loses the overwrite; + // removing advance or reset loses that premise; catching up here // would move the failure out of the final observation cut. ...(ending === `arbitrary` ? [] @@ -1040,11 +1047,26 @@ describe(`multi-tab Electric over one persisted database`, () => { { kind: `catch up`, tab: index, pick: 0 }, ], ).flat(), - { kind: `server change` as const, key, pick: 0 }, - { kind: `advance` as const, tab, pick: 0 }, - ending === `reset` - ? { kind: `peer reset` as const } - : { kind: `stop` as const, tab, pick: stopOrder[0]! }, + ...(ending === `lagging` + ? [ + { kind: `server change` as const, key, pick: 1 }, + ...Array.from({ length: tabCount }, (_, index) => ({ + kind: `catch up` as const, + tab: index, + pick: 0, + })), + { kind: `server change` as const, key, pick: 1 }, + { kind: `server change` as const, key, pick: 1 }, + { kind: `catch up` as const, tab: peer, pick: 0 }, + { kind: `catch up` as const, tab, pick: 1 }, + ] + : [ + { kind: `server change` as const, key, pick: 0 }, + { kind: `advance` as const, tab, pick: 0 }, + ending === `reset` + ? { kind: `peer reset` as const } + : { kind: `stop` as const, tab, pick: stopOrder[0]! }, + ]), ]), ], } @@ -1162,13 +1184,52 @@ describe(`multi-tab Electric over one persisted database`, () => { let serverVersion = 0 generatedReach.beginHistory() + // Reach provenance comes from the server messages the driver delivered, + // not merely a difference between durable rows and the current server. + // A newer peer commit must be overwritten by an older change to that key. + // This model-only provenance is a delivered message's server position and + // committing tab. Snapshot rows start at version zero of their handle; + // change messages retain their exact log version, including deletions. + const durableOrigins = new Map< + number, + { + tab: TabModel + handle: number | undefined + version: number + } + >() + const laggingOverwrites = new Set() const commit = (model: TabModel) => { - if (model.truncates) durable.clear() + if (model.truncates) { + durable.clear() + durableOrigins.clear() + laggingOverwrites.clear() + } for (const message of model.open ?? []) { if (!(`key` in message)) continue const row = message.value + const version = + log.changes.find((change) => change.message === message)?.version ?? 0 + const previous = durableOrigins.get(row.id) + const value = + message.headers.operation === `delete` ? undefined : row.label + if ( + previous && + previous.tab !== model && + previous.handle === model.handle && + previous.version > version && + durable.get(row.id) !== value + ) + laggingOverwrites.add(row.id) if (message.headers.operation === `delete`) durable.delete(row.id) else durable.set(row.id, row.label) + durableOrigins.set(row.id, { + tab: model, + handle: model.handle, + version, + }) + if (durable.get(row.id) === log.rows.get(row.id)) + laggingOverwrites.delete(row.id) } model.open = undefined model.truncates = false @@ -1543,20 +1604,30 @@ describe(`multi-tab Electric over one persisted database`, () => { generatedReach.reach(`stopped source restarted during final catch-up`) await recover(index, `restart`) } - const laggingBeforeFinal = + const overwrittenBeforeFinal = [...laggingOverwrites] + const divergentBeforeFinal = rowsOf(durable).some(({ id, label }) => log.rows.get(id) !== label) || rowsOf(log.rows).some(({ id, label }) => durable.get(id) !== label) await deliver(index, `all`, true) + // A reset-abandoned transaction fails only when delivery tries to + // commit it. Judge Law 5 at that cut before the restart establishes + // Law 2's successful catch-up premise. + await check(`after final delivery to tab ${tabs[index]!.tab}`) if ( - laggingBeforeFinal && + divergentBeforeFinal && !models[index]!.failedByReset && JSON.stringify(rowsOf(durable)) === JSON.stringify(rowsOf(log.rows)) + ) + generatedReach.reach( + `durable divergence repaired during final catch-up`, + ) + if ( + overwrittenBeforeFinal.some( + (id) => + !laggingOverwrites.has(id) && durable.get(id) === log.rows.get(id), + ) ) generatedReach.reach(`lagging commit rewritten during final catch-up`) - // A reset-abandoned transaction fails only when delivery tries to - // commit it. Judge Law 5 at that cut before the restart establishes - // Law 2's successful catch-up premise. - await check(`after final delivery to tab ${tabs[index]!.tab}`) if (models[index]!.failedByReset) { await recover(index, `restart`) await deliver(index, `all`, true) @@ -1741,6 +1812,7 @@ describe(`multi-tab Electric over one persisted database`, () => { type GeneratedReach = | `stopped source restarted during final catch-up` + | `durable divergence repaired during final catch-up` | `lagging commit rewritten during final catch-up` | `must-refetch delivered` | `peer commit during an open reserved transaction` @@ -1765,6 +1837,7 @@ describe(`multi-tab Electric over one persisted database`, () => { const createGeneratedReach = () => createOracleReach(`browser.multi-tab-reserved-history`, [ `stopped source restarted during final catch-up`, + `durable divergence repaired during final catch-up`, `lagging commit rewritten during final catch-up`, `must-refetch delivered`, `peer commit during an open reserved transaction`, diff --git a/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts b/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts index 9b29526b7a..b549f74352 100644 --- a/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts +++ b/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts @@ -41,18 +41,25 @@ * Its admission releases that deferral. A failed sync start remains an error * in that run; releasing the old render handle does not implicitly retry it. * - * Limits: source Collections load synchronously, so every checkpoint is - * settled. The grammar crosses seven source states, two query shapes, and one or + * Limits: the broad grammar uses synchronous source loads and settled + * checkpoints. It crosses seven source states, two query shapes, and one or * two live-query levels, with one peer consumer and one truncate before the - * first subscriber. It does not generate source failure, asynchronous loads, - * or several live-query consumers; the subscription lifecycle oracle owns - * those once acquisition has resumed. Pinned blocks cover pooled views, + * first subscriber. The separate sibling-resumption grammar below uses + * controlled asynchronous loads. The broad grammar does not generate source + * failure, asynchronous loads, or several live-query consumers. The + * subscription lifecycle oracle owns those once acquisition has resumed. Pinned blocks cover pooled views, * `DbClient` stream preloads, a subscriber that outlives cleanup, and reads * that wait for readiness. */ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { fc } from '@fast-check/vitest' -import { createCollection } from '../src' +import { + BTreeIndex, + createCollection, + createLiveQueryCollection, + eq, +} from '../src' +import { CollectionSubscription } from '../src/collection/subscription.js' import { resumeDeferredLiveQueryCollections } from '../src/live-query-options.js' import { Query } from '../src/query/builder/index.js' import { createPooledLiveQuery } from '../src/query/pooled-live-query.js' @@ -435,8 +442,13 @@ describe(`live-query deferred acquisition`, () => { */ // The pooled entry point takes the internal builder, as its own oracle does. declareOracle({ - check: [runAcquisitionHistory, runPooledHistory], - model: [initialModel, applyModel, expectedReplacementRun], + check: [runAcquisitionHistory, runPooledHistory, runDeferredSiblingHistory], + model: [ + initialModel, + applyModel, + expectedReplacementRun, + deferredSiblingRows, + ], }) async function runPooledHistory( @@ -747,3 +759,168 @@ describe(`render sync-start handoff error provenance`, () => { }) } }) + +/** + * A first subscriber resumes source subscriptions in registration order. The + * first source load can synchronously deliver rows that create a lazy demand + * in another, still-deferred subscription to that same source. Law 15 keeps + * that demand detached until its own resumption; laws 9 and 10 require the + * completed query once every resumed load has applied. A sibling's publication + * and the initial-query waiter must not wait for one another in a cycle. + * + * This finite grammar crosses subscriber/preload, one/two rows, and literal or + * held Promise results for each acquisition, and both settlement orders. + * It uses the real self-join builder, without a supplied publication-control + * callback. Request instrumentation records the + * actual deferred-to-joined transition; it supplies no expected row values. + * The independent self-join law below retains each input row exactly once. + * No provider or framework scheduling promise follows from this core path. + */ +type DeferredSiblingHistory = { + reader: `subscribe` | `preload` + rowCount: 1 | 2 + rootAsync: boolean + siblingAsync: boolean + newestFirst: boolean +} +const deferredSiblingHistories: Array = ( + [`subscribe`, `preload`] as const +).flatMap((reader) => + ([1, 2] as const).flatMap((rowCount) => + [false, true].flatMap((rootAsync) => + [false, true].flatMap((siblingAsync) => + [false, true].map((newestFirst) => ({ + reader, + rowCount, + rootAsync, + siblingAsync, + newestFirst, + })), + ), + ), + ), +) +function deferredSiblingRows(rows: ReadonlyArray<{ id: string }>) { + return rows.map(({ id }) => ({ id, peer: id })) +} +async function runDeferredSiblingHistory(history: DeferredSiblingHistory) { + const rows = Array.from({ length: history.rowCount }, (_, i) => ({ + id: `row-${i}`, + })) + const gates: Array<() => void> = [] + let loads = 0 + let wrote = false + const source = createCollection<{ id: string }>({ + getKey: (row) => row.id, + syncMode: `on-demand`, + autoIndex: `eager`, + defaultIndexType: BTreeIndex, + gcTime: 0, + sync: { + sync: ({ begin, write, commit, markReady }) => { + markReady() + return { + loadSubset: () => { + loads++ + if (!wrote) { + wrote = true + begin() + for (const row of rows) + write({ type: `insert`, value: { ...row } }) + commit() + } + if (!(loads === 1 ? history.rootAsync : history.siblingAsync)) + return true + return new Promise((resolve) => gates.push(resolve)) + }, + } + }, + }, + }) + const query = createLiveQueryCollection({ + query: (q) => + q + .from({ left: source }) + .leftJoin({ right: source }, ({ left, right }) => eq(left.id, right.id)) + .select(({ left, right }) => ({ id: left.id, peer: right.id })), + gcTime: 0, + }) + const reach = createOracleReach(`deferred-sibling`, [ + `deferred joined demand`, + ]) + reach.beginHistory() + const original = CollectionSubscription.prototype.requestSnapshot + const spy = vi + .spyOn(CollectionSubscription.prototype, `requestSnapshot`) + .mockImplementation(function (this: CollectionSubscription, ...args) { + const deferred = this.isDeferringAcquisition() + const isPending = () => this.hasPendingTruncateReplacement + const before = isPending() + const result = original.apply(this, args) + if (deferred && !before && isPending()) + reach.reach(`deferred joined demand`) + return result + }) + let subscription: ReturnType | undefined + let preloadSettled = false + let preloadFailure: unknown + try { + query.startSyncImmediate() + await settle() + expect(loads, `deferred query starts no acquisition`).toBe(0) + if (history.reader === `subscribe`) + subscription = query.subscribeChanges(() => {}) + else + void query.preload().then( + () => { + preloadSettled = true + }, + (error: unknown) => { + preloadFailure = error + }, + ) + // The provider promises are semantic gates. Drain every issued gate, + // including any continuation produced by their settlement. No truncate, + // abort, window move or extra preload is allowed to repair this history. + await settle() + if (history.rootAsync || history.siblingAsync) { + expect(query.status, `held acquisition gates initial readiness`).toBe( + `loading`, + ) + expect(preloadSettled).toBe(false) + } + for (let turn = 0; turn < 12; turn++) { + const resolve = history.newestFirst ? gates.pop() : gates.shift() + resolve?.() + await settle() + } + console.info( + `deferred-sibling reach`, + history, + Object.fromEntries(reach.counts()), + ) + reach.assertReached() + expect(loads, `each source subscription acquires once`).toBe(2) + expect(query.status, `resumed sibling replay must finish`).toBe(`ready`) + if (history.reader === `preload`) { + expect(preloadFailure).toBeUndefined() + expect(preloadSettled).toBe(true) + } + expect( + query.toArray + .map(({ id, peer }) => ({ id, peer })) + .sort((a, b) => a.id.localeCompare(b.id)), + ).toEqual(deferredSiblingRows(rows)) + } finally { + spy.mockRestore() + for (const resolve of gates.splice(0)) resolve() + subscription?.unsubscribe() + await query.cleanup() + await source.cleanup() + } +} +describe(`deferred demand joining a sibling replay`, () => { + for (const history of deferredSiblingHistories) + it(`finishes after resumption: ${JSON.stringify(history)}`, () => + runDeferredSiblingHistory(history)) +}) From 5bd58aad4c32eef410946f209047d3199b294ce0 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 2 Nov 2026 08:37:06 -0700 Subject: [PATCH 5/6] docs: clarify controlled replay evidence scope --- .../oracle-reviews/2026-10-30-release-replay-retention.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md index 1a296f8c95..31e727e1b2 100644 --- a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md +++ b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md @@ -580,7 +580,7 @@ the first reader and never settles an acquisition that was not issued. | ORC-011 | No shared semantic-fault hypothesis needs a second formulation. The independent self-join result and explicit commit provenance address the two named hypotheses. | | ORC-012 | This versioned record binds the reviewed parent heads, evidence, dispositions, and every applicable requirement. | | ORC-013 | Divergence without an older overwrite is the neighboring false-positive case. Literal versus held results and both settlement orders distinguish readiness boundaries. | -| ORC-014 | Claims stop at core controlled acquisition and Node SQLite/coordinator controlled Electric messages. No HTTP provider, framework, or browser-process handoff claim follows. | +| ORC-014 | Claims cover controlled core acquisition. The SQLite/coordinator tests run in Node with controlled Electric messages. No HTTP provider, framework, or browser-process handoff claim follows. | | ORC-015 | Browser reach requires each retained minimum plus honest divergence and actual overwrite states. Every deferred history requires one actual deferred joined demand. | | ORC-016 | Histories share independent projection/comparison or the existing generated driver. No standalone fixed expected rows or type assertions enter an oracle. | From 5616198b63dee7347b8e06cda50b13812cc571cd Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 2 Nov 2026 09:19:24 -0700 Subject: [PATCH 6/6] refactor(db): reduce replay acquisition code weight --- .../2026-10-30-release-replay-retention.md | 52 +++++++ packages/db/src/collection/subscription.ts | 144 ++++++++---------- 2 files changed, 114 insertions(+), 82 deletions(-) diff --git a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md index 31e727e1b2..542a0bff23 100644 --- a/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md +++ b/docs/contributing/oracle-reviews/2026-10-30-release-replay-retention.md @@ -607,3 +607,55 @@ excluded/pinned known gaps. No new product behavior or shipped source changes. Current-head CI after the oracle commits and actual-main repair integration remain separate readiness requirements. No merge or publication is authorized by this review. + +## Compressed-size refactor on current main (2026-11-02) + +The maintainer requested negative compressed bundle size for this repair. Both +branches now include main `035a5260ef994ce3aabed77a3f09b35c71482a9a` through normal +merge commits. Their merged trees exactly matched the preceding clean scratch +integration trees before this refactor. + +The subscription constructs its acquisition record once, including for detached +demand. It captures the replay attempt once, uses the existing attempt finisher +for its setup hold, and shares the existing uncaught-error reporter. Replay +failure collection retains the same membership checks and Map insertion order +without intermediate filter/map arrays. Acquisition admission has one result +return after its cancellation, release, or participation branch. The replay +factory reads the current sync-run generation itself instead of receiving the +same value from every caller. + +This is an internal refactor under architecture laws 9, 10, 15 and 16. The +accepted behavior, oracle models, history grammars, comparisons, reach minima +and run budgets do not change. The original production RED and hostile-mutant +evidence above remains historical. This refactor introduces no new behavior or +bug-class closure claim, so it uses existing applicable owners without changing +their expectations. + +On each branch, one final-source run passes 1,787 tests in 11 files: replay +refinement, its fixed neighbors, deferred acquisition, lifecycle history and +publication, subscription replay, lifecycle ownership and cells, adapter +ownership, preload indexing and index reuse. Focused TypeScript, ESLint, +formatting, whitespace and `pnpm lint:changed` pass. Each branch uses one Vitest +worker, for two total. Local Vitest is cached 3.2.4, while CI uses 4.1.12. No +install, lockfile edit, full repository build or full `pnpm test` run occurs. + +The existing `pnpm size:compare --base 035a5260ef994ce3aabed77a3f09b35c71482a9a +--exact-base --packages db --files` command measures the targeted production +build and app scenarios. The repair's package gzip changes from 109,825 to +109,824 bytes, a decrease of 1 byte. Its package Brotli decreases by 114 bytes. +Its typical app gzip changes from 96,410 to 96,399 bytes, a decrease of 11 bytes. +Every measured app scenario decreases with gzip. Typical app Brotli increases +by 14 bytes, and some other Brotli scenarios increase. The package-only `--dev` +measure, which includes development code as CI does, decreases by 2 gzip bytes +and 19 Brotli bytes. These local measures do not replace the remote size check. + +The callback port's package gzip decreases by 59 bytes and package Brotli by +38 bytes against the same main. Its typical app gzip decreases by 75 bytes. +Typical app Brotli increases by 26 bytes. Its earlier size figures retain their +original historical bindings. + +The finite core histories and current-main indexing checks supply the bounds +of this verification. Existing provider, framework, browser-process and deeper +history limits remain. Browser and Solid receiving evidence above retains its +historical source binding. Final-head CI and external review coverage remain +separate from these local results. diff --git a/packages/db/src/collection/subscription.ts b/packages/db/src/collection/subscription.ts index 552c2f96c5..aaf45c0cf1 100644 --- a/packages/db/src/collection/subscription.ts +++ b/packages/db/src/collection/subscription.ts @@ -447,7 +447,7 @@ export class CollectionSubscription const joins = open?.syncRunGeneration === syncRunGeneration const replayState = joins ? open - : this.createTruncateReplayState(syncRunGeneration, () => { + : this.createTruncateReplayState(() => { const currentRows = this.collection.currentStateAsChanges({ optimizedOnly: false, }) @@ -510,7 +510,6 @@ export class CollectionSubscription } else { // Every overlapping attempt shares one publication baseline and buffer. replayState = this.createTruncateReplayState( - this.collection._sync.getSyncRunGeneration(), () => new Map(this.publishedRows), ) this.truncateReplayState = replayState @@ -561,11 +560,9 @@ export class CollectionSubscription attempt: TruncateReplayAttempt, demand: SubsetDemand, ): void { - const isCurrentAttempt = () => - this.truncateReplayState === replayState && - replayState.currentAttempt === attempt const isCurrent = () => - isCurrentAttempt() && + this.truncateReplayState === replayState && + replayState.currentAttempt === attempt && this.isSyncRunGenerationCurrent(replayState.syncRunGeneration) && this.isDemandActive(demand) const waiting = demand.initialResult @@ -590,7 +587,7 @@ export class CollectionSubscription if (hadPreviousAcquisition) this.releaseAcquisition(previous) if (!isCurrent() || demand.requestOptions.signal?.aborted) return - const next = this.createSubsetAcquisitionRecord(demand) + const next = this.createSubsetAcquisitionRecord(demand.requestOptions) next.attempt = attempt demand.acquisition = next demand.acquisitionState = `starting` @@ -637,9 +634,7 @@ export class CollectionSubscription // Replay settlement runs from a Promise callback, so throwing here would // create an unobserved derived rejection. Surface subscriber errors like // other async collection events instead. - queueMicrotask(() => { - throw error - }) + reportUncaught(error) } } @@ -714,18 +709,19 @@ export class CollectionSubscription if (replayState.pendingSetups > 0 || replayState.pending.size > 0) return // A held replacement that loaded decides the demands it replaces. - const activeFailures = [...replayState.failures] - .filter( - ([demand]) => - this.subsetDemands.includes(demand) && - !this.subsetDemands.some( - (replacement) => - replacement.replaces?.includes(demand) && - !replayState.failures.has(replacement) && - !replayState.unloaded.has(replacement), - ), + const activeFailures: Array = [] + for (const [demand, error] of replayState.failures) { + if ( + this.subsetDemands.includes(demand) && + !this.subsetDemands.some( + (replacement) => + replacement.replaces?.includes(demand) && + !replayState.failures.has(replacement) && + !replayState.unloaded.has(replacement), + ) ) - .map(([, error]) => error) + activeFailures.push(error) + } try { if (activeFailures.length > 0) { this.abandonTruncateReplay(replayState, activeFailures) @@ -880,10 +876,7 @@ export class CollectionSubscription */ failReplacement(error: Error): void { if (this.truncateReplayState) return - const replayState = this.createTruncateReplayState( - this.collection._sync.getSyncRunGeneration(), - () => new Map(), - ) + const replayState = this.createTruncateReplayState(() => new Map()) replayState.pendingSetups = 0 replayState.failed = true replayState.completion.reject(error) @@ -933,11 +926,10 @@ export class CollectionSubscription /** One replay state; only direct subscribers buffer a private replacement. */ private createTruncateReplayState( - syncRunGeneration: number, privateRows: () => Map, ): TruncateReplayState { return { - syncRunGeneration, + syncRunGeneration: this.collection._sync.getSyncRunGeneration(), publicationState: { loadedInitialState: this.loadedInitialState, snapshotSent: this.snapshotSent, @@ -1125,18 +1117,19 @@ export class CollectionSubscription /** Create the record for a fresh, abortable acquisition attempt. */ private createSubsetAcquisitionRecord( - demand: SubsetDemand, + requestOptions: LoadSubsetOptions, + detached = false, ): SubsetAcquisitionRecord { - // Eager sync never passes subset options to an adapter, so an abortable - // acquisition would only allocate a controller and an AbortError. - if (this.collection.config.syncMode !== `on-demand`) { + // Detached demand has no physical work. Eager sync never passes subset + // options to an adapter. Neither needs an abortable acquisition yet. + if (detached || this.collection.config.syncMode !== `on-demand`) { return { - options: demand.requestOptions, + options: requestOptions, syncRunGeneration: this.collection._sync.getSyncRunGeneration(), } } const abortController = new AbortController() - const requestSignal = demand.requestOptions.signal + const requestSignal = requestOptions.signal let removeRequestAbortListener: (() => void) | undefined if (requestSignal?.aborted) { @@ -1150,7 +1143,7 @@ export class CollectionSubscription return { options: { - ...demand.requestOptions, + ...requestOptions, signal: abortController.signal, }, syncRunGeneration: this.collection._sync.getSyncRunGeneration(), @@ -1193,28 +1186,14 @@ export class CollectionSubscription !this.truncateReplayState && this.options.truncateReplayPublication?.siblingReplays() ) { - joinedReplay = this.createTruncateReplayState( - this.collection._sync.getSyncRunGeneration(), - () => new Map(), - ) + joinedReplay = this.createTruncateReplayState(() => new Map()) this.truncateReplayState = joinedReplay this.truncateReplacementPending = true } - const joinedAttempt = joinedReplay?.currentAttempt + const replayState = this.truncateReplayState + const replayAttempt = replayState?.currentAttempt try { - const demand: SubsetDemand = { - requestOptions, - acquisition: { - options: requestOptions, - syncRunGeneration: this.collection._sync.getSyncRunGeneration(), - }, - acquisitionState: `starting`, - } - // A demand that starts during an open replay joins it, parked or not (law - // 9: a new demand joins the open replay). - const replayState = this.truncateReplayState - const replayAttempt = replayState?.currentAttempt - if ( + const detached = this.collection.status === `cleaned-up` || // A deferring subscription starts no acquisition attempt until it resumes. (this.defersAcquisition && @@ -1225,9 +1204,21 @@ export class CollectionSubscription (isFailedStatus(this.collection.status) || (this.collection.status !== `idle` && this.collection._sync.syncLoadSubsetFn === null))) - ) { - demand.acquisitionState = `detached` - this.subsetDemands.push(demand) + const acquisition = this.createSubsetAcquisitionRecord( + requestOptions, + detached, + ) + const demand: SubsetDemand = { + requestOptions, + acquisition, + acquisitionState: detached ? `detached` : `starting`, + } + // A demand that starts during an open replay joins it, parked or not (law + // 9: a new demand joins the open replay). + // Reentrant release must see the exact acquisition before adapter work + // starts. A load that throws is a rejection: it keeps its owner. + this.subsetDemands.push(demand) + if (detached) { const initialResult = createDeferred() demand.initialResult = initialResult const abort = () => @@ -1248,44 +1239,33 @@ export class CollectionSubscription ) return { demand, result: initialResult.promise, started: false } } - const acquisition = this.createSubsetAcquisitionRecord(demand) - demand.acquisition = acquisition - const syncRunGeneration = this.collection._sync.getSyncRunGeneration() - // Reentrant release must see the exact acquisition before adapter work - // starts. A load that throws is a rejection: it keeps its owner. - this.subsetDemands.push(demand) + const syncRunGeneration = acquisition.syncRunGeneration const result = this.collection._sync.loadSubset(acquisition.options) if (!this.isSyncRunGenerationCurrent(syncRunGeneration)) { const demandIndex = this.subsetDemands.indexOf(demand) if (demandIndex !== -1) this.subsetDemands.splice(demandIndex, 1) cancelAcquisition(acquisition) - return { demand, result, started: true } - } - - demand.acquisitionState = `active` - if (!this.subsetDemands.includes(demand)) { - this.releaseAcquisition(acquisition) - return { demand, result, started: true } - } - - if (replayState && replayAttempt) { - this.trackTruncateReplayParticipant( - replayState, - replayAttempt, - demand, - result, - acquisition.options.signal, - ) + } else { + demand.acquisitionState = `active` + if (!this.subsetDemands.includes(demand)) { + this.releaseAcquisition(acquisition) + } else if (replayState && replayAttempt) { + this.trackTruncateReplayParticipant( + replayState, + replayAttempt, + demand, + result, + acquisition.options.signal, + ) + } } return { demand, result, started: true } } finally { // Adapter and status callbacks can reenter before returning a promise. // Setup holds the replacement until that tentative work is registered. - if (joinedReplay && joinedAttempt) { - joinedReplay.pendingSetups-- - joinedAttempt.setupComplete = true - this.checkTruncateReplayComplete(joinedReplay) + if (joinedReplay && replayAttempt) { + this.startTruncateReplayAttempt(joinedReplay, replayAttempt, []) } } }