From 8c8e1517abbdc4532ea512bbe4bc95dac010509a Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 9 Nov 2026 18:54:02 -0700 Subject: [PATCH 01/24] fix(query-db-collection): reconcile persisted owners before eager startup Await the existing persisted baseline before applying the first complete eager Query response. Preserve other owners and avoid a second full row scan. Extend the shared ownership grammar and real SQLite/OPFS receivers; preserve oracle failures through cleanup and correct direct-write reference scheduling. Law: complete Query results replace their own persisted ownership, while peer owners survive (query-collection.md, Direct Writes and Query Sync). Complete initialData/cache responses materialize under the same contract. The existing reload oracle permits one full row read per eager tab/run. Reach: each controlled/Node host ran fixed and random 80-history campaigns, requiring both restore orders, empty results, peer ownership and all five origins. Response-first/restore-first counts were 59/21 and 63/17 (controlled), 59/21 and 67/13 (Node); all other required states reached. Chromium received eight histories. RED: the extended ownership oracle on unchanged main failed original omission comparisons. Before the baseline-reuse repair, the expanded controlled/Node run failed 20 tests and Chromium reported eight duplicate-read violations. See the versioned issue-2367 review record for commands, recorder correction and counts. GREEN: four Query test files passed 552 tests, cleanup helper passed three, Chromium passed eight histories, and typecheck/lint/format/diff checks passed. Corrected direct-write oracle passed 45 cases on both final source and main. Mutant: original omission behavior failed 18 tests; redundant-scan repair failed 20; nonempty-only repair failed eight. Direct seed 2367/path 0:0:0:0:0 replay failed the scan mutant and passed the repair at the work comparison. Limits: three string keys, numeric values, complete eager startup, one persistence instance and named cuts. No on-demand, multi-tab writer or failed-I/O claim. Registry 403 required uncommitted local overrides for Query Core 5.106.0 and fake-indexeddb 6.2.7; exact locked Query Core 5.107.1 remains untested locally. --- docs/contributing/oracle-coverage.md | 29 ++ .../oracle-reviews/2026-11-09-issue-2367.md | 474 ++++++++++++++++++ ...uery-startup-ownership-oracle.opfs.spec.ts | 26 + .../e2e/query-startup-ownership.opfs.html | 11 + .../e2e/query-startup-ownership.opfs.ts | 70 +++ .../playwright.opfs.config.ts | 1 + .../tests/reload-class-oracle.ts | 16 +- packages/db/tests/oracle-cleanup.ts | 37 ++ packages/db/tests/oracle-config.ts | 1 + packages/db/tests/utils-cleanup.test.ts | 66 +++ packages/db/tests/utils.ts | 35 +- packages/query-db-collection/src/query.ts | 61 ++- .../tests/ownership-lifecycle.oracle.test.ts | 260 +++++++++- ...rsisted-direct-write-window.oracle.test.ts | 277 +++++++--- .../persisted-startup-ownership-oracle.ts | 454 +++++++++++++++++ 15 files changed, 1683 insertions(+), 135 deletions(-) create mode 100644 docs/contributing/oracle-reviews/2026-11-09-issue-2367.md create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.ts create mode 100644 packages/db/tests/oracle-cleanup.ts create mode 100644 packages/db/tests/utils-cleanup.test.ts create mode 100644 packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index e64751f415..75502013d7 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -2377,6 +2377,35 @@ remaining design and coverage questions, including valid Temporal kinds without public order and mixed-kind query order. Earlier PR-branch counterexamples do not by themselves establish an `origin/main` failure. +The Query DB ownership oracle's +[persisted startup dimension](../../packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts) +crosses a complete eager Query response with a held persisted restore (#2367). +Its independent set-replacement model removes only that Query's old ownership +and preserves another persisted owner. The generated grammar crosses nonempty +prior subsets of three keys, any next subset (weighted toward empty), changed +values on overlapping keys, a peer owner, and response-before/after-restore. +It also crosses complete initialData and cached responses with fetching disabled +or pending. Controlled storage and real Node SQLite each run fixed and random +80-history campaigns with separate declared reach for both schedules, empty +results, peer owners, and all five response origins. +The [Chromium OPFS receiver](../../packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts) +runs eight histories through the shared driver and comparison: the reported +race, an empty answer, shared ownership, a restore-first control, and each +cached/initialData origin. The checkpoints compare public and durable rows and +exact ownership after preload, before refetch can repair storage, then inside +and after refetch fulfillment and after a local-only reopen. Startup also checks +the reload oracle's existing one-full-row-read law. The repair awaits the +wrapper's baseline publication instead of scanning the same rows again. +Original omission behavior, a nonempty-only repair, and a redundant-scan repair +fail these comparisons. Captured mismatches and primary runtime errors survive +secondary cleanup errors. The neighboring persisted direct-write oracle records +source commit order independently of Query cache availability and compares the +matching nonpersisted schedule; Node SQLite receives all three held boundaries. +Limits: eager startup, three string keys and numeric +values, one persistence instance; no on-demand window, concurrent tab writer, +arbitrary metadata namespace, or failed-I/O claim. Evidence is in the +[issue evaluation](oracle-reviews/2026-11-09-issue-2367.md). + The Query DB ownership oracle also owns the explicit mutation-handler refresh contract ([evidence](oracle-reviews/pr-2171-explicit-query-refresh.md)). Its 18-history table crosses insert, update, and delete with no confirmation, explicit refetch, and direct writes, with and without discarded diff --git a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md new file mode 100644 index 0000000000..f319958398 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md @@ -0,0 +1,474 @@ +# Persisted Query startup ownership: issue #2367 + +## Implementation state + +Evaluated [issue #2367](https://github.com/TanStack/db/issues/2367), reported by +`jackmccloy`, on 2026-11-09. This is an issue report, not a pull-request review. +Starting and current HEAD: +`ea3c920c1540486d0fb7410e089baa3a1583299c` (fresh `origin/main`). +The production repair and oracle extension are uncommitted on +`codex/evaluate-review-2367`. The repaired `query.ts` Git blob is +`97e8defd46d448c6ec3e19145d7490f3c5328074`. +This entry records that working-tree implementation; later evidence should be +appended with its new revision rather than replacing these results. + +The worktree is `.worktrees/codex-evaluate-review-2367`. Initial publication to +`origin/codex/evaluate-review-2367` succeeded. No repair commit or PR was pushed. + +## Law, experiment, and inference + +No product-law change needs a decision. A successful complete Query response +replaces that Query's ownership edges, including edges stored in persistence. +An omitted row disappears when no other owner retains it. Another owner's row +and ownership survive. Persistence must retain the resulting rows and ownership. + +Authority: [Direct Writes and Query Sync](../../collections/query-collection.md#direct-writes-and-query-sync), +especially successful fetch reconciliation and scope-local omission, together +with the [ownership lifecycle oracle](../../../packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts). +This does not grant a partial response global omission authority. + +The ownership oracle previously exercised persisted ownership, but did not +cross an ordinary eager first success with a held persisted restore. +The [reload oracle](../../../packages/db-sqlite-persistence-core/tests/reload-class-oracle.ts) +correctly covers startup work and ordering. Its eager sources supply local rows; +it does not judge a Query response's replacement of persisted ownership. +The failure was a missing history and receiving-path crossing, not evidence +that those narrower laws were wrong. + +The new [shared dimension](../../../packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts) +uses independent set replacement. It removes the old Query edges, adds the +response's edges, and retains rows with a peer owner. It copies no production +query cache, reconciliation branch, or startup state machine. + +Its grammar generates nonempty prior subsets of `a,b,c`, any next subset, +changed values on overlap, optional peer ownership, and either response/restore +order. Empty answers receive extra weight. Each host runs the same property for +40 histories with campaign seed `2367`, then 40 with a random campaign seed. +Four fixed histories also use the same model, production driver and comparison. + +The driver holds the actual first baseline row read. Query cache success while +that read remains held establishes the response-first premise. The restore-first +history holds the response until restored rows are visible. It compares: + +- Public rows and exact Query ownership after `preload()` fulfills. +- The same observations inside explicit `refetch()` fulfillment. +- Durable rows and ownership after refetch. +- Rows after cleanup and a local-only reopen. + +Ownership metadata is read through the Collection's internal metadata store and +the adapter snapshot; public row values use Collection reads. There is no claim +about every intermediate publication or about durable state before refetch. + +Controlled storage and real Node SQLite execute generated campaigns. A +[Chromium OPFS receiver](../../../packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts) +executes all four fixed histories through the actual worker and public Browser +persistence factory. It reuses the shared judgment without separate answers. + +Unchanged production failed these comparisons before any production edit. +The repair makes the first eager persisted result acquire its durable ownership +baseline, using existing reconciliation machinery. It also preserves ownership +reconstructed by a durable scan before metadata restore publishes. It adds no +new lifecycle state. + +## Evidence block + +- **Law:** Complete Query results replace their own persisted ownership; + other owners survive. Authority and observation cuts are stated above. +- **Reach:** Separate generated counters require response-before-restore, + restore-before-response, empty-result, and peer-owner reach for each host + and each campaign. The final normal package run measured the counts below. +- **RED:** On unchanged `ea3c920c1`, the extended ownership suite produced + **10 failed / 2 passed** through controlled storage and Node SQLite. Fast + histories failed startup, refetch, durable, and reopen comparisons; + restore-first controls passed. The real Chromium OPFS run completed four + histories, with **11 violations across three fast histories** and no + violation in the restore-first control. +- **GREEN:** The normal four-file package run passed **527 tests**. The final + Chromium OPFS receiver passed **one test containing four shared histories**. + Query package `tsc --noEmit` and `pnpm lint:changed` exited successfully. +- **Mutants:** Restoring retention-only baseline selection produced + **10 failed / 2 passed**. Restricting the repair to nonempty answers produced + **6 failed / 6 passed**. Reading owners only from not-yet-restored metadata + produced **3 failed / 9 passed**, exposing lost peer ownership. All were + comparison failures, not crashes, timeouts, or setup errors. All mutations + were restored afterward. +- **Limits:** Eager startup; three string keys; numeric row values; one + persistence instance; one revalidated Query plus an optional persisted peer + owner. No claim about arbitrary metadata namespaces, partial/on-demand + windows, concurrent tab writes, failed I/O, or every publication before + readiness. Chromium receives four fixed histories, not a generated campaign. + These bounds supplement the primary owner's other dimensions; they do not + waive its accepted laws. No reachable in-scope counterexample remains known. + +| Host / campaign | Response first | Restore first | Empty answer | Peer owner | +| --- | ---: | ---: | ---: | ---: | +| Controlled / fixed | 19 | 21 | 24 | 22 | +| Controlled / random, seed `123777923` | 20 | 20 | 23 | 23 | +| Node SQLite / fixed | 19 | 21 | 24 | 22 | +| Node SQLite / random, seed `1470328179` | 15 | 25 | 21 | 22 | + +Each host's four fixed histories separately reached response-first three times, +restore-first once, empty once, and peer ownership once. The Chromium receiver +completed the same four histories and checks their premises in the comparison. + +The original-behavior mutant shrank to +`{previous:1,next:0,peer:false,response:"before-restore"}`. Direct replay with +campaign seed `2367` and shrink path `1:0:0` failed after exactly one history at +all four omission comparisons. The same replay passed on repaired production. +Fixed campaigns and fixed histories stood aside during replay. + +Run from the repository root: + +```sh +pnpm --filter @tanstack/query-db-collection exec vitest run \ + --coverage.enabled=false --typecheck.enabled=false --disableConsoleIntercept \ + tests/ownership-lifecycle.oracle.test.ts \ + tests/persisted-direct-write-window.oracle.test.ts \ + tests/persisted-direct-write.test.ts tests/query.test.ts + +pnpm --filter @tanstack/browser-db-sqlite-persistence exec playwright test \ + --config playwright.opfs.config.ts query-startup-ownership-oracle.opfs.spec.ts + +TANSTACK_DB_ORACLE_PROPERTY=query.startup-ownership \ +TANSTACK_DB_ORACLE_SEED=2367 TANSTACK_DB_ORACLE_PATH=1:0:0 \ +pnpm --filter @tanstack/query-db-collection exec vitest run \ + --coverage.enabled=false --typecheck.enabled=false --disableConsoleIntercept \ + tests/ownership-lifecycle.oracle.test.ts \ + -t 'persisted startup ownership through controlled.*generated startup histories \(random\)' +``` + +The first pre-edit Node RED used a task-local Vitest configuration that aliases +workspace dependencies to source. The final normal package command also passed. +Initial fixture target/import errors, a missing Playwright browser revision, +and SQLite filenames exceeding the VFS path bound were setup failures; none +counts as RED. Short database names and system Chrome supplied the real-host +witness before production edits. Localhost sandbox restrictions required an +escalated browser command; the successful run has no outstanding approval block. + +## Lossless finding ledger + +The raw report has no comments or separate capped findings. These five IDs +preserve its distinct claims in source order. They share one governing law; +they are not five independent production defects. Severity is this evaluation's +assessment, since the reporter assigned none. + +| ID | Original claim and requested behavior | Verdict / action | Evidence and durable destination | +| --- | --- | --- | --- | +| F1 | Source startup precedes baseline restore. A fast first full answer sees no old ownership, then restore adds server-deleted `a`; the first full answer must remove it. | Confirmed, P1; `fixed-now`. | Response-first generated dimension; startup row/owner comparison fails on original and passes after repair. Shared dimension and this record preserve it. | +| F2 | Explicit `utils.refetch()` still leaves `a`, because its known Query rows now contain only the first answer's keys. | Confirmed, P1; `fixed-now` with F1. | Comparison inside refetch fulfillment rejects the retained row. Refetch cannot be credited from a later read alone. | +| F3 | The obsolete row remains in SQLite, not just the Collection; expected behavior removes it from the mirror. | Confirmed, P1; `fixed-now` with F1. | Durable row/owner comparison and local-only reopen fail on original and pass after repair. Durable cut is after refetch. | +| F4 | `reload-class-oracle.ts` describes source-before-baseline ordering but misses a fast full Query response crossing restore. | Confirmed coverage gap; `fixed-now`. | Reload grammar's eager local-row source cannot generate Query ownership replacement. Extend the ownership oracle with both schedules, Node SQLite and the OPFS receiver; retain the reload oracle's distinct scope. | +| F5 | Persisted `a` already carries `queryCollection.owners` for `["todos"]`, so the deletion's ownership information exists on disk. | Confirmed mechanism claim; `fixed-now` means the information is now used and protected by assertions. | Seeded durable metadata and exact owner comparisons demonstrate sufficient input. Peer ownership checks prevent deleting another scope's row. This is evidence for F1's repair, not another standalone bug. | + +The report's concrete trace is `a → b`, fast response, then explicit refetch. +The grammar reconstructs it as `previous=1,next=2,peer=false,response=before-restore`. +The issue's two published package-version sets and Linux/Node 24.22.1 environment +were reported evidence, not independently rerun here. Current-source failures +were independently demonstrated through Node SQLite and Chromium OPFS. + +Sibling **S1**, discovered by the extended oracle during the first repair: +a durable scan can reconstruct another owner's edge before public metadata +restore. Consulting only public metadata loses that edge while retaining its +row. **Confirmed, P1; fixed-now.** Reconciliation now uses the reconstructed +ownership graph. The peer-owner mutant fails exact startup/refetch/durable +ownership comparisons. This sibling is separate from the five raw items. + +## Oracle conformance self-review + +| Requirement | Outcome and evidence | +| --- | --- | +| ORC-001 | Law, authority, checkpoints and finite bounds appear in the shared dimension and this record. | +| ORC-002 | Expected output is independent set replacement. Production hashing identifies fixture metadata only; it does not compute the expected ownership judgment. | +| ORC-003 | Opening prose explains the law and limits; named model, grammar, production driver and checker remain distinguishable. | +| ORC-004 | Reconstruction: all four fixed histories belong to the generated grammar, including the reported trace. Ablation: timing exposes the race; old/new masks distinguish omission, insertion and overlap; changed values expose stale overlap; peer ownership rejects truncation. Range: old mask 1–7, next 0–7, both peer choices and schedules. Exclusion: no duplicate-key response, empty prior baseline or response before a fetch exists. Empty prior baseline is outside this persisted-obsolete-row dimension, not an invalid product input. | +| ORC-005 | Real QueryClient and persisted Collection entry points; held baseline and cache success establish timing. Exact rows/owners and reopen observations check the named cuts. Durable ownership supplies the concrete information needed for omission; no unresolved missing-input hypothesis remains at these cuts. | +| ORC-006 | Three mutants fail relevant comparisons, including nonempty-only repair and lost peer ownership. | +| ORC-007 | Fixed/random campaigns share generator, checker and 40-history run budget. Captured seed/path replay fails the original-behavior mutant and passes the repair directly. No `fc.commands` replay path applies. | +| ORC-008 | No stateful transition model introduced: expected output is a stateless recomputation from prior/next membership and peer ownership. | +| ORC-009 | Query/peer labels abstract persisted ownership edges; the peer is an owner not revalidated in this run. Response/restore names map to actual delivery boundaries. | +| ORC-010 | Observations copy row values and owner lists before cleanup. Runtime failures retain primary cause and aggregate secondary cleanup failures. Recorded reductions preserve the same four mismatch cuts. No injected cleanup-failure campaign was run. | +| ORC-011 | No concrete shared semantic-fault hypothesis needing another formulation was identified. The small set-replacement model is separate from production's cache and reconciliation machinery. Multiple hosts are receiving paths, not independent semantic formulations. | +| ORC-012 | This versioned record identifies exact HEAD and repaired source blob, records every requirement, and bounds closure by law × histories × paths × cuts. | +| ORC-013 | Fast `a → b`, fast `a → empty`, peer-owned omission, overlap with changed values, and restore-first histories distinguish the race and plausible wrong consequences. | +| ORC-014 | Actual Node SQLite and Chromium worker/OPFS receive the held-row-read/fast-response premise and reuse the shared comparison. | +| ORC-015 | Separate required generated reach per host/campaign; measured counts above. Fixed receiving histories have separate completion and premise checks. | +| ORC-016 | Fixed histories and browser receiving histories supply inputs to the shared law. They contain no separate expected row answers or concrete type assertions. | + +## Reviewer assessment + +**Hire recommendation: yes, based on this debugging sample.** Technical accuracy +and signal are high: the report identifies the timing boundary, provides a +small executable reproduction, checks refetch persistence, and shows durable +ownership metadata. Its expected behavior follows the existing contract. +The source explanation agrees with the independent failing observations. + +The proposed direction is sound, but the report does not supply an implemented +repair or establish bug-class closure. It misses the empty-answer and peer-owner +distinctions that reject plausible narrow repairs. Prioritizing a persistent +server-deleted row is appropriate. This evidence supports debugging ability; +one issue report cannot establish whole-PR review breadth or implementation quality. + +## Validation, installation, and accounting + +`pnpm lint:changed` succeeded. Explicit ESLint reported zero errors and four +warnings on existing shadowing/async locations. Query package typechecking +succeeded. `pnpm size:compare --packages query-db-collection --files` measured +package gzip **14,457 → 14,480 bytes (+23, 0.2%)**, brotli **+12 bytes**. +The ordinary app scenario measured gzip **95,826 → 95,859 (+33 bytes)**. + +The repair merges the retained/non-retained result enqueue branches and reuses +existing reconciliation instead of adding another lifecycle. Shipped bytes +still grow because ordinary eager startup needs durable baseline selection and +reconstructed peer ownership. The shorter retention-only predicate fails the +original-behavior mutant; the metadata-only ownership lookup fails the peer +mutant. Those simplifications lose required behavior. Test/support/configuration +adds 575 net lines separately from the shipped-byte measurement; its model, +campaigns and receiving drivers do not ship in the package. Documentation adds this record +and 20 coverage-map lines. + +Registry recovery is local and must remain uncommitted: `@tanstack/query-core` +**5.107.1 → 5.106.0** and `fake-indexeddb` **6.2.8 → 6.2.7**. Only blocked +dependencies were overridden; existing overrides were preserved. Scoped frozen +installation for affected packages succeeded. Full-workspace install also hit +an unrelated `@tailwindcss/postcss@4.3.5` 403; no override was added for it. +The size baseline used the same local recovery. Locked Query Core 5.107.1 was +unavailable, so validation against that exact dependency version remains unrun. + +The final audit returned to the complete raw issue body, including its metadata +postscript, reproduction, expected behavior, environment and version claims. +Raw items **5 = 5 fixed-now**; zero confirmed-open, stale, refuted, +accepted-design, deferred, design-decision or duplicate items. Sibling S1 is +separately fixed. Accounting and authorized repair are complete within the +declared boundary. No known in-scope counterexample or law decision is left open. +The coverage map links this dimension and its finite evidence. + +**PR readiness: not assessed.** This task evaluated an issue and left an +uncommitted repair. It did not create a PR or assess a pushed repair's CI. + + +## Prep-pr follow-up, 2026-11-09 + +This append-only entry supersedes the implementation and coverage limits in the +initial evaluation above. HEAD remains +`ea3c920c1540486d0fb7410e089baa3a1583299c`; the reviewed first repair was source +blob `97e8defd46d448c6ec3e19145d7490f3c5328074`. The final production source blob +is `48b4624f5b179dd5b5e380b01161d510827c318c`. No repair commit or PR exists at +this checkpoint. Dependency recovery and scratch evidence remain uncommitted. + +### Laws, experiments, and resulting repair + +The ownership law remains complete-response set replacement. The Query docs' +“Initial Data” and “Understanding the Data Stores” sections also give complete +initialData and cached Query responses materialization authority. Disabled +fetching or a pending physical fetch does not turn those responses into partial +results. The accepted reload model additionally requires one full row read per +eager tab/run. Its budget law predates this change; it is not a new performance +promise inferred from the faster implementation. + +The first repair satisfied membership but scanned durable rows again. Before +changing that production code, the shared ownership grammar gained five origins +(physical fetch, initialData with disabled/pending fetch, and cached data with +disabled/pending fetch), a pre-refetch durable comparison, and a startup work +observation. Controlled storage, Node SQLite, and Chromium OPFS all received +those premises before the production repair. A whitespace-sensitive SQL recorder +initially missed multiline scans. Correcting that recorder and rerunning the +unchanged first repair produced the decisive RED results below. The earlier +false-green recorder runs are not evidence of the work law. + +Production now awaits the existing persistence `hydrateBaseline()` capability +before diffing the first eager nonretained result. The wrapper publishes its +already-required full row read and key evidence; Query does not read them again. +Retained revalidations keep their existing scan path. The earlier graph lookup +workaround is removed: restored metadata supplies the peer edges before the +first result applies. No lifecycle state, row-loss waiver, or new product law +was added. The final simplification pass found no further worthwhile proposal. + +Captured public/durable cuts and work/premise metrics are checked before later +operations or cleanup. A browser-safe extraction of `withOracleCleanup` retains +the check's error as the aggregate cause and each cleanup error separately. +The nested reopened Collection uses the same helper. It returns callback values +without changing the existing error ordering or resource-release behavior. + +The surrounding direct-write oracle exposed a different problem in its own +premise. Query cache success had been treated as proof of a source commit. +During held startup hydration, the new implementation has zero source commits; +update/delete can legitimately reject before any row loads. The public docs' +“Individual Write Operations” rule refers to earlier sync commits, and explicitly +allows missing-row rejection before the first load. The revised driver records +source commits and schedules its nonpersisted reference in the same causal +order. It never derives expected row values from observed production rows. +An explicit held initial-result durable write preserves the committed-before-write +case, alongside the existing later source-write case. The controlled matrix has +24 cases; Node SQLite receives every write operation at all three boundaries, +plus the existing later-refetch ordering cases. This repairs the oracle's timing +assumption without changing its outcome/cache/row/durability comparisons. + +### Prep review ledger and loss audit + +The initial review supplied five separate items. The later rereview supplied a +sixth detail and three fixture findings, retained separately rather than disappearing into overlaps. + +| ID | Original claim and assessment | Disposition and durable result | +| --- | --- | --- | +| R1a, P2 | Cleanup can discard a captured membership mismatch before the caller compares it. Confirmed by the exact-module probe. | `fixed-now`: each captured cut is checked before cleanup, with primary mismatch and cleanup sentinel both retained. Shared dimension and cleanup helper own the repair. | +| R1b, P2 | Reopened preload failure can be overwritten by reopened cleanup failure. Independently confirmed. | `fixed-now`: nested failure-preserving cleanup retains the preload error as primary and the cleanup error as secondary. | +| R2, P2 | First repair adds redundant full row scans, violating the existing reload work law. Confirmed by real SQLite counters. | `fixed-now`: reuse baseline hydration; shared work model judges controlled, Node, and OPFS startup observations. | +| R3, P2 | Refetch precedes the first durable comparison and can hide broken startup durability. Confirmed sensitivity gap; the probe did not establish an independent product defect. | `fixed-now`: compare startup durability before refetch, with stale-durability checker calibration through the same model and cut comparison. | +| R4, P2 | Cold physical-response histories miss changed initialData/cache paths, including disabled and pending fetches. Confirmed grammar/receiving gap. | `fixed-now`: all five origins are generated with separate reach per host and campaign and received by actual OPFS. | +| R5, P2 | R1a's repair initially still left work/premise metrics unchecked until after cleanup. Confirmed by a duplicate-read plus cleanup-error probe. | `fixed-now`: metrics use the shared judgment immediately before durable observation. The resulting aggregate preserves both the work mismatch and cleanup sentinel. This is linked to R1a but retains its distinct missing observation. | +| R6, P2 | Startup cache availability does not establish an earlier source commit. Eight held-read histories record zero commits; eight held-initial-write histories record one. | `fixed-now`: causal-order reference scheduling and the initial-result-write holder correct the oracle's premise. This is the separately tracked T1 discovered while validating the first review. | +| R7, P2 | Real SQLite hydration bypasses the outer adapter method used by the new startup gate. Four receiving cases time out without reaching that gate. | `fixed-now`: wrap the adapter supplied by the actual hydration scope; all four reach the gate and the full 45-case oracle passes. No model or result comparison is relaxed. | +| R8, P2 | A buffered first source commit can also bypass an outer-only durable-write gate through the scoped adapter. The original-main control times out in four newly added initial-result-write cases. | `fixed-now`: share the held-write gate and acknowledgment recorder across outer and scoped adapters. All 45 cases pass on both repaired production and unchanged main. | + +Sibling S2 is the SQL recorder's multiline-statement false green; `fixed-now` +through whitespace normalization, followed by fresh pre-production RED on both +real hosts. Sibling T1 is the direct-write oracle's cache/commit conflation; +`fixed-now` through causal-order reference scheduling and the third held boundary. +The real SQLite receiver originally timed out because hydration supplies a scoped +adapter that bypasses an outer-method gate. Holding the scoped adapter's actual +read repairs the fixture; those timeouts are setup failures, never mutant kills. +Sibling S1's peer-ownership obligation from the initial evaluation remains +covered; its initial graph workaround is superseded by baseline publication. + +Both raw outputs were reread after repair, including the review's caveat that +R3 proved a coverage gap, its per-origin scan counts, and its retained-query +warning. There were zero original or final simplification proposals and no +unreviewed simplifier edits. Raw review items **9 = 9 fixed-now**; zero open, +stale, refuted, accepted-design, deferred, design-decision, or duplicate items. +Sibling recorder and timing defects are accounted for separately. No accepted +contract was weakened and no user law decision is required. + +### RED, GREEN, calibration, and replay evidence + +- **Pre-edit RED:** exact-module fidelity probes lost membership mismatches and + reopened primary errors; a durability probe passed after a later refetch + repaired stale storage. Real SQLite scan attribution on main versus the first + repair was 1→2 for fast fetch, 1→3 for disabled initialData, and 1→4 for pending + initialData/cache. After the recorder correction, the expanded controlled/Node + suite had **20 failures** on the unchanged first repair. Chromium completed + all eight histories and reported **eight work violations**, with startup row + reads `2,2,2,2,3,4,3,4` instead of one. +- **GREEN:** the expanded startup dimension passed all 21 tests (eight fixed + histories per host, four generated campaigns, and one durability calibration). + The final Chromium run passed all eight histories through real worker/OPFS. + All three ordinary cleanup-helper tests passed. Broader final validation is + recorded below. +- **Mutants:** restoring original omission behavior produced 18 failures and + two passes; restoring the redundant-scan repair produced 20 failures; limiting + the repair to nonempty results produced eight failures and 12 passes. These + are comparison failures, not timeouts or setup errors. The original evaluation's + peer-loss mutant evidence remains historical calibration of that distinction. +- **Failure fidelity:** exact transpiled-driver probes now reject stale durable + state before refetch, preserve a captured mismatch plus cleanup failure, and + preserve reopened preload plus cleanup errors. A separate work-metric probe + retains `startup row reads: expected 1, got 2` as the aggregate cause and the + cleanup sentinel as its second error. Aggregate nesting is permitted by ORC-010. +- **Direct replay:** property `query.startup-ownership`, campaign seed `2367`, + path `0:0:0:0:0` reconstructs `previous=1,next=0,peer=false,origin=initial-disabled, + response=before-restore`. The redundant-scan mutant fails at the work comparison + (`expected 1, got 3`); restored production passes the same direct replay. Normal + campaigns and fixed histories stand aside during replay. + +Commands are the four-file Query command and Chromium command above, with +`--maxWorkers=2` for Vitest 5. Replay uses the same environment variables above +with the new path and the controlled random-property test selector. Scratch raw +outputs, attribution probes, mutation logs, and ledgers remain local under +`.review-2367/`; the verdict-critical results are preserved in this record. + +### Updated oracle conformance + +| Requirement | Follow-up outcome | +| --- | --- | +| ORC-001 | Same ownership authority, plus documented complete cache/initialData materialization and existing reload work law. Finite bounds below. | +| ORC-002 | Independent set replacement and shared pure eager-read count; no production baseline classifier computes expected answers. Direct-write differential reference shares only the unwrapped Query contract, retaining its stated shared-code limit. | +| ORC-003 | Shared dimension explains origins, timing, work recorder, model and each comparison cut. Direct-write prose distinguishes cache availability from source commits and explains reference scheduling. | +| ORC-004 | All eight fixed histories reconstruct within the grammar. Origins expose distinct response delivery paths; disabled/pending choices distinguish physical fetching from complete cache materialization. Timing, old/new masks, changed overlap and peer ownership retain their prior ablation roles. Domains remain old mask 1–7 and new mask 0–7. Cache/initialData are already available before restore; impossible after-restore schedules for those origins are excluded. | +| ORC-005 | Public Collection and actual persistence entry points; immediate startup public/durable comparisons precede refetch. Work is counted before the observer's own durable read. Complete cached responses and durable ownership supply the concrete input needed at the cut. No unresolved missing-input hypothesis was found. | +| ORC-006 | Original omission, redundant scans and nonempty-only repair fail the intended comparisons. Stale startup durability is rejected by the same cut checker. Setup timeouts are classified separately. | +| ORC-007 | Each host runs fixed and random campaigns with identical grammar, recorder, judgment and 80-history run budget. Direct seed/path replay is RED on the scan mutant and GREEN after restore. No commands replayPath applies. No budget was lowered or removed. | +| ORC-008 | Ownership remains stateless set recomputation; no new reference-machine state. The differential schedule retains source commit order because it changes lawful validation before versus after initial loading. | +| ORC-009 | Query/peer edges and response/restore mappings remain explicit. Origin separates response availability from physical fetch state; direct-write source commits are distinct from cache availability. | +| ORC-010 | Cuts and metrics checked before cleanup; executed probes retain each original violation/runtime failure and secondary cleanup diagnostic. Shared helper attempts every cleanup and returns the successful check value. | +| ORC-011 | No new concrete semantic fault shared by the set model and production was identified. Differential direct-write reference keeps its existing limit for faults shared by Query's direct-write code. | +| ORC-012 | This append-only record ties final evidence to exact HEAD/source blob and preserves earlier superseded findings. No known reachable in-scope violation remains at the claimed cuts after the final checks. | +| ORC-013 | Empty/nonempty, peer/no-peer, overlap/disjoint, response-first/restore-first, and disabled/pending origins distinguish narrow repairs. One-versus-redundant full reads rejects the wrong work rule. | +| ORC-014 | Node SQLite and Chromium worker/OPFS receive each new response origin and held-restore premise through the same comparison. Direct-write Node receiver holds the actual scoped hydration read and shares the differential judgment. | +| ORC-015 | Each host/campaign separately requires both orders, empty answers, peers and all five origins. Measured counts are recorded below. OPFS completes eight histories with matching premise checks. | +| ORC-016 | Fixed histories supply inputs to the shared model/driver/comparison. Durability calibration supplies a wrong observation to that judgment. Supplemental cleanup examples live in an ordinary test file. | + +### Reviewer assessment and bounds + +**Hire recommendation: yes for this review sample.** The reviewers found a +measurable neighboring work-law violation and two independent failure-fidelity +problems despite an initially green suite. They distinguished R3's sensitivity +probe from a proven production bug and identified missing cached receiving paths. +Their proposed baseline reuse preserved useful implementation freedom. The later +metric check and scoped-adapter diagnosis improved depth. The simplifier's zero +proposals avoided unnecessary churn. This supports strong oracle/debugging review +judgment; it does not establish every reviewer skill from one sample. + +Closure is bounded by complete eager startup responses over three string keys +and numeric values, one persistence instance, the five response origins and two +legal restore schedules, exact row/ownership cuts after preload and refetch, +startup work, and local-only durable reopen. It covers controlled storage, +Node SQLite, and eight actual Chromium OPFS histories. It does not establish +on-demand windows, concurrent tab writers, arbitrary metadata namespaces, +provider failures, or every intermediate publication. Existing retention and +direct-write laws remain under their existing owners; neighboring tests check +the affected paths. Random green does not prove all possible histories. + +The source-only package measurement is gzip **14,457→14,483 bytes (+26, 0.2%)**, +brotli **+18 bytes**. The ordinary app scenario is gzip **95,826→95,873 (+47)**. +The final repair removes the earlier duplicate-read and graph-lookup machinery; +the remaining shipped growth is the first-eager baseline choice and await. +Local dependency overrides and exact-version validation limits remain as stated +in the initial entry: they are excluded from intended PR content. + + +### Final validation and measured reach + +The final four-file Query run passed **552 tests**; the ordinary cleanup-helper +file passed **3 tests**. Chromium passed its **8 shared OPFS histories**. Query +package typechecking passed. The final direct-write gate extension then passed +all **45 cases** on repaired production and all **45 cases** with unchanged +`origin/main` Query source, restored afterward. Thus its causal-order correction +also agrees independently of this production repair. Earlier original-main +control timeouts did not establish a product deadlock or a mutant kill; their +intended gate had been bypassed. The final gate catches both buffered scoped +writes and ordinary outer-adapter writes, including durable acknowledgments. + +The normal four-file run measured these generated reaches. Both fixed campaigns +use seed 2367; random seeds were controlled **-913728351** and Node SQLite +**1382228077**. Every required state had minimum one, independently per host +and campaign. Each campaign ran 80 histories. + +| Declared state | Controlled fixed | Controlled random | Node fixed | Node random | +| --- | ---: | ---: | ---: | ---: | +| response-before-restore | 59 | 63 | 59 | 67 | +| restore-before-response | 21 | 17 | 21 | 13 | +| empty-result | 44 | 56 | 44 | 48 | +| peer-owner | 36 | 35 | 36 | 41 | +| fetch | 35 | 34 | 35 | 36 | +| initial-disabled | 13 | 15 | 13 | 11 | +| initial-pending | 9 | 12 | 9 | 15 | +| cached-disabled | 9 | 3 | 9 | 14 | +| cached-pending | 14 | 16 | 14 | 4 | + +Each host's eight fixed histories separately reached response-first seven times, +restore-first once, empty twice, peer three times, fetch four times, and each +cached/initialData origin once. Chromium completed those same eight histories +with all shared premise and result comparisons passing. + +Branch-wide `pnpm lint:changed` passed without lowered/removed oracle budgets. +Explicit ESLint found four mechanical import/type-assertion errors, which were +fixed without weakening any rule. Final explicit lint has zero errors; its six +warnings are existing shadowing/require-await locations. Formatting and +`git diff --check` passed. Production remains blob +`48b4624f5b179dd5b5e380b01161d510827c318c`. + +**Phase readiness:** local review, required oracle repairs and bounded validation +are complete; ready for the prep-pr confirmation before committing and preparing +the changeset/PR. **Merge readiness remains unassessed:** no repair PR exists, +and pushed-branch CI has not run. Exact locked Query Core 5.107.1 remains untested +because its registry request returned 403. Local recovery versions, their +lockfile changes, and scratch artifacts must remain outside the commit. diff --git a/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts b/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts new file mode 100644 index 0000000000..c39d648048 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from '@playwright/test' +import { declareOracle } from '../../db/tests/oracle-declaration' +import type { QueryStartupResult } from './query-startup-ownership.opfs' + +// The page uses the shared ownership comparison at every checkpoint. Check +// completion separately so a setup failure cannot masquerade as a law failure. +function checkStartupOwnership(result: QueryStartupResult) { + expect(result.error).toBeUndefined() + expect(result.results).toHaveLength(8) + expect(result.results.flatMap(({ violations }) => violations)).toEqual([]) +} +declareOracle({ check: [checkStartupOwnership] }) +test(`a Query response reconciles persisted owners across OPFS startup`, async ({ + page, +}) => { + page.on(`console`, (message) => + console.info(`browser ${message.type()}: ${message.text()}`), + ) + const errors: Array = [] + page.on(`pageerror`, (error) => errors.push(error.message)) + await page.goto(`/e2e/query-startup-ownership.opfs.html`) + await page.waitForFunction(() => window.__queryStartupOwnership !== undefined) + const result = await page.evaluate(() => window.__queryStartupOwnership!) + expect(errors).toEqual([]) + checkStartupOwnership(result) +}) diff --git a/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.html b/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.html new file mode 100644 index 0000000000..e578ac0f8f --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.html @@ -0,0 +1,11 @@ + + + + + + Query startup ownership oracle + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.ts new file mode 100644 index 0000000000..40f4ecf3cc --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.ts @@ -0,0 +1,70 @@ +// Real Chromium OPFS receives the ownership oracle's held baseline / fast +// Query response premise. It supplies storage only, never expected results. +import { + StartupOwnershipViolationError, + isStartupRowRead, + observeStartupOwnership, + startupOwnershipHistories, + startupOwnershipViolations, +} from '../../query-db-collection/tests/persisted-startup-ownership-oracle' +import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' +import { + createBrowserWASQLitePersistence, + openBrowserWASQLiteOPFSDatabase, +} from '../src' + +export type QueryStartupResult = { + results: Array<{ history: unknown; violations: Array }> + error?: string +} +declare global { + interface Window { + __queryStartupOwnership?: QueryStartupResult + } +} +async function run() { + const results: QueryStartupResult[`results`] = [] + try { + for (const [index, history] of startupOwnershipHistories.entries()) { + const database = await openBrowserWASQLiteOPFSDatabase({ + databaseName: `qs-${index}.sqlite`, + }) + try { + let rowReads = 0 + const execute = database.execute.bind(database) + database.execute = (sql, params) => { + if (isStartupRowRead(sql)) rowReads++ + return execute(sql, params) + } + try { + const observation = await observeStartupOwnership( + createBrowserWASQLitePersistence({ database }), + inProcessSeams.holdFirstRowSnapshot, + history, + `startup-${index}`, + () => rowReads, + ) + results.push({ + history, + violations: startupOwnershipViolations(history, observation), + }) + } catch (error) { + if (!(error instanceof StartupOwnershipViolationError)) throw error + results.push({ history, violations: error.violations }) + } + } finally { + await database.close?.() + } + } + window.__queryStartupOwnership = { results } + } catch (error) { + window.__queryStartupOwnership = { + results, + error: + error instanceof Error + ? `${error.stack} ${JSON.stringify(error, Object.getOwnPropertyNames(error))}` + : String(error), + } + } +} +void run() diff --git a/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts b/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts index db2fc5a79d..2002ae2e55 100644 --- a/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts +++ b/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ `leader-close.opfs.spec.ts`, `opfs-request-order-oracle.opfs.spec.ts`, `reload-class-oracle.opfs.spec.ts`, + `query-startup-ownership-oracle.opfs.spec.ts`, `persisted-on-demand-coverage.opfs.spec.ts`, ], timeout: 60_000, diff --git a/packages/db-sqlite-persistence-core/tests/reload-class-oracle.ts b/packages/db-sqlite-persistence-core/tests/reload-class-oracle.ts index 3cdc325af9..c91ac6bb8b 100644 --- a/packages/db-sqlite-persistence-core/tests/reload-class-oracle.ts +++ b/packages/db-sqlite-persistence-core/tests/reload-class-oracle.ts @@ -1132,6 +1132,12 @@ type ExpectedCollection = { terms: number } +// Each eager tab reads its rows once, and each restart reads them once more. +// Query startup receivers share this work judgment rather than restating it. +export function expectedEagerRowScans(tabs: number, restarts: number): number { + return tabs + restarts +} + export function expectedReload( history: ReloadHistory, topology: { adapterPerTab: boolean } = { adapterPerTab: true }, @@ -1189,7 +1195,10 @@ export function expectedReload( ? tabsOnThisGeneration : 1, // Each tab reads an eager Collection's rows once; a restart rereads. - rowScans: eager && !disposed ? tabs + (restarted ? 1 : 0) : undefined, + rowScans: + eager && !disposed + ? expectedEagerRowScans(tabs, restarted ? 1 : 0) + : undefined, // A held snapshot may or may not run after its tab closes. snapshots: disposed ? undefined @@ -1684,4 +1693,7 @@ export const fixedReloadHistories: ReadonlyArray<[string, ReloadHistory]> = [ // Owns nothing: this module holds the model and check only. The production // path is driven by the browser and Electron reload-class tests, which declare their own owned entries, which declares the owned entry. -declareOracle({ check: [reloadViolations], model: [expectedReload] }) +declareOracle({ + check: [reloadViolations], + model: [expectedReload, expectedEagerRowScans], +}) diff --git a/packages/db/tests/oracle-cleanup.ts b/packages/db/tests/oracle-cleanup.ts new file mode 100644 index 0000000000..07e58f89c2 --- /dev/null +++ b/packages/db/tests/oracle-cleanup.ts @@ -0,0 +1,37 @@ +/** + * Runs an oracle check, then every cleanup step in order, and returns its result. + * A cleanup failure never replaces the check's own failure: the check failure + * is thrown alone or as the `cause` of an `AggregateError` that also holds each + * cleanup failure. This module has no runtime imports, so browser drivers can + * use the same failure-preserving cleanup as Node drivers. + */ +export async function withOracleCleanup( + check: () => Promise | T, + cleanups: ReadonlyArray<() => unknown>, +): Promise { + const failures: Array = [] + let checkFailed = false + let result!: T + try { + result = await check() + } catch (error) { + checkFailed = true + failures.push(error) + } + for (const cleanup of cleanups) { + try { + await cleanup() + } catch (error) { + failures.push(error) + } + } + if (failures.length === 1) throw failures[0] + if (failures.length > 1) { + throw new AggregateError( + failures, + checkFailed ? `Oracle check and cleanup failed` : `Oracle cleanup failed`, + { cause: failures[0] }, + ) + } + return result +} diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 6279b56a7a..569ca6fc1e 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -329,6 +329,7 @@ const staticOracleProperties = [ `persisted-demand-coverage.history`, `persisted-demand-coverage.required`, `sqlite-reload.class`, + `query.startup-ownership`, `browser.multi-tab-reserved-history`, `browser.electric-source-prefix`, `sqlite-reload.class.electron`, diff --git a/packages/db/tests/utils-cleanup.test.ts b/packages/db/tests/utils-cleanup.test.ts new file mode 100644 index 0000000000..a78b1fdbf1 --- /dev/null +++ b/packages/db/tests/utils-cleanup.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest' +import { withOracleCleanup } from './utils' + +describe(`withOracleCleanup`, () => { + it(`returns the check result after cleanup`, async () => { + const result = { value: 1 } + let cleaned = false + const observed = await withOracleCleanup( + () => result, + [ + () => { + cleaned = true + }, + ], + ) + + expect(observed).toBe(result) + expect(cleaned).toBe(true) + }) + + it(`preserves the primary failure and every cleanup failure`, async () => { + const primary = new Error(`check failed`) + const first = new Error(`first cleanup failed`) + const second = new Error(`second cleanup failed`) + let observed: unknown + try { + await withOracleCleanup(() => { + throw primary + }, [ + () => { + throw first + }, + () => Promise.reject(second), + ]) + } catch (error) { + observed = error + } + + expect(observed).toBeInstanceOf(AggregateError) + const failure = observed as AggregateError + expect(failure.cause).toBe(primary) + expect(failure.errors).toEqual([primary, first, second]) + }) + + it(`attempts later cleanup steps after a cleanup failure`, async () => { + const failure = new Error(`cleanup failed`) + const attempts: Array = [] + const result = withOracleCleanup(() => { + attempts.push(`check`) + }, [ + () => { + attempts.push(`first`) + throw failure + }, + () => { + attempts.push(`second`) + }, + () => { + attempts.push(`third`) + }, + ]) + + await expect(result).rejects.toBe(failure) + expect(attempts).toEqual([`check`, `first`, `second`, `third`]) + }) +}) diff --git a/packages/db/tests/utils.ts b/packages/db/tests/utils.ts index 422b1582ad..b246cff779 100644 --- a/packages/db/tests/utils.ts +++ b/packages/db/tests/utils.ts @@ -15,43 +15,12 @@ import type { WithVirtualProps } from '../src/virtual-props.js' import type { Transaction } from '../src/transactions.js' import type { Collection } from '../src/collection/index.js' +export { withOracleCleanup } from './oracle-cleanup' + export type OutputWithVirtual< T extends object, TKey extends string | number = string | number, > = WithVirtualProps -/** - * Runs an oracle check, then every cleanup step in order. A cleanup failure - * never replaces the check's own failure: the check failure is thrown alone or - * as the `cause` of an `AggregateError` that also holds each cleanup failure. - */ -export async function withOracleCleanup( - check: () => Promise | void, - cleanups: ReadonlyArray<() => unknown>, -): Promise { - const failures: Array = [] - let checkFailed = false - try { - await check() - } catch (error) { - checkFailed = true - failures.push(error) - } - for (const cleanup of cleanups) { - try { - await cleanup() - } catch (error) { - failures.push(error) - } - } - if (failures.length === 1) throw failures[0] - if (failures.length > 1) { - throw new AggregateError( - failures, - checkFailed ? `Oracle check and cleanup failed` : `Oracle cleanup failed`, - { cause: failures[0] }, - ) - } -} // Keep sync startup, writes, readiness, and load outcomes in the test itself. export function createOnDemandCollection( diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 15b9a53206..48b4624f5b 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -2874,6 +2874,14 @@ export function queryCollectionOptions( } } + // The first eager result can arrive before persisted restore publishes. + // Its ownership baseline must include durable rows still absent in memory. + const needsPersistedBaseline = (queryHash: string) => + retainedQueriesPendingRevalidation.has(queryHash) || + (syncMode === `eager` && + persistence !== null && + !queryToRows.has(queryHash)) + const reconcileSuccessfulResult = async ( queryKey: QueryKey, result: QueryObserverResult, @@ -2888,10 +2896,18 @@ export function queryCollectionOptions( const validatedItems = selectedItems ?? validateSuccessfulResultItemsForApplication(queryKey, result) - const persistedBaseline = await loadPersistedBaselineForQuery( - hashedQueryKey, - validatedItems.length === 0, - ) + // Eager restore already owns the complete row read and its key evidence. + // Await that publication before diffing, rather than scanning it again. + const useHydratedBaseline = + syncMode === `eager` && + !retainedQueriesPendingRevalidation.has(hashedQueryKey) + if (useHydratedBaseline) await persistence?.hydrateBaseline() + const persistedBaseline = useHydratedBaseline + ? undefined + : await loadPersistedBaselineForQuery( + hashedQueryKey, + validatedItems.length === 0, + ) if ( collection.status === `cleaned-up` || resultApplicationControllers.get(hashedQueryKey) !== applicationToken @@ -3220,7 +3236,7 @@ export function queryCollectionOptions( hashedQueryKey, result, (signal, token) => - retainedQueriesPendingRevalidation.has(hashedQueryKey) + needsPersistedBaseline(hashedQueryKey) ? reconcileSuccessfulResult( queryKey, result, @@ -3286,23 +3302,26 @@ export function queryCollectionOptions( return } if (result.isFetching) return - - enqueueResultApplication(hashedQueryKey, result, (signal, token) => - reconcileSuccessfulResult( - queryKey, - result, - token, - signal, - undefined, - fetchStartGenerations.get(hashedQueryKey) ?? - directWriteGeneration, - ), - ) - } else { - enqueueResultApplication(hashedQueryKey, result, (signal, token) => - applySuccessfulResult(queryKey, result, token, undefined, signal), - ) } + enqueueResultApplication(hashedQueryKey, result, (signal, token) => + needsPersistedBaseline(hashedQueryKey) + ? reconcileSuccessfulResult( + queryKey, + result, + token, + signal, + undefined, + fetchStartGenerations.get(hashedQueryKey) ?? + directWriteGeneration, + ) + : applySuccessfulResult( + queryKey, + result, + token, + undefined, + signal, + ), + ) } if ( diff --git a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts index 72084d0978..70698ccf94 100644 --- a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts +++ b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts @@ -1,4 +1,6 @@ +import { DatabaseSync } from 'node:sqlite' import { hashKey, isCancelledError } from '@tanstack/query-core' +import fc from 'fast-check' import { DbClient, IR, @@ -11,7 +13,14 @@ import { } from '@tanstack/db' import { afterAll, afterEach, describe, expect, it, vi } from 'vitest' import { declareOracle } from '../../db/tests/oracle-declaration.js' -import { oracleRoute } from '../../db/tests/oracle-config.js' +import { + oracleFixedSeed, + oraclePropertyOptions, + oracleReplayTest, + oracleRoute, + oracleRouteOf, + oracleRuns, +} from '../../db/tests/oracle-config.js' import { createOracleReach } from '../../db/tests/oracle-reach' import { withOracleCleanup } from '../../db/tests/utils' import { atMutationCheckpoint } from '../../db/tests/mutation-confirmation-support' @@ -29,7 +38,21 @@ import { import { InvalidQueryResultError } from '../src/errors.js' import { queryCollectionOptions } from '../src/query.js' -import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { + createSQLiteCorePersistenceAdapter, + persistedCollectionOptions, +} from '../../db-sqlite-persistence-core/src' +import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' +import { BrowserWASQLiteDriver } from '../../browser-db-sqlite-persistence/src/wa-sqlite-driver' +import { createAdapter } from './persisted-direct-write-support' +import { + expectedStartupOwnership, + isStartupRowRead, + observeStartupOwnership, + startupOwnershipCutViolations, + startupOwnershipHistories, + startupOwnershipViolations, +} from './persisted-startup-ownership-oracle' import { categorySubset, cleanups, @@ -47,6 +70,8 @@ import { shared, storedItems, } from './ownership-lifecycle-support.js' +import type { StartupOwnershipHistory } from './persisted-startup-ownership-oracle' +import type { SQLInputValue } from 'node:sqlite' import type { MutationConfirmationObservation, MutationConfirmationRow, @@ -5991,3 +6016,234 @@ describe(`query collection ownership lifecycle`, () => { expect(publications).not.toContainEqual([first.id]) }) }) + +// Persisted restore and first Query response are independent participants. This +// generated dimension of the ownership graph crosses their delivery order. +const startupMembershipArb = fc.record({ + previous: fc.integer({ min: 1, max: 7 }), + next: fc.oneof(fc.constant(0), fc.integer({ min: 0, max: 7 })), + peer: fc.boolean(), +}) +const startupOwnershipHistoryArb: fc.Arbitrary = fc + .oneof( + { + weight: 3, + arbitrary: fc.record({ + membership: startupMembershipArb, + origin: fc.constant(`fetch` as const), + response: fc.constantFrom( + `before-restore` as const, + `after-restore` as const, + ), + }), + }, + { + weight: 4, + arbitrary: fc.record({ + membership: startupMembershipArb, + origin: fc.constantFrom( + `initial-disabled` as const, + `initial-pending` as const, + `cached-disabled` as const, + `cached-pending` as const, + ), + response: fc.constant(`before-restore` as const), + }), + }, + ) + .map(({ membership, ...schedule }) => ({ ...membership, ...schedule })) + +declareOracle({ + grammar: [startupOwnershipHistoryArb], + check: [startupOwnershipViolations], +}) + +// Calibration supplies a stale durable answer to the same cut comparison. +// A later refetch cannot erase an already checked startup mismatch. +oracleReplayTest(it)(`rejects stale durability before a later refetch`, () => { + const history = startupOwnershipHistories[0]! + const stale = expectedStartupOwnership({ ...history, next: history.previous }) + expect( + startupOwnershipCutViolations(history, `startupDurable`, stale), + ).not.toEqual([]) +}) + +for (const host of [`controlled`, `node-sqlite`] as const) { + describe(`persisted startup ownership through ${host}`, () => { + const reach = createOracleReach(`query.startup-ownership.${host}`, [ + `response-before-restore`, + `restore-before-response`, + `empty-result`, + `peer-owner`, + `fetch`, + `initial-disabled`, + `initial-pending`, + `cached-disabled`, + `cached-pending`, + ]) + afterAll(() => { + reach.assertReached() + console.info( + `query.startup-ownership.${host}`, + Object.fromEntries(reach.counts()), + ) + }) + let sequence = 0 + async function check( + history: StartupOwnershipHistory, + historyReach = reach, + ) { + historyReach.beginHistory() + const id = `startup-${host}-${sequence++}` + const database = + host === `node-sqlite` ? new DatabaseSync(`:memory:`) : undefined + let rowReads = 0 + const adapter = createAdapter([]) + const loadSnapshot = adapter.loadResumeSnapshot + adapter.loadResumeSnapshot = (...args) => { + if (args[1]?.includeRows !== false) rowReads++ + return loadSnapshot(...args) + } + const scanRows = adapter.scanRows + adapter.scanRows = (...args) => { + rowReads++ + return scanRows(...args) + } + const persistence = database + ? { + adapter: createSQLiteCorePersistenceAdapter({ + driver: new BrowserWASQLiteDriver({ + database: { + execute: ( + sql: string, + params: ReadonlyArray = [], + ) => { + if (isStartupRowRead(sql)) rowReads++ + if ( + !/^\s*(select|with|pragma)/i.test(sql) && + params.length === 0 + ) { + database.exec(sql) + return Promise.resolve([]) + } + return Promise.resolve( + database + .prepare(sql) + .all( + ...(params.map((value) => + value === undefined + ? null + : typeof value === `boolean` + ? Number(value) + : value, + ) as Array), + ) as Array, + ) + }, + }, + }), + }), + } + : { adapter } + const holdBaseline = database + ? inProcessSeams.holdFirstRowSnapshot + : () => { + const original = adapter.loadResumeSnapshot + let release!: () => void + const gate = new Promise((resolve) => { + release = resolve + }) + let held = false + let used = false + adapter.loadResumeSnapshot = async (...args) => { + if (!used && args[1]?.includeRows !== false) { + used = true + held = true + await gate + held = false + } + return original(...args) + } + return { + isHeld: () => held, + release, + uninstall: () => { + release() + adapter.loadResumeSnapshot = original + }, + } + } + try { + const observation = await observeStartupOwnership( + persistence, + holdBaseline, + history, + id, + () => rowReads, + () => { + historyReach.reach(history.origin) + historyReach.reach( + history.response === `before-restore` + ? `response-before-restore` + : `restore-before-response`, + ) + if (history.next === 0) historyReach.reach(`empty-result`) + if (history.peer) historyReach.reach(`peer-owner`) + }, + ) + expect(startupOwnershipViolations(history, observation)).toEqual([]) + } finally { + database?.close() + } + } + oracleReplayTest(it).each(startupOwnershipHistories)( + `reconciles persisted owners: %j`, + async (history) => { + oracleRoute(`fixed history`) + await check(history) + }, + ) + for (const campaign of [`fixed`, `random`] as const) { + const register = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, `query.startup-ownership`) + register( + `reconciles generated startup histories (${campaign})`, + async () => { + const parameters = + campaign === `fixed` + ? oracleFixedSeed(2367, { numRuns: oracleRuns(80) }) + : oraclePropertyOptions(80, `query.startup-ownership`) + oracleRoute(oracleRouteOf(parameters)) + const generatedReach = createOracleReach( + `query.startup-ownership.${host}.${campaign}`, + [ + `response-before-restore`, + `restore-before-response`, + `empty-result`, + `peer-owner`, + `fetch`, + `initial-disabled`, + `initial-pending`, + `cached-disabled`, + `cached-pending`, + ], + ) + await fc.assert( + fc.asyncProperty(startupOwnershipHistoryArb, (history) => + check(history, generatedReach), + ), + parameters, + ) + generatedReach.assertReached() + console.info( + `query.startup-ownership.${host}.${campaign}`, + Object.fromEntries(generatedReach.counts()), + ) + }, + 60_000, + ) + } + }) +} diff --git a/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts b/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts index 8f842e1f2c..54628be5ad 100644 --- a/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts +++ b/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts @@ -17,21 +17,24 @@ import { write, } from './persisted-direct-write-support' import type { QueryCollectionUtils } from '../src/query' +import type { PersistenceAdapter } from '../../db-sqlite-persistence-core/src' import type { Op, Row, StorageKind } from './persisted-direct-write-support' /** * # Does a direct write see the commits that came before it? * * A persisted Query Collection puts each source commit behind the persistence * wrapper's apply lock. While another task holds that lock, a committed - * refetch waits there, and core has not accepted it yet. Its result is already - * in the Query cache. The contract: a direct write (`writeInsert`, + * refetch can wait there before core accepts it. Startup hydration can also + * hold the first result before its source commit. The Query cache alone does + * not distinguish those orders. The contract: a direct write (`writeInsert`, * `writeUpdate`, `writeDelete`, `writeUpsert`) that runs in that window is * validated against, and applies on top of, the rows of every commit that came * before it. Persistence must not change what the write does. * * The reference is the same Query Collection without persistence. It runs the - * same operations, but no lock exists, so the earlier refetch has applied when - * the write runs. The reference does not share the persistence wrapper, which + * same causal order: a preceding source commit applies before the write; + * otherwise, its first result waits until after the write. The reference does + * not share the persistence wrapper, which * is the code under judgment. It shares the Query Collection's direct-write * code, so it cannot find a fault in that code that both paths share. The * precedence rules for writes that interleave with refetches are fixed as @@ -41,11 +44,11 @@ import type { Op, Row, StorageKind } from './persisted-direct-write-support' * * | Contract | History/domain | Production path | Observation and checkpoint | Owner and limits | * | --- | --- | --- | --- | --- | - * | validation sees earlier commits | 4 write types x 2 lock holders x 2 key cases | utils.write* -> manual-sync -> persisted commit | outcome of the write: `ok` or the error name | this oracle | + * | validation respects source commit order | 4 write types x 3 lock holders x 2 key cases, including startup before the first source commit | utils.write* -> manual-sync -> persisted commit | outcome of the write: `ok` or the error name | this oracle | * | the write applies on top | the same | the same | visible rows after every promise settles | this oracle | * | the Query cache agrees | the same | updateCacheData | Query cache rows after settlement | this oracle | * | storage agrees | the same | persisted durable write | stored rows after settlement equal the visible rows | this oracle | - * | real SQLite receives the same | a subset: each write type, `refetch-only`, `source-write`, and the ordering cases | the same, over the node SQLite adapter | the same observations | this oracle; the full matrix runs over an in-memory fake adapter | + * | real SQLite receives the same | a subset: each write type, `refetch-only`, all three holders, and the ordering cases | the same, over the node SQLite adapter | the same observations | this oracle; the full matrix runs over an in-memory fake adapter | * | rejected writes gain no precedence | 4 rejected writes during a held refetch, with and without persistence | utils.write* -> validation | rows and Query cache inside the refetch's fulfillment | unit tests in `persisted-direct-write.test.ts`; no oracle owner | * | a later accepted write keeps precedence | a second write after a deferred result arrives, same key and sibling key | deferred merge | rows, Query cache and stored rows after settlement | unit tests in `persisted-direct-write.test.ts`; no oracle owner | * | refetch settles after its rows apply | a deferred result, and one whose merged write is held | deferred merge -> result settlement | rows inside the refetch's fulfillment | unit tests in `persisted-direct-write.test.ts`; no oracle owner | @@ -57,27 +60,39 @@ import type { Op, Row, StorageKind } from './persisted-direct-write-support' * - `source-write`: an earlier refetch's durable write is held after core * applied it, so the lock stays busy. * - `startup`: persisted startup hydration is held, so the first fetch result - * waits for it. + * waits for it. Cache availability does not prove the source committed. + * - `initial-result-write`: the first Query result has committed; its durable + * write is held, so a later direct write waits for that earlier commit. + * + * The production driver records source commits before the direct write. That + * causal order selects the reference schedule, never its expected row values. + * With no earlier commit, the reference holds its first result until after the + * direct write. An early update/delete can then reject because no row is loaded + * (query-collection.md, "Individual Write Operations"). Both committed holders + * require positive commit reach; startup permits either legal commit order. * * A mutation confirmation does not take this lock for a Query Collection. * `persistAndConfirmCollectionMutations` runs only for sync-absent * collections, so that holder is outside this oracle. * - * Key cases: `refetch-only` means only the waiting refetch holds key `k`. - * `both` means the applied rows hold `k`, and the waiting refetch changes it. + * Key cases: `refetch-only` means only the waiting result holds key `k`. + * `both` means the baseline also holds `k`, and the waiting result changes it. + * The startup holder keeps that durable baseline from applying before the write. * - * The domain is a finite matrix of 16 cases, not a generated history, so no + * The domain is a finite matrix of 24 cases, not a generated history, so no * random campaign is claimed. The matrix comparison ignores whether an error * is thrown at once or rejects later. The precedence rules and the * duplicate-insert witness check that validation errors reject the promise. * - * The driver checks that it reached the window: when the write runs, storage - * does not hold the waiting refetch's row for `k` yet. This check does not - * depend on when core accepts the refetch, which a fix may change. Each failure message carries - * both observations, so a failure keeps the case that broke the law. + * The driver checks that it reached the window: when the write runs, no + * durable apply carrying the waiting result's `k` has completed. It records + * adapter acknowledgments instead of querying SQLite inside a held transaction. + * This check does not depend on when core accepts the refetch, which a fix may + * change. Each failure message carries both observations, so a failure keeps + * the case that broke the law. */ -type Holder = `source-write` | `startup` +type Holder = `source-write` | `startup` | `initial-result-write` type KeyCase = `refetch-only` | `both` type Observation = { @@ -87,7 +102,11 @@ type Observation = { } const ops: Array = [`insert`, `update`, `delete`, `upsert`] -const holders: Array = [`source-write`, `startup`] +const holders: Array = [ + `source-write`, + `startup`, + `initial-result-write`, +] const keyCases: Array = [`refetch-only`, `both`] /** The applied rows before the window, and the waiting refetch's rows. */ @@ -113,41 +132,59 @@ function scenario(keyCase: KeyCase): { base: Array; next: Array } { } /** - * Reference: no persistence. The waiting refetch has applied when the write - * runs, so its rows decide validation. + * Reference: no persistence. Replay the observed causal order of source commit + * and direct write. An earlier commit decides validation; before the first + * commit, the store is empty and the first fetch remains pending. */ -async function reference(op: Op, keyCase: KeyCase): Promise { +async function reference( + op: Op, + keyCase: KeyCase, + committedBeforeWrite = true, +): Promise { const { base, next } = scenario(keyCase) - let server = base + let server = committedBeforeWrite ? base : next + const releaseInitial = deferred() const queryClient = newQueryClient() const queryKey = [`reference`, op, keyCase] const collection = createCollection( queryCollectionOptions({ id: `reference-${op}-${keyCase}`, queryKey, - queryFn: () => Promise.resolve(server.map((row) => ({ ...row }))), + queryFn: async () => { + if (!committedBeforeWrite) await releaseInitial.promise + return server.map((row) => ({ ...row })) + }, queryClient, getKey: (row) => row.id, startSync: true, }), ) return checked(async () => { - await collection.preload() - server = next - await collection.utils.refetch() + const ready = collection.preload() + if (committedBeforeWrite) { + await ready + server = next + await collection.utils.refetch() + } const { outcome } = await outcomeOf(() => write(collection.utils, op)) + releaseInitial.resolve() + await ready await flush() return { outcome, visible: sortRows(collection.values()), cache: sortRows(queryClient.getQueryData(queryKey) ?? []), } - }, [() => collection.cleanup()]) + }, [ + () => releaseInitial.resolve(), + () => collection.cleanup(), + () => queryClient.clear(), + ]) } /** * Production driver: the persisted Query Collection. The write runs while the - * refetch that carries `next` waits on the apply lock. + * result carrying `next` waits for the held startup read or durable write. */ async function persisted( op: Op, @@ -155,12 +192,38 @@ async function persisted( keyCase: KeyCase, kind: StorageKind = `fake`, ): Promise< - Observation & { stored: Array; timing: string; waited: boolean } + Observation & { + stored: Array + timing: string + waited: boolean + commitsBeforeWrite: number + } > { const { base, next } = scenario(keyCase) const id = `persisted-${kind}-${op}-${holder}-${keyCase}` const storage = createStorage(kind, base, id) const adapter = storage.adapter + if (kind === `sqlite` && holder === `startup`) { + // The real adapter starts empty. Establish the same durable baseline as + // the controlled adapter before asking the startup read to hold it. + const seed = createCollection( + persistedCollectionOptions({ + id, + getKey: (row) => row.id, + persistence: storage.persistence, + sync: { + sync: ({ begin, write: syncWrite, commit, markReady }) => { + begin() + for (const row of base) syncWrite({ type: `insert`, value: row }) + commit() + markReady() + }, + }, + }), + ) + await seed.preload() + await seed.cleanup() + } const queryClient = newQueryClient() const queryKey = [`persisted`, kind, op, holder, keyCase] const release = deferred() @@ -168,32 +231,97 @@ async function persisted( let server = base let hold = false - if (holder === `source-write`) { - const applyCommittedTx = adapter.applyCommittedTx.bind(adapter) - adapter.applyCommittedTx = async (...args) => { + let nextDurablyApplied = false + const holdDurableWrite = + ( + apply: PersistenceAdapter[`applyCommittedTx`], + ): PersistenceAdapter[`applyCommittedTx`] => + async (...args) => { if (hold && args[1].mutations.length > 0) { hold = false entered.resolve() await release.promise } - return applyCommittedTx(...args) + const applied = await apply(...args) + if ( + args[1].mutations.some( + (mutation) => + mutation.type !== `delete` && + mutation.key === `k` && + (mutation.value as Row).value === 5, + ) + ) + nextDurablyApplied = true + return applied } - } else { - // Hold startup hydration: the row read after the metadata read. The - // source starts after the metadata read, and its first fetch returns - // `next`. - server = next - const loadResumeSnapshot = adapter.loadResumeSnapshot - adapter.loadResumeSnapshot = async (collectionId, options) => { - if (options?.includeRows !== false) { + const holdBaselineRead = + ( + read: PersistenceAdapter[`loadResumeSnapshot`], + ): PersistenceAdapter[`loadResumeSnapshot`] => + async (target, options) => { + if (holder === `startup` && options?.includeRows !== false) { entered.resolve() await release.promise } - return loadResumeSnapshot(collectionId, options) + return read(target, options) } + const persistenceAdapter: PersistenceAdapter = adapter + persistenceAdapter.applyCommittedTx = holdDurableWrite( + adapter.applyCommittedTx.bind(adapter), + ) + persistenceAdapter.loadResumeSnapshot = holdBaselineRead( + adapter.loadResumeSnapshot.bind(adapter), + ) + if (persistenceAdapter.runInHydrationScope) { + // SQLite startup reads and buffered source commits use this supplied + // adapter. Preserve the real scheduler turn and gate its methods too; + // wrapping only the outer adapter misses commits buffered during startup. + const runInHydrationScope = + persistenceAdapter.runInHydrationScope.bind(persistenceAdapter) + persistenceAdapter.runInHydrationScope = (task) => + runInHydrationScope((scopedAdapter) => + task({ + ...scopedAdapter, + applyCommittedTx: holdDurableWrite( + scopedAdapter.applyCommittedTx.bind(scopedAdapter), + ), + loadResumeSnapshot: holdBaselineRead( + scopedAdapter.loadResumeSnapshot.bind(scopedAdapter), + ), + }), + ) + } + if (holder === `initial-result-write`) { + server = next + hold = true + } else if (holder === `startup`) { + // Hold the baseline before it applies. Neither adapter path issues an + // observation read while the hold is live. + server = next } const errors = vi.spyOn(console, `error`).mockImplementation(() => {}) + let sourceCommits = 0 + const source = queryCollectionOptions({ + id, + queryKey, + queryFn: () => Promise.resolve(server.map((row) => ({ ...row }))), + queryClient, + getKey: (row) => row.id, + startSync: true, + }) + const sourceSync = source.sync.sync + source.sync = { + ...source.sync, + sync: (params) => + sourceSync({ + ...params, + commit: (...args) => { + sourceCommits++ + return params.commit(...args) + }, + }), + } const collection = createCollection( persistedCollectionOptions< Row, @@ -201,14 +329,7 @@ async function persisted( never, QueryCollectionUtils >({ - ...queryCollectionOptions({ - id, - queryKey, - queryFn: () => Promise.resolve(server.map((row) => ({ ...row }))), - queryClient, - getKey: (row) => row.id, - startSync: true, - }), + ...source, persistence: storage.persistence, }), ) @@ -228,16 +349,16 @@ async function persisted( pending.push(collection.preload()) await entered.promise } - // The Query cache holds `next` once its result is committed. + // Cache availability is separate from the recorded source commit boundary. await vi.waitFor(() => expect(sortRows(queryClient.getQueryData(queryKey) ?? [])).toEqual( sortRows(next), ), ) - // The window: the refetch is committed, but its durable write has not - // finished, because another task holds the lock. - const waited = - (await storage.storedRows(id)).find((row) => row.id === `k`)?.value !== 5 + // Durable storage has not received `next`. Record whether the source has + // committed, rather than inferring that fact from cache availability. + const waited = !nextDurablyApplied + const commitsBeforeWrite = sourceCommits const settled = outcomeOf(() => write(collection.utils, op)) release.resolve() const { outcome, timing } = await settled @@ -248,6 +369,7 @@ async function persisted( outcome, timing, waited, + commitsBeforeWrite, visible: sortRows(collection.values()), cache: sortRows(queryClient.getQueryData(queryKey) ?? []), stored: await storage.storedRows(id), @@ -261,11 +383,10 @@ async function persisted( } /** - * The matrix law: a direct write that runs while an earlier refetch waits for - * the apply lock returns the outcome and rows of the no-persistence reference, - * and storage then holds the visible rows. The reference is the model: it runs - * the same operations after the waiting refetch has applied. The driver's - * `waited` observation shows that the write ran inside the window. + * The matrix law: a direct write returns the outcome and rows of the + * no-persistence reference with the same source-commit order, and storage then + * holds the visible rows. The driver's `waited` observation shows that the + * write ran before the waiting result durably applied. */ async function compareWriteWithReference( op: Op, @@ -273,10 +394,15 @@ async function compareWriteWithReference( keyCase: KeyCase, kind: StorageKind = `fake`, ): Promise { - const expected = await reference(op, keyCase) const actual = await persisted(op, holder, keyCase, kind) + if (holder !== `startup`) + expect( + actual.commitsBeforeWrite, + `source committed before the write`, + ).toBeGreaterThan(0) + const expected = await reference(op, keyCase, actual.commitsBeforeWrite > 0) const label = kind === `sqlite` ? `sqlite` : `persisted` - // The driver reached the window: core had not accepted `next`. + // The driver reached the held durable boundary; source commit is separate. expect(actual.waited, `the refetch waited on the lock`).toBe(true) const observed = { outcome: actual.outcome, @@ -529,20 +655,17 @@ describe(`a later refetch while a persisted direct write waits`, () => { * The matrix above runs over an in-memory fake adapter, which supplies the * durable-write delay. This subset runs the same drivers over the node SQLite * adapter, gated the same way: each write type with the key that only the - * waiting refetch holds and an earlier refetch's durable write holding the - * lock, plus the four ordering cases. + * waiting refetch holds, at all three held boundaries, plus the four ordering + * cases. The same commit recorder determines the reference's causal order. */ describe(`real SQLite: persisted direct writes behind a waiting refetch`, () => { - for (const op of ops) { - it(`${op} with source-write holding the lock, key refetch-only`, async () => { - oracleRoute(`case`) - await compareWriteWithReference( - op, - `source-write`, - `refetch-only`, - `sqlite`, - ) - }) + for (const holder of holders) { + for (const op of ops) { + it(`${op} with ${holder} holding the lock, key refetch-only`, async () => { + oracleRoute(`case`) + await compareWriteWithReference(op, holder, `refetch-only`, `sqlite`) + }) + } } for (const start of [`before`, `after`] as const) { for (const touch of [`same`, `different`] as const) { @@ -568,7 +691,7 @@ describe(`real SQLite: persisted direct writes behind a waiting refetch`, () => */ async function compareDuplicateInsert(): Promise { const base: Array = [{ id: `k`, value: 1 }] - const reference = createCollection( + const referenceCollection = createCollection( queryCollectionOptions({ id: `reference-duplicate-insert`, queryKey: [`reference-duplicate-insert`], @@ -598,22 +721,22 @@ async function compareDuplicateInsert(): Promise { }), ) await checked(async () => { - await reference.preload() + await referenceCollection.preload() await persistedCollection.preload() // The returned promise rejects with DuplicateKeySyncError. A // synchronous throw fails here, because it escapes the call before // `expect` receives a promise. - for (const collection of [reference, persistedCollection]) { + for (const collection of [referenceCollection, persistedCollection]) { await expect( collection.utils.writeInsert({ id: `k`, value: 9 }), ).rejects.toBeInstanceOf(DuplicateKeySyncError) } await flush() expect(sortRows(persistedCollection.values())).toEqual( - sortRows(reference.values()), + sortRows(referenceCollection.values()), ) expect(sortRows(adapter.rows.values())).toEqual([{ id: `k`, value: 1 }]) - }, [() => reference.cleanup(), () => persistedCollection.cleanup()]) + }, [() => referenceCollection.cleanup(), () => persistedCollection.cleanup()]) } describe(`a persisted direct insert of an existing key`, () => { diff --git a/packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts b/packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts new file mode 100644 index 0000000000..8a094a9474 --- /dev/null +++ b/packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts @@ -0,0 +1,454 @@ +/** + * A complete Query result replaces that Query's persisted ownership, including + * rows whose persisted restore has not published yet. Other owners survive. + * Authority: docs/collections/query-collection.md, "Direct Writes and Query + * Sync" (a successful fetch reconciles ownership), and the ownership lifecycle + * oracle. Complete initialData/cache responses have the same materialization + * authority ("Initial Data" and "How Query Collections Work"). Persistence + * changes neither that judgment nor its durable result. Startup work also + * follows reload-class-oracle's one-row-read law; that model is shared below. + * + * This is a dimension of the ownership oracle, not a new ownership policy. + * The model is set replacement: remove the old Query edge, add the response's + * edges, and keep rows with another owner. It has no Query cache or hydration + * machinery. A peer is a persisted owner that this run has not revalidated. + * + * Histories use nonempty prior subsets of three keys, any next subset (including + * empty), optional peer ownership of the first prior key, and response delivery + * before or after restore. Complete initialData and cached responses are + * already available before restore, with fetching disabled or held pending. + * Values change on overlapping keys. Removing timing + * loses the reported race; removing overlap misses stale values; removing peers + * admits truncation. Duplicate keys and a response-before-fetch are excluded. + * + * The shared driver runs through persistedCollectionOptions and real QueryClient. + * A host holds the first baseline read, before its rows reach the wrapper. Query + * cache success proves response delivery while the hold remains active. Public + * rows and durable rows/owners are compared immediately after preload, before + * refetch can repair storage, and again inside/after refetch's fulfillment. + * The work recorder counts full row reads before its observation's own read. A final local-only reopen checks durable removal independently. + * Node SQLite and Chromium OPFS supply the same driver and comparison. This + * does not cover on-demand windows, multi-tab writes, or failed SQLite I/O. + */ +import { QueryClient, hashKey } from '@tanstack/query-core' +import { createCollection } from '@tanstack/db' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { queryCollectionOptions } from '../src/query' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { expectedEagerRowScans } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' +import type { HeldCall } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' + +export function isStartupRowRead(sql: string): boolean { + return sql + .replace(/\s+/g, ` `) + .trim() + .startsWith(`SELECT key, value, metadata, row_version FROM`) +} + +export class StartupOwnershipViolationError extends Error { + constructor(readonly violations: Array) { + super(violations.join(`\n`)) + this.name = `StartupOwnershipViolationError` + } +} + +export type StartupOwnershipHistory = { + origin: + | `fetch` + | `initial-disabled` + | `initial-pending` + | `cached-disabled` + | `cached-pending` + previous: number + next: number + peer: boolean + response: `before-restore` | `after-restore` +} +type Row = { id: string; value: number } +type Snapshot = { rows: Array; owners: Array<[string, Array]> } +export type StartupOwnershipObservation = { + responseBeforeRestore: boolean + startupRowReads: number + startupFetches: number + pendingFetchAtStartup: boolean + startupDurable: Snapshot + startup: Snapshot + refetch: Snapshot + durable: Snapshot + reopened: Array +} + +const rows = (mask: number, value: number): Array => + [`a`, `b`, `c`].flatMap((id, index) => + mask & (1 << index) ? [{ id, value }] : [], + ) +const sorted = (values: Iterable): Array => + Array.from(values, ({ id, value }) => ({ id, value })).sort((a, b) => + a.id.localeCompare(b.id), + ) + +export function expectedStartupOwnership(h: StartupOwnershipHistory): Snapshot { + const next = new Map(rows(h.next, 2).map((row) => [row.id, row])) + const peerKey = h.peer ? rows(h.previous, 1)[0]!.id : undefined + if (peerKey && !next.has(peerKey)) + next.set(peerKey, { id: peerKey, value: 1 }) + return { + rows: sorted(next.values()), + owners: [...next.keys()] + .sort() + .map((key) => [ + key, + [ + ...(rows(h.next, 2).some((row) => row.id === key) ? [`query`] : []), + ...(key === peerKey ? [`peer`] : []), + ].sort(), + ]), + } +} + +// Each captured cut is checked before cleanup can throw. The final receiver +// comparison reuses the same judgment; no fixed history carries an answer. +export function startupOwnershipCutViolations( + h: StartupOwnershipHistory, + cut: string, + actual: Snapshot, +): Array { + const expected = expectedStartupOwnership(h) + return JSON.stringify(actual) === JSON.stringify(expected) + ? [] + : [ + `${cut}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + ] +} + +export function startupOwnershipPremiseViolations( + h: StartupOwnershipHistory, + observation: Pick< + StartupOwnershipObservation, + | `responseBeforeRestore` + | `startupRowReads` + | `startupFetches` + | `pendingFetchAtStartup` + >, +): Array { + const violations: Array = [] + if (observation.responseBeforeRestore !== (h.response === `before-restore`)) + violations.push(`response/restore premise was not reached`) + const expectedReads = expectedEagerRowScans(1, 0) + if (observation.startupRowReads !== expectedReads) + violations.push( + `startup row reads: expected ${expectedReads}, got ${observation.startupRowReads}`, + ) + const disabled = h.origin.endsWith(`disabled`) + if (observation.startupFetches !== (disabled ? 0 : 1)) + violations.push(`startup fetch count did not match ${h.origin}`) + if (observation.pendingFetchAtStartup !== h.origin.endsWith(`pending`)) + violations.push(`pending fetch premise did not match ${h.origin}`) + return violations +} + +export function startupOwnershipViolations( + h: StartupOwnershipHistory, + observation: StartupOwnershipObservation, +): Array { + const violations = startupOwnershipPremiseViolations(h, observation) + for (const cut of [ + `startup`, + `startupDurable`, + `refetch`, + `durable`, + ] as const) + violations.push(...startupOwnershipCutViolations(h, cut, observation[cut])) + const expected = expectedStartupOwnership(h) + if (JSON.stringify(observation.reopened) !== JSON.stringify(expected.rows)) + violations.push( + `reopen: expected ${JSON.stringify(expected.rows)}, got ${JSON.stringify(observation.reopened)}`, + ) + return violations +} + +export const startupOwnershipHistories: Array = [ + { + origin: `fetch`, + previous: 1, + next: 2, + peer: false, + response: `before-restore`, + }, + { + origin: `fetch`, + previous: 1, + next: 0, + peer: false, + response: `before-restore`, + }, + { + origin: `fetch`, + previous: 3, + next: 6, + peer: true, + response: `before-restore`, + }, + { + origin: `fetch`, + previous: 3, + next: 6, + peer: false, + response: `after-restore`, + }, + { + origin: `initial-disabled`, + previous: 1, + next: 2, + peer: false, + response: `before-restore`, + }, + { + origin: `initial-pending`, + previous: 3, + next: 6, + peer: true, + response: `before-restore`, + }, + { + origin: `cached-disabled`, + previous: 1, + next: 0, + peer: false, + response: `before-restore`, + }, + { + origin: `cached-pending`, + previous: 3, + next: 6, + peer: true, + response: `before-restore`, + }, +] + +const turn = () => new Promise((resolve) => setTimeout(resolve, 0)) +async function until(premise: () => boolean): Promise { + for (let attempt = 0; attempt < 2_000; attempt++) { + if (premise()) return + await turn() + } + throw new Error(`Startup ownership driver did not reach its premise`) +} + +export async function observeStartupOwnership( + persistence: PersistedCollectionPersistence, + holdBaseline: (id: string) => HeldCall, + h: StartupOwnershipHistory, + id: string, + readRowCount: () => number, + onStartup: () => void = () => {}, +): Promise { + const queryHash = hashKey([id]) + const peerKey = h.peer ? rows(h.previous, 1)[0]!.id : undefined + // Seed a valid durable row/owner snapshot through the public sync boundary. + const seed = createCollection( + persistedCollectionOptions({ + id, + getKey: (row) => row.id, + persistence, + sync: { + sync: ({ begin, write, metadata, commit, markReady }) => { + begin() + for (const row of rows(h.previous, 1)) { + write({ type: `insert`, value: row }) + metadata!.row.set(row.id, { + queryCollection: { + owners: { + [queryHash]: true, + ...(row.id === peerKey ? { peer: true } : {}), + }, + }, + }) + } + commit() + markReady() + }, + }, + }), + ) + await seed.preload() + await seed.cleanup() + + const initialReadCount = readRowCount() + const hold = holdBaseline(id) + let releaseResponse!: () => void + const response = new Promise((resolve) => { + releaseResponse = resolve + }) + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: Infinity, + staleTime: Infinity, + }, + }, + }) + const pendingOrigin = h.origin.endsWith(`pending`) + const disabledOrigin = h.origin.endsWith(`disabled`) + if (h.origin.startsWith(`cached`)) + await client.fetchQuery({ + queryKey: [id], + queryFn: () => Promise.resolve(rows(h.next, 2)), + }) + const fetch = { completed: false, calls: 0 } + const unsubscribe = client.getQueryCache().subscribe((event) => { + if (event.type === `updated` && event.action.type === `success`) + fetch.completed = true + }) + const collection = createCollection( + persistedCollectionOptions({ + ...queryCollectionOptions({ + id, + queryKey: [id], + queryClient: client, + getKey: (row) => row.id, + enabled: !disabledOrigin, + staleTime: pendingOrigin ? 0 : Infinity, + ...(h.origin.startsWith(`initial`) + ? { initialData: rows(h.next, 2) } + : {}), + queryFn: async () => { + fetch.calls++ + if (pendingOrigin || h.response === `after-restore`) await response + return rows(h.next, 2) + }, + }), + persistence, + }), + ) + const snapshot = (): Snapshot => ({ + rows: sorted(collection.values()), + owners: [...collection.keys()].sort().map((key) => { + const metadata = collection._state.syncedMetadata.get(key) as + { queryCollection?: { owners?: Record } } | undefined + return [ + String(key), + Object.keys(metadata?.queryCollection?.owners ?? {}) + .map((owner) => (owner === queryHash ? `query` : owner)) + .sort(), + ] + }), + }) + const assertCut = (cut: string, actual: Snapshot) => { + const violations = startupOwnershipCutViolations(h, cut, actual) + if (violations.length) throw new StartupOwnershipViolationError(violations) + } + const durableSnapshot = async (): Promise => { + const stored = await persistence.adapter.loadResumeSnapshot( + { kind: `eager`, collectionId: id }, + { includeRows: true }, + ) + return { + rows: sorted(stored.rows.map((row) => row.value as Row)), + owners: [...stored.rows] + .sort((a, b) => String(a.key).localeCompare(String(b.key))) + .map((row) => [ + String(row.key), + Object.keys( + ( + row.metadata as + | { queryCollection?: { owners?: Record } } + | undefined + )?.queryCollection?.owners ?? {}, + ) + .map((owner) => (owner === queryHash ? `query` : owner)) + .sort(), + ]), + } + } + return withOracleCleanup(async () => { + const ready = collection.preload() + await until(hold.isHeld) + if (h.response === `before-restore` && h.origin === `fetch`) + await until(() => fetch.completed) + await turn() + const responseAvailable = + h.origin === `fetch` + ? fetch.completed + : client.getQueryData([id]) !== undefined + const responseBeforeRestore = responseAvailable && hold.isHeld() + hold.release() + if (h.response === `after-restore`) { + await until(() => collection.size > 0) + releaseResponse() + } + await ready + const startup = snapshot() + if (responseBeforeRestore !== (h.response === `before-restore`)) + throw new Error(`Startup response/restore premise was not reached`) + onStartup() + assertCut(`startup`, startup) + // Count startup work before the observation's own durable read. + const startupRowReads = readRowCount() - initialReadCount + const startupFetches = fetch.calls + const pendingFetchAtStartup = + client.getQueryState([id])?.fetchStatus === `fetching` + const premiseViolations = startupOwnershipPremiseViolations(h, { + responseBeforeRestore, + startupRowReads, + startupFetches, + pendingFetchAtStartup, + }) + if (premiseViolations.length) + throw new StartupOwnershipViolationError(premiseViolations) + const startupDurable = await durableSnapshot() + assertCut(`startupDurable`, startupDurable) + releaseResponse() + const refetch = await collection.utils.refetch().then(() => { + const captured = snapshot() + assertCut(`refetch`, captured) + return captured + }) + const durable = await durableSnapshot() + assertCut(`durable`, durable) + await collection.cleanup() + const reopened = createCollection( + persistedCollectionOptions({ + id, + getKey: (row) => row.id, + persistence, + }), + ) + return withOracleCleanup(async () => { + await reopened.preload() + const observation: StartupOwnershipObservation = { + responseBeforeRestore, + startupRowReads, + startupFetches, + pendingFetchAtStartup, + startup, + startupDurable, + refetch, + durable, + reopened: sorted(reopened.values()), + } + const violations = startupOwnershipViolations(h, observation) + if (violations.length) + throw new StartupOwnershipViolationError(violations) + return observation + }, [() => reopened.cleanup()]) + }, [ + () => hold.uninstall(), + releaseResponse, + unsubscribe, + () => collection.cleanup(), + () => client.clear(), + ]) +} + +declareOracle({ + model: [expectedStartupOwnership], + check: [ + startupOwnershipCutViolations, + startupOwnershipPremiseViolations, + startupOwnershipViolations, + ], +}) From 8c04cb34e875f9fcd46d8ce0e45a000c8a1db134 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 9 Nov 2026 18:55:13 -0700 Subject: [PATCH 02/24] chore: add changeset for persisted Query startup --- .changeset/fix-persisted-query-startup.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-persisted-query-startup.md diff --git a/.changeset/fix-persisted-query-startup.md b/.changeset/fix-persisted-query-startup.md new file mode 100644 index 0000000000..4631a5de0a --- /dev/null +++ b/.changeset/fix-persisted-query-startup.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-db-collection': patch +--- + +Remove obsolete persisted Query-owned rows on eager startup, including when initial data or cached results arrive before persisted restore. Preserve rows owned by other Queries. From 172f5d914c306dd76986391d7885885e61c0dedb Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 9 Nov 2026 19:06:52 -0700 Subject: [PATCH 03/24] test(db): register persisted Query startup replay Map query.startup-ownership to the existing owner and random replay campaigns. The completeness check was RED with no-named-owner; all 116 replay tests now pass, and the guarded seed 2367/path 0:0:0:0:0 records successful controlled and Node SQLite witnesses. Production code, histories, budgets and comparisons are unchanged. --- .../oracle-reviews/2026-11-09-issue-2367.md | 26 +++++++++++++++++++ packages/db/tests/oracle-replay-manifest.ts | 9 +++++++ 2 files changed, 35 insertions(+) diff --git a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md index f319958398..3b2edbcb98 100644 --- a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md +++ b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md @@ -472,3 +472,29 @@ the changeset/PR. **Merge readiness remains unassessed:** no repair PR exists, and pushed-branch CI has not run. Exact locked Query Core 5.107.1 remains untested because its registry request returned 403. Local recovery versions, their lockfile changes, and scratch artifacts must remain outside the commit. + + +## CI replay registration follow-up, 2026-11-09 + +PR #2394 initially ran at `8c04cb34e875f9fcd46d8ce0e45a000c8a1db134`. +The DB replay group rejected `query.startup-ownership` as `no-named-owner`. +The normal selector and direct Vitest replay existed, but the guarded replay +manifest had no file mapping. A local run of the existing “names an owner for +every registered property” check reproduced that exact assertion failure before +the repair. This is CI finding C1, separate from the earlier review ledger. + +The manifest now maps the property to the existing ownership oracle and selects +its random/replay campaigns. The model, histories, work budget, production +source and result comparisons are unchanged. Seed `2367`, path `0:0:0:0:0`, +through `node --import tsx ../db/tests/oracle-replay.ts` completed one history on +each of the controlled and Node SQLite paths. The guarded runner recorded two +matching, successful witnesses; it did not credit skipped or unrelated tests. +The full replay test file and static checks supply the remaining registration +verification. C1 is fixed by this mapping, rather than an exclusion from the +existing completeness assertion. + +The complete local `tests/oracle-replay.test.ts` run passed **116 tests** with +`--maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false`. The focused +manifest group independently passed **69 checks**. Explicit ESLint, formatting, +and diff checks passed for the manifest change. C1 is `fixed-now`; the original +production source blob and its oracle evidence remain unchanged. diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index 2bb7b6e262..5b9a4135f8 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -419,6 +419,11 @@ const ownerGroups: ReadonlyArray< ], // Owners of properties that a module names and a host file runs carry // that module as the fourth element. + [ + `query-db-collection/tests/ownership-lifecycle.oracle.test.ts`, + `query`, + `startup-ownership`, + ], [ `query-db-collection/tests/cursor-pagination.no-peek.test.ts`, `cursor-pagination`, @@ -951,6 +956,10 @@ export type OracleReplayManifestEntry = { // Pagination cells carry their property name in the test title so the guard // can select one cell without running the rest of the matrix. const replayTestNames = new Map([ + [ + `query.startup-ownership`, + `reconciles generated startup histories \\(random\\)`, + ], [ `includes-work.projection-payload`, `nested source payloads need no metadata descriptor scans \\(random or replayed\\)`, From 3044e20470b226ec642bf232222f09dee8e84fd4 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 9 Nov 2026 19:22:31 -0700 Subject: [PATCH 04/24] docs: keep startup review record focused on technical evidence --- .../oracle-reviews/2026-11-09-issue-2367.md | 58 ++++--------------- 1 file changed, 10 insertions(+), 48 deletions(-) diff --git a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md index 3b2edbcb98..6bbb8842a7 100644 --- a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md +++ b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md @@ -4,16 +4,11 @@ Evaluated [issue #2367](https://github.com/TanStack/db/issues/2367), reported by `jackmccloy`, on 2026-11-09. This is an issue report, not a pull-request review. -Starting and current HEAD: +Evaluation base: `ea3c920c1540486d0fb7410e089baa3a1583299c` (fresh `origin/main`). -The production repair and oracle extension are uncommitted on -`codex/evaluate-review-2367`. The repaired `query.ts` Git blob is +The first repaired `query.ts` Git blob was `97e8defd46d448c6ec3e19145d7490f3c5328074`. -This entry records that working-tree implementation; later evidence should be -appended with its new revision rather than replacing these results. - -The worktree is `.worktrees/codex-evaluate-review-2367`. Initial publication to -`origin/codex/evaluate-review-2367` succeeded. No repair commit or PR was pushed. +The prep-pr follow-up below records the later repair and superseding evidence. ## Law, experiment, and inference @@ -194,20 +189,6 @@ ownership comparisons. This sibling is separate from the five raw items. | ORC-015 | Separate required generated reach per host/campaign; measured counts above. Fixed receiving histories have separate completion and premise checks. | | ORC-016 | Fixed histories and browser receiving histories supply inputs to the shared law. They contain no separate expected row answers or concrete type assertions. | -## Reviewer assessment - -**Hire recommendation: yes, based on this debugging sample.** Technical accuracy -and signal are high: the report identifies the timing boundary, provides a -small executable reproduction, checks refetch persistence, and shows durable -ownership metadata. Its expected behavior follows the existing contract. -The source explanation agrees with the independent failing observations. - -The proposed direction is sound, but the report does not supply an implemented -repair or establish bug-class closure. It misses the empty-answer and peer-owner -distinctions that reject plausible narrow repairs. Prioritizing a persistent -server-deleted row is appropriate. This evidence supports debugging ability; -one issue report cannot establish whole-PR review breadth or implementation quality. - ## Validation, installation, and accounting `pnpm lint:changed` succeeded. Explicit ESLint reported zero errors and four @@ -242,18 +223,13 @@ separately fixed. Accounting and authorized repair are complete within the declared boundary. No known in-scope counterexample or law decision is left open. The coverage map links this dimension and its finite evidence. -**PR readiness: not assessed.** This task evaluated an issue and left an -uncommitted repair. It did not create a PR or assess a pushed repair's CI. - - ## Prep-pr follow-up, 2026-11-09 This append-only entry supersedes the implementation and coverage limits in the -initial evaluation above. HEAD remains +initial evaluation above. The evaluation base remains `ea3c920c1540486d0fb7410e089baa3a1583299c`; the reviewed first repair was source blob `97e8defd46d448c6ec3e19145d7490f3c5328074`. The final production source blob -is `48b4624f5b179dd5b5e380b01161d510827c318c`. No repair commit or PR exists at -this checkpoint. Dependency recovery and scratch evidence remain uncommitted. +is `48b4624f5b179dd5b5e380b01161d510827c318c`. ### Laws, experiments, and resulting repair @@ -371,9 +347,8 @@ contract was weakened and no user law decision is required. Commands are the four-file Query command and Chromium command above, with `--maxWorkers=2` for Vitest 5. Replay uses the same environment variables above -with the new path and the controlled random-property test selector. Scratch raw -outputs, attribution probes, mutation logs, and ledgers remain local under -`.review-2367/`; the verdict-critical results are preserved in this record. +with the new path and the controlled random-property test selector. The +verdict-critical results are preserved in this record. ### Updated oracle conformance @@ -396,16 +371,7 @@ outputs, attribution probes, mutation logs, and ledgers remain local under | ORC-015 | Each host/campaign separately requires both orders, empty answers, peers and all five origins. Measured counts are recorded below. OPFS completes eight histories with matching premise checks. | | ORC-016 | Fixed histories supply inputs to the shared model/driver/comparison. Durability calibration supplies a wrong observation to that judgment. Supplemental cleanup examples live in an ordinary test file. | -### Reviewer assessment and bounds - -**Hire recommendation: yes for this review sample.** The reviewers found a -measurable neighboring work-law violation and two independent failure-fidelity -problems despite an initially green suite. They distinguished R3's sensitivity -probe from a proven production bug and identified missing cached receiving paths. -Their proposed baseline reuse preserved useful implementation freedom. The later -metric check and scoped-adapter diagnosis improved depth. The simplifier's zero -proposals avoided unnecessary churn. This supports strong oracle/debugging review -judgment; it does not establish every reviewer skill from one sample. +### Evidence bounds Closure is bounded by complete eager startup responses over three string keys and numeric values, one persistence instance, the five response origins and two @@ -466,12 +432,8 @@ warnings are existing shadowing/require-await locations. Formatting and `git diff --check` passed. Production remains blob `48b4624f5b179dd5b5e380b01161d510827c318c`. -**Phase readiness:** local review, required oracle repairs and bounded validation -are complete; ready for the prep-pr confirmation before committing and preparing -the changeset/PR. **Merge readiness remains unassessed:** no repair PR exists, -and pushed-branch CI has not run. Exact locked Query Core 5.107.1 remains untested -because its registry request returned 403. Local recovery versions, their -lockfile changes, and scratch artifacts must remain outside the commit. +Exact locked Query Core 5.107.1 was not tested locally because its registry +request returned 403. The local dependency recovery is described above. ## CI replay registration follow-up, 2026-11-09 From 662a41ad68a0847e8123e80cdc37bf2e17910a2a Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 9 Nov 2026 20:34:28 -0700 Subject: [PATCH 05/24] Move persisted startup checks into shared adapter conformance --- docs/contributing/oracle-coverage.md | 82 ++-- .../oracle-reviews/2026-11-09-issue-2367.md | 159 +++++- ...ed-startup-conformance-oracle.opfs.spec.ts | 37 ++ .../persisted-startup-conformance.opfs.html | 14 + .../e2e/persisted-startup-conformance.opfs.ts | 100 ++++ ...uery-startup-ownership-oracle.opfs.spec.ts | 26 - .../e2e/query-startup-ownership.opfs.html | 11 - .../e2e/query-startup-ownership.opfs.ts | 70 --- .../playwright.opfs.config.ts | 2 +- packages/convex-db-collection/package.json | 2 +- .../tests/conformance-driver.ts | 10 +- ...tartup-conformance-oracle.property.test.ts | 25 + .../tests/persisted-startup-driver.ts | 70 +++ packages/db-adapter-conformance/MIGRATION.md | 75 ++- packages/db-adapter-conformance/README.md | 35 ++ .../src/persisted-startup-oracle.ts | 190 ++++++++ .../tests/memory-persistence.ts | 92 ++++ .../tests/persisted-startup-driver.ts | 201 ++++++++ .../tests/persisted-startup-host.ts | 96 ++++ .../tests/persisted-startup-oracle-suite.ts | 163 +++++++ packages/db/tests/oracle-config.ts | 1 + packages/db/tests/oracle-replay-manifest.ts | 18 +- packages/query-db-collection/package.json | 2 +- .../tests/ownership-lifecycle.oracle.test.ts | 260 +--------- .../tests/persisted-direct-write-support.ts | 103 +--- ...rsisted-startup-conformance-oracle.test.ts | 65 +++ .../tests/persisted-startup-driver.ts | 204 ++++++++ .../persisted-startup-ownership-oracle.ts | 454 ------------------ 28 files changed, 1592 insertions(+), 975 deletions(-) create mode 100644 packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts delete mode 100644 packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts delete mode 100644 packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.html delete mode 100644 packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.ts create mode 100644 packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts create mode 100644 packages/convex-db-collection/tests/persisted-startup-driver.ts create mode 100644 packages/db-adapter-conformance/src/persisted-startup-oracle.ts create mode 100644 packages/db-adapter-conformance/tests/memory-persistence.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-startup-driver.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-startup-host.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts create mode 100644 packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts create mode 100644 packages/query-db-collection/tests/persisted-startup-driver.ts delete mode 100644 packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 75502013d7..f3350daab7 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -2377,34 +2377,11 @@ remaining design and coverage questions, including valid Temporal kinds without public order and mixed-kind query order. Earlier PR-branch counterexamples do not by themselves establish an `origin/main` failure. -The Query DB ownership oracle's -[persisted startup dimension](../../packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts) -crosses a complete eager Query response with a held persisted restore (#2367). -Its independent set-replacement model removes only that Query's old ownership -and preserves another persisted owner. The generated grammar crosses nonempty -prior subsets of three keys, any next subset (weighted toward empty), changed -values on overlapping keys, a peer owner, and response-before/after-restore. -It also crosses complete initialData and cached responses with fetching disabled -or pending. Controlled storage and real Node SQLite each run fixed and random -80-history campaigns with separate declared reach for both schedules, empty -results, peer owners, and all five response origins. -The [Chromium OPFS receiver](../../packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts) -runs eight histories through the shared driver and comparison: the reported -race, an empty answer, shared ownership, a restore-first control, and each -cached/initialData origin. The checkpoints compare public and durable rows and -exact ownership after preload, before refetch can repair storage, then inside -and after refetch fulfillment and after a local-only reopen. Startup also checks -the reload oracle's existing one-full-row-read law. The repair awaits the -wrapper's baseline publication instead of scanning the same rows again. -Original omission behavior, a nonempty-only repair, and a redundant-scan repair -fail these comparisons. Captured mismatches and primary runtime errors survive -secondary cleanup errors. The neighboring persisted direct-write oracle records -source commit order independently of Query cache availability and compares the -matching nonpersisted schedule; Node SQLite receives all three held boundaries. -Limits: eager startup, three string keys and numeric -values, one persistence instance; no on-demand window, concurrent tab writer, -arbitrary metadata namespace, or failed-I/O claim. Evidence is in the -[issue evaluation](oracle-reviews/2026-11-09-issue-2367.md). +Persisted startup replacement is owned by the shared +[adapter persistence conformance oracle](#adapter-conformance-persisted-startup-2367). +The Query ownership oracle retains its scope-local removal, peer ownership, +mutation, and non-startup lifecycle laws. Its former startup dimension now runs +as a receiving path of the shared owner, with the same histories and cuts. The Query DB ownership oracle also owns the explicit mutation-handler refresh contract ([evidence](oracle-reviews/pr-2171-explicit-query-refresh.md)). Its 18-history table crosses insert, update, and delete with no @@ -7744,6 +7721,55 @@ 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). +## Adapter conformance: persisted startup (#2367) + +The [shared persistence owner](../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts) +judges complete source replacement across persisted restore. A collection-wide +source replaces every row. A scoped source removes only its old ownership; +an omitted row with a surviving peer stays. Authority: READY-3/PUB-7, the Query +complete-result and scope-local omission contract, and persistence's durable +acceptance contract. The maintainer selected this shared owner on 2026-11-09. +The independent set model has no Query cache, source diff, or restore algorithm. + +[Query](../../packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts) +and [Convex](../../packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts) +are real receiving paths: a QueryClient and a Convex JavaScript SDK over a +controlled protocol socket. Each uses the same +[composition driver](../../packages/db-adapter-conformance/tests/persisted-startup-driver.ts), +judgment, controlled storage, and real Node SQLite hosts. Per host, fixed and +random campaigns each run 80 generated histories with separate enforced reach. +Histories cross nonempty prior subsets of three keys, any next subset weighted +toward empty, changed overlap values, and response before/after persisted +restore. Query additionally crosses peer ownership and five response origins: +fetch, initialData/cache with fetching disabled or held pending. Already +available results precede restore; fetched and push results exercise both orders. + +The shared comparisons check public and durable rows immediately after preload, +before refresh can repair storage, then public rows inside refresh fulfillment, +durable rows afterward, and rows after a local-only reopen. Scoped Query +snapshots also check exact ownership at all four snapshot cuts. Startup checks +the reload owner's one-full-row-read law before its own durable observation. +Query's receiving driver preserves exact fetch-count and pending-fetch premises. +The [Chromium OPFS receiver](../../packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts) +runs all eight previous Query histories and four complete-snapshot Convex +histories through the same composition and check on the real worker/OPFS host. + +The original Query omission fails startup on both Node hosts and OPFS. Convex +without its first-result truncate, a survivor in the bare eager-source grammar, +now fails the same comparison on all three hosts. Current Convex passes; this +migration confirms no new Convex defect. The original Query baseline-await +repair stays at its adapter boundary. The old Query-only startup model/runner +and OPFS files are retired after receiving parity; Query-specific ownership +laws and the unrelated direct-write/failure-fidelity repairs remain. + +Limits: eager startup, three string keys and numeric values, one persistence +instance, one source scope plus an optional persisted peer. No on-demand window, +concurrent tab writer, arbitrary metadata namespace, failed-I/O, or every +intermediate-publication claim. Other adapters' persisted receiving paths remain +open under this shared owner; their bare eager conformance does not establish +persisted startup. See [migration status](../../packages/db-adapter-conformance/MIGRATION.md) +and the [versioned evidence](oracle-reviews/2026-11-09-issue-2367.md). + ## Adapter conformance: eager live sources (#2118) `packages/db-adapter-conformance/src/eager-live-source-oracle.ts` owns the diff --git a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md index 6bbb8842a7..16f9c16aba 100644 --- a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md +++ b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md @@ -1,5 +1,9 @@ # Persisted Query startup ownership: issue #2367 +The shared persistence migration below supersedes the startup oracle locations +and run commands in the historical sections. It preserves their recorded +outcomes and keeps Query's production repair unchanged. + ## Implementation state Evaluated [issue #2367](https://github.com/TanStack/db/issues/2367), reported by @@ -30,7 +34,7 @@ it does not judge a Query response's replacement of persisted ownership. The failure was a missing history and receiving-path crossing, not evidence that those narrower laws were wrong. -The new [shared dimension](../../../packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts) +The new [shared dimension](../../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts) uses independent set replacement. It removes the old Query edges, adds the response's edges, and retains rows with a peer owner. It copies no production query cache, reconciliation branch, or startup state machine. @@ -55,7 +59,7 @@ the adapter snapshot; public row values use Collection reads. There is no claim about every intermediate publication or about durable state before refetch. Controlled storage and real Node SQLite execute generated campaigns. A -[Chromium OPFS receiver](../../../packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts) +[Chromium OPFS receiver](../../../packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts) executes all four fixed histories through the actual worker and public Browser persistence factory. It reuses the shared judgment without separate answers. @@ -122,7 +126,7 @@ pnpm --filter @tanstack/query-db-collection exec vitest run \ tests/persisted-direct-write.test.ts tests/query.test.ts pnpm --filter @tanstack/browser-db-sqlite-persistence exec playwright test \ - --config playwright.opfs.config.ts query-startup-ownership-oracle.opfs.spec.ts + --config playwright.opfs.config.ts persisted-startup-conformance-oracle.opfs.spec.ts TANSTACK_DB_ORACLE_PROPERTY=query.startup-ownership \ TANSTACK_DB_ORACLE_SEED=2367 TANSTACK_DB_ORACLE_PATH=1:0:0 \ @@ -460,3 +464,152 @@ The complete local `tests/oracle-replay.test.ts` run passed **116 tests** with manifest group independently passed **69 checks**. Explicit ESLint, formatting, and diff checks passed for the manifest change. C1 is `fixed-now`; the original production source blob and its oracle evidence remain unchanged. + + +## Shared persistence conformance migration, 2026-11-09 + +The maintainer selected shared persistence conformance as the persisted-startup +oracle owner, with real receiving drivers for client-cache and push adapters. +This extends instrumentation and moves ownership; it changes no product law. +The Query repair remains blob `48b4624f5b179dd5b5e380b01161d510827c318c`. +Convex production is unchanged. Review started at +`3044e20470b226ec642bf232222f09dee8e84fd4` with the migration diff. + +The new primary owner is +[`persisted-startup-oracle.ts`](../../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts). +Its shared composition, host fixtures, and receiving suite live under +`packages/db-adapter-conformance/tests/persisted-startup-*`. +Query and Convex supply their real Collections and delivery controls. Query +projects its query hash to the abstract `source` owner; `peer` represents a +surviving persisted scope. Convex supplies a complete Collection-wide snapshot, +so it exposes rows and has no independent peer scope. This is a declared +projection, not a claim that Convex stores Query ownership metadata. + +The Query-only startup model and the appended startup runner in the ownership +lifecycle oracle are removed. Their eight fixed histories, exact generated +membership/origin weights, ownership checks, response schedules, pending/disabled +fetch premises, work bound, and observation cuts all remain in the receiving +path. Query's specialized lifecycle laws and the unrelated persisted direct-write +and cleanup repairs remain. The browser receiver is renamed and now receives +all eight Query histories plus four Convex histories through the same model, +composition, and comparison. The controlled storage fixture also moves without +changing its row/metadata transaction semantics. + +### Evidence block + +- **Law:** A complete source result replaces its persisted baseline. A scoped + source replaces only its own ownership, preserving peer-supported rows. Public + and durable snapshots must agree at startup, before another refresh can + repair storage, inside refresh fulfillment, after refresh durability, and + after local-only reopen. Authority: READY-3/PUB-7, the Query complete-result + and scope-local omission contract, and persistence's durable acceptance + contract. One initial restore owes one full row read, under the reload law. +- **Reach:** Both adapters run 80 generated histories per fixed/random campaign + per host. Each host and campaign enforces its own reach. Both schedules, + empty results, changed overlap, and omissions are required; Query additionally + requires a peer and all five response origins. Fixed histories are separate. + The table below records final normal package runs. +- **RED:** Restoring the Query source from the parent of `8c8e1517a` produces + 18 failed / 3 passed Node tests, including calibration. Fast responses retain + an omitted row or obsolete source ownership. The OPFS page completes all + 12 histories and rejects seven Query startup observations. This is a + comparison RED, not a setup failure. This migration introduces no production + repair, so it does not claim a new pre-edit production gate. +- **GREEN:** Normal Query validation passes 810 runtime/type checks in ten + files, with no type errors. Convex's startup, eager conformance, SDK/cache, + and snapshot suites pass 34 checks in five files, with no type errors. + The actual Chromium worker/OPFS test passes all 12 histories. All three + package `tsc --noEmit` checks pass. Additional replay/cleanup results and + final static checks are recorded below. +- **Mutants:** Query's nonempty-only baseline await causes 8 failures / 13 + passes; redundant durable scanning causes 20 failures / 1 calibration pass + (`startup row reads`, expected 1, observed 3). Convex without its first-result + truncate causes 12 failures / 1 calibration pass on Node and four startup + mismatches on OPFS. That mutant survived the bare eager grammar because its + restarts begin empty. The new persisted grammar supplies the retained-row + distinction. Every listed failure is at the intended comparison. All source + mutants are restored afterward. +- **Limits:** Three string keys and numeric values, nonempty prior baselines, + one eager persistence instance, one source with optional peer scope, and + reconfirmation of the same answer on refresh. No generated on-demand windows, + concurrent tabs, failed I/O, arbitrary metadata, intermediate-publication + claim, or other adapters' persisted receiving paths. The shared coverage-map + entry owns the remaining receiving work. Green sampling is not a universal + proof. No known reachable counterexample remains in the declared domain. + +| Adapter / host / campaign | Response first | Restore first | Empty | Changed overlap | Omission | Peer | Fetch | Initial disabled | Initial pending | Cached disabled | Cached pending | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| Query / controlled / fixed | 59 | 21 | 44 | 25 | 69 | 36 | 35 | 13 | 9 | 9 | 14 | +| Query / controlled / random | 64 | 16 | 51 | 22 | 70 | 39 | 31 | 13 | 11 | 10 | 15 | +| Query / node-sqlite / fixed | 59 | 21 | 44 | 25 | 69 | 36 | 35 | 13 | 9 | 9 | 14 | +| Query / node-sqlite / random | 61 | 19 | 43 | 28 | 66 | 45 | 40 | 14 | 9 | 12 | 5 | +| Convex / controlled / fixed | 37 | 43 | 46 | 27 | 64 | — | — | — | — | — | — | +| Convex / controlled / random | 33 | 47 | 43 | 26 | 65 | — | — | — | — | — | — | +| Convex / node-sqlite / fixed | 37 | 43 | 46 | 27 | 64 | — | — | — | — | — | — | +| Convex / node-sqlite / random | 39 | 41 | 49 | 25 | 68 | — | — | — | — | — | — | + + +The fixed campaign seed stays `2367`. The exact Query grammar rebuilds the +original `a → b` response-first trace, empty-result neighbor, overlapping update, +peer-survival case, restore-first control, and each cached/initialData origin. +The common mask grammar excludes duplicates and an empty prior baseline. +Collection-wide peers are rejected. Cached/initialData results are available +before restore, so their receiving grammar excludes restore-first schedules. + +Ablation: removing timing loses the startup race; removing empty results admits +a nonempty-only fix; removing overlap misses stale values; removing peers +admits global truncation for scoped sources; removing startup durability admits +repair by a later refetch. The row-read comparison rejects a correct-value but +redundant-scan repair. The generic stale-durability calibration rejects an old +snapshot before refresh can mask it. + +### Guide conformance and follow-up review + +| Requirement | Migration evidence | +| --- | --- | +| ORC-001 | Authority and explicit finite limits above and beside the shared model. | +| ORC-002 | Independent set replacement and ownership union, without source diff/cache/restore machinery. | +| ORC-003 | Shared literate model/grammar/check, named composition/hosts, and separate real adapter receivers. | +| ORC-004 | Reconstruction, ablation, ranges, and exclusions stated above. Query grammar weights and eight fixed histories preserved. | +| ORC-005 | Real QueryClient and Convex SDK/socket, actual persisted wrapper, controlled and Node SQLite plus actual worker/OPFS. Required ownership facts are seeded durably and available before the judged startup cut. | +| ORC-006 | Stale-durable calibration, original Query RED, narrow Query repairs, Convex no-truncate comparison RED. | +| ORC-007 | Same property and 80-history budget in fixed/random campaigns. Both replay IDs map to their receiving file and shared random selector. Guarded replay requires one successful comparison per host. | +| ORC-008 | No new state machine. The stateless model retains membership, new values and peer support because they distinguish removal from retention at the next observation. | +| ORC-009 | Query hash maps to source scope, peer maps to another persisted scope. A Collection-wide source has no peer metadata projection. | +| ORC-010 | Cuts checked immediately, before cleanup or later refresh. Shared withOracleCleanup preserves primary and secondary failures and runs every disposal. Existing cleanup calibration remains. | +| ORC-011 | No new shared semantic-fault hypothesis requiring a second formulation. The model is set algebra derived from the established contract. | +| ORC-012 | This amendment records applicable outcomes and bounded closure. Final implementation commit is identified below. | +| ORC-013 | Empty/nonempty, overlap/omission, peer/no-peer, before/after restore, and cached/fetched paths distinguish nearby wrong repairs. | +| ORC-014 | Identical composition and comparison on real SQLite and Chromium worker/OPFS. Convex server delivery remains a controlled wire premise, not a deployed-provider claim. | +| ORC-015 | Separate required counters per adapter, host and campaign, with measured counts above. No aggregated host receives credit for a missing path. | +| ORC-016 | Shared laws and calibrations only. Fixed histories supply no expected answers. | + +Independent follow-up review found two migration defects: stale coverage-map +links and TS2742 in the moved storage fixture's inferred return type. Both are +fixed. The latter has a normal-command RED and a read-only HEAD-overlay control; +an explicit public return type restores declaration portability. A simplifier +identified the synchronous/asynchronous factory return-type mismatch while the +same mechanical fix was being applied. A normal-command run also exposed +Vitest's static collector missing shared helper registrations, despite all 810 +runtime/type checks passing. Receiving `describe` suites make registration +visible to that collector. The final normal Query run passes all 810 checks. +No product-law revision or additional production fix follows from these items. + + +Final additional validation: the replay registry/guard suite and cleanup helper +suite pass **119 checks**. The existing Query replay ID, seed `2367`, path +`0:0:0:0:0`, records one successful comparison per host. The new Convex replay +ID `adapter.convex.persisted-startup`, seed `2367`, path `0`, records the same +two-host witness. Fixed histories and fixed-seed campaigns stand aside during +explicit replay. Package typechecking, explicit ESLint, formatting and staged +`pnpm lint:changed` pass. No lowered or removed oracle run budget is reported. +The only local dependency workarounds remain the previously documented Query +Core and fake-indexeddb overrides, excluded from every commit. + +Current receiving commands (repository root): + +```sh +pnpm --filter @tanstack/query-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false tests/persisted-startup-conformance-oracle.test.ts +pnpm --filter @tanstack/convex-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false tests/persisted-startup-conformance-oracle.property.test.ts +pnpm --filter @tanstack/browser-db-sqlite-persistence exec playwright test --config playwright.opfs.config.ts persisted-startup-conformance-oracle.opfs.spec.ts +``` diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts new file mode 100644 index 0000000000..1eae73d4ee --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts @@ -0,0 +1,37 @@ +import { expect, test } from '@playwright/test' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { queryStartupHistories } from '../../query-db-collection/tests/persisted-startup-driver' +import { persistedStartupHistories } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { StartupConformanceResult } from './persisted-startup-conformance.opfs' + +// The page compares each cut with the shared model. Count each adapter's +// completion separately so a missing receiver cannot masquerade as conformance. +function checkStartupConformance(result: StartupConformanceResult) { + expect(result.error).toBeUndefined() + expect( + result.results.filter((item) => item.adapter === `query`), + ).toHaveLength(queryStartupHistories.length) + expect( + result.results.filter((item) => item.adapter === `convex`), + ).toHaveLength(persistedStartupHistories.length) + expect(result.results.flatMap(({ violations }) => violations)).toEqual([]) +} +declareOracle({ check: [checkStartupConformance] }) +test(`complete source results replace persisted rows across OPFS startup`, async ({ + page, +}) => { + page.on(`console`, (message) => + console.info(`browser ${message.type()}: ${message.text()}`), + ) + const errors: Array = [] + page.on(`pageerror`, (error) => errors.push(error.message)) + await page.goto(`/e2e/persisted-startup-conformance.opfs.html`) + await page.waitForFunction( + () => window.__persistedStartupConformance !== undefined, + ) + const result = await page.evaluate( + () => window.__persistedStartupConformance!, + ) + expect(errors).toEqual([]) + checkStartupConformance(result) +}) diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.html b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.html new file mode 100644 index 0000000000..2b49076478 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.html @@ -0,0 +1,14 @@ + + + + + + Persisted startup conformance oracle + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts new file mode 100644 index 0000000000..cab07f5b46 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts @@ -0,0 +1,100 @@ +// Chromium OPFS receives the same shared startup comparison as both Node +// hosts. The real QueryClient and Convex SDK supply their adapter boundaries; +// this page supplies only actual worker/OPFS storage and result reporting. +import { + PersistedStartupViolation, + persistedStartupHistories, + persistedStartupViolations, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { + isStartupRowRead, + observePersistedStartup, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { + queryStartupAdapter, + queryStartupHistories, +} from '../../query-db-collection/tests/persisted-startup-driver' +import { convexStartupAdapter } from '../../convex-db-collection/tests/persisted-startup-driver' +import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' +import { + createBrowserWASQLitePersistence, + openBrowserWASQLiteOPFSDatabase, +} from '../src' +import type { PersistedStartupHistory } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' + +export type StartupConformanceResult = { + results: Array<{ + adapter: string + history: unknown + violations: Array + }> + error?: string +} +declare global { + interface Window { + __persistedStartupConformance?: StartupConformanceResult + } +} +async function run() { + const results: StartupConformanceResult[`results`] = [] + let sequence = 0 + async function receive( + name: string, + adapter: PersistedStartupAdapter, + histories: Array, + ) { + for (const history of histories) { + const index = sequence++ + const database = await openBrowserWASQLiteOPFSDatabase({ + databaseName: `ps-${index}.sqlite`, + }) + try { + let rowReads = 0 + const execute = database.execute.bind(database) + database.execute = (sql, params) => { + if (isStartupRowRead(sql)) rowReads++ + return execute(sql, params) + } + try { + const observation = await observePersistedStartup( + adapter, + createBrowserWASQLitePersistence({ database }), + inProcessSeams.holdFirstRowSnapshot, + history, + `startup-${index}`, + () => rowReads, + ) + results.push({ + adapter: name, + history, + violations: persistedStartupViolations( + history, + adapter.scope, + observation, + ), + }) + } catch (error) { + if (!(error instanceof PersistedStartupViolation)) throw error + results.push({ adapter: name, history, violations: error.violations }) + } + } finally { + await database.close?.() + } + } + } + try { + await receive(`query`, queryStartupAdapter, queryStartupHistories) + await receive(`convex`, convexStartupAdapter, persistedStartupHistories) + window.__persistedStartupConformance = { results } + } catch (error) { + window.__persistedStartupConformance = { + results, + error: + error instanceof Error + ? `${error.stack} ${JSON.stringify(error, Object.getOwnPropertyNames(error))}` + : String(error), + } + } +} +void run() diff --git a/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts b/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts deleted file mode 100644 index c39d648048..0000000000 --- a/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership-oracle.opfs.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect, test } from '@playwright/test' -import { declareOracle } from '../../db/tests/oracle-declaration' -import type { QueryStartupResult } from './query-startup-ownership.opfs' - -// The page uses the shared ownership comparison at every checkpoint. Check -// completion separately so a setup failure cannot masquerade as a law failure. -function checkStartupOwnership(result: QueryStartupResult) { - expect(result.error).toBeUndefined() - expect(result.results).toHaveLength(8) - expect(result.results.flatMap(({ violations }) => violations)).toEqual([]) -} -declareOracle({ check: [checkStartupOwnership] }) -test(`a Query response reconciles persisted owners across OPFS startup`, async ({ - page, -}) => { - page.on(`console`, (message) => - console.info(`browser ${message.type()}: ${message.text()}`), - ) - const errors: Array = [] - page.on(`pageerror`, (error) => errors.push(error.message)) - await page.goto(`/e2e/query-startup-ownership.opfs.html`) - await page.waitForFunction(() => window.__queryStartupOwnership !== undefined) - const result = await page.evaluate(() => window.__queryStartupOwnership!) - expect(errors).toEqual([]) - checkStartupOwnership(result) -}) diff --git a/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.html b/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.html deleted file mode 100644 index e578ac0f8f..0000000000 --- a/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Query startup ownership oracle - - - - - diff --git a/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.ts deleted file mode 100644 index 40f4ecf3cc..0000000000 --- a/packages/browser-db-sqlite-persistence/e2e/query-startup-ownership.opfs.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Real Chromium OPFS receives the ownership oracle's held baseline / fast -// Query response premise. It supplies storage only, never expected results. -import { - StartupOwnershipViolationError, - isStartupRowRead, - observeStartupOwnership, - startupOwnershipHistories, - startupOwnershipViolations, -} from '../../query-db-collection/tests/persisted-startup-ownership-oracle' -import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' -import { - createBrowserWASQLitePersistence, - openBrowserWASQLiteOPFSDatabase, -} from '../src' - -export type QueryStartupResult = { - results: Array<{ history: unknown; violations: Array }> - error?: string -} -declare global { - interface Window { - __queryStartupOwnership?: QueryStartupResult - } -} -async function run() { - const results: QueryStartupResult[`results`] = [] - try { - for (const [index, history] of startupOwnershipHistories.entries()) { - const database = await openBrowserWASQLiteOPFSDatabase({ - databaseName: `qs-${index}.sqlite`, - }) - try { - let rowReads = 0 - const execute = database.execute.bind(database) - database.execute = (sql, params) => { - if (isStartupRowRead(sql)) rowReads++ - return execute(sql, params) - } - try { - const observation = await observeStartupOwnership( - createBrowserWASQLitePersistence({ database }), - inProcessSeams.holdFirstRowSnapshot, - history, - `startup-${index}`, - () => rowReads, - ) - results.push({ - history, - violations: startupOwnershipViolations(history, observation), - }) - } catch (error) { - if (!(error instanceof StartupOwnershipViolationError)) throw error - results.push({ history, violations: error.violations }) - } - } finally { - await database.close?.() - } - } - window.__queryStartupOwnership = { results } - } catch (error) { - window.__queryStartupOwnership = { - results, - error: - error instanceof Error - ? `${error.stack} ${JSON.stringify(error, Object.getOwnPropertyNames(error))}` - : String(error), - } - } -} -void run() diff --git a/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts b/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts index 2002ae2e55..0a3c2a180b 100644 --- a/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts +++ b/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ `leader-close.opfs.spec.ts`, `opfs-request-order-oracle.opfs.spec.ts`, `reload-class-oracle.opfs.spec.ts`, - `query-startup-ownership-oracle.opfs.spec.ts`, + `persisted-startup-conformance-oracle.opfs.spec.ts`, `persisted-on-demand-coverage.opfs.spec.ts`, ], timeout: 60_000, diff --git a/packages/convex-db-collection/package.json b/packages/convex-db-collection/package.json index 0cdcbe8b9c..57b0926592 100644 --- a/packages/convex-db-collection/package.json +++ b/packages/convex-db-collection/package.json @@ -21,7 +21,7 @@ "dev": "vite build --watch", "lint": "eslint . --fix", "test": "vitest run", - "test:oracles": "vitest run tests/snapshot-oracle.property.test.ts tests/ownership-oracle.property.test.ts tests/pagination-oracle.property.test.ts tests/offline-oracle.property.test.ts tests/values-oracle.property.test.ts tests/lazy-paging-oracle.property.test.ts tests/lazy-paging-history-oracle.property.test.ts tests/lazy-paging-receipt-oracle.property.test.ts tests/sdk-cache-oracle.test.ts tests/lazy-paging-interleaving-oracle.property.test.ts tests/lazy-paging-native-receipt-oracle.property.test.ts tests/lazy-paging-version-oracle.property.test.ts tests/lazy-paging-failure-history-oracle.property.test.ts tests/lazy-paging-revalidation-oracle.property.test.ts tests/lazy-paging-load-outcome-oracle.property.test.ts tests/lazy-paging-abort-replay-oracle.property.test.ts tests/lazy-paging-write-model-oracle.test.ts", + "test:oracles": "vitest run tests/snapshot-oracle.property.test.ts tests/ownership-oracle.property.test.ts tests/pagination-oracle.property.test.ts tests/offline-oracle.property.test.ts tests/values-oracle.property.test.ts tests/lazy-paging-oracle.property.test.ts tests/lazy-paging-history-oracle.property.test.ts tests/lazy-paging-receipt-oracle.property.test.ts tests/sdk-cache-oracle.test.ts tests/lazy-paging-interleaving-oracle.property.test.ts tests/lazy-paging-native-receipt-oracle.property.test.ts tests/lazy-paging-version-oracle.property.test.ts tests/lazy-paging-failure-history-oracle.property.test.ts tests/lazy-paging-revalidation-oracle.property.test.ts tests/lazy-paging-load-outcome-oracle.property.test.ts tests/lazy-paging-abort-replay-oracle.property.test.ts tests/lazy-paging-write-model-oracle.test.ts tests/persisted-startup-conformance-oracle.property.test.ts", "typecheck": "tsc --noEmit", "test:e2e": "vitest run --config vitest.e2e.config.ts", "test:e2e:lazy": "vitest run --config vitest.lazy.e2e.config.ts" diff --git a/packages/convex-db-collection/tests/conformance-driver.ts b/packages/convex-db-collection/tests/conformance-driver.ts index d80574dd92..f41a736335 100644 --- a/packages/convex-db-collection/tests/conformance-driver.ts +++ b/packages/convex-db-collection/tests/conformance-driver.ts @@ -75,7 +75,7 @@ const VALUE_KINDS: ReadonlyArray = [ `object`, ] -const rowsQuery = makeFunctionReference< +export const rowsQuery = makeFunctionReference< `query`, Record, Array @@ -113,9 +113,9 @@ class Socket { } function timestamp(value: number): string { - const bytes = Buffer.alloc(8) - bytes.writeBigUInt64LE(BigInt(value)) - return bytes.toString(`base64`) + const bytes = new Uint8Array(8) + new DataView(bytes.buffer).setBigUint64(0, BigInt(value), true) + return btoa(String.fromCharCode(...bytes)) } const turn = () => new Promise((resolve) => setTimeout(resolve, 0)) @@ -124,7 +124,7 @@ const turn = () => new Promise((resolve) => setTimeout(resolve, 0)) const hasDuplicateKey = (rows: ReadonlyArray) => new Set(rows.map((row) => row.id)).size !== rows.length -class ConvexProvider implements ControlledProvider { +export class ConvexProvider implements ControlledProvider { readonly client: ConvexClient | ConvexReactClient private socketOrUndefined: Socket | undefined private readonly registrations: Array<{ releases: number }> = [] diff --git a/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts new file mode 100644 index 0000000000..a0ab070588 --- /dev/null +++ b/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -0,0 +1,25 @@ +/** Convex receives the shared persistence law through complete push snapshots. + * Its Collection-wide scope has no peer ownership. Nonempty durable rows, + * omissions, overlapping updates and empty results cross both restore orders. + */ +import { describe } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { + persistedStartupHistories, + persistedStartupHistoryArb, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' +import { convexStartupAdapter } from './persisted-startup-driver' + +const historyArb = persistedStartupHistoryArb(`collection`) +declareOracle({ grammar: [historyArb] }) +// Keep registration visible to Vitest's static typecheck collector as well +// as its runtime collector; the shared suite registers the actual law checks. +describe(`persisted startup conformance: Convex`, () => { + registerPersistedStartupConformance({ + property: `adapter.convex.persisted-startup`, + adapter: convexStartupAdapter, + histories: persistedStartupHistories, + arbitrary: historyArb, + }) +}) diff --git a/packages/convex-db-collection/tests/persisted-startup-driver.ts b/packages/convex-db-collection/tests/persisted-startup-driver.ts new file mode 100644 index 0000000000..c90b22119c --- /dev/null +++ b/packages/convex-db-collection/tests/persisted-startup-driver.ts @@ -0,0 +1,70 @@ +/** Complete-snapshot push receiving path for shared persistence conformance. + * The real Convex adapter/client decode QueryUpdated messages over the shared + * controlled socket. `select` maps the wire's numeric ID to the common row. + * A single complete source owns the Collection, so independent peers do not + * exist here. Both delivery/restore orders use the same shared comparison. + */ +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { convexCollectionOptions } from '../src' +import { + sortedStartupRows, + startupRows, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { ConvexProvider, rowsQuery } from './conformance-driver' +import type { PersistedStartupHistory } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' + +export const convexStartupAdapter: PersistedStartupAdapter = + { + scope: `collection`, + create(persistence, h, id) { + const provider = new ConvexProvider(`javascript`) + const collection = createCollection( + persistedCollectionOptions({ + ...convexCollectionOptions({ + id, + client: provider.client, + query: rowsQuery, + queryArgs: {}, + select: (result) => + result.map((row) => ({ + id: String.fromCharCode(97 + row.id), + value: row.value as number, + })), + getKey: (row) => row.id, + }), + persistence, + }), + ) + let available = false + const deliver = async () => { + await provider.settle() + provider.deliverSnapshot( + startupRows(h.next, 2).map((row) => ({ + id: row.id.charCodeAt(0) - 97, + title: row.id, + value: row.value, + })), + ) + await provider.settle() + available = true + } + return { + collection, + snapshot: () => ({ rows: sortedStartupRows(collection.values()) }), + deliver, + responseAvailable: () => available, + refresh: async () => { + await deliver() + await collection.utils.awaitSync() + }, + dispose: () => + withOracleCleanup(async () => {}, [ + () => collection.cleanup(), + provider.dispose, + ]), + } + }, + } diff --git a/packages/db-adapter-conformance/MIGRATION.md b/packages/db-adapter-conformance/MIGRATION.md index 403ab601d7..9d8e8fe7b1 100644 --- a/packages/db-adapter-conformance/MIGRATION.md +++ b/packages/db-adapter-conformance/MIGRATION.md @@ -14,32 +14,32 @@ reviewed change. ## Oracle families -| Family | Status | Laws | -| ------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| Eager live source | Built (`src/eager-live-source-oracle.ts`) | READY-2/3/4/8/10, HEALTH-1/2/5, PUB-1/2/3/5/7/9, FID-1/4, LIFE-2/5/10, EVID-1/2/3, RETRY-1 (eager), RECON-1, RENEW-1 | -| Mutations and confirmation | Needed | MUT-1/2/4/5/7/10/13, FID-5 | -| On-demand loading | Needed | DEM-1 to DEM-23, MAP-1 to MAP-4 | -| Reload and retry | Needed | RELOAD-1 to RELOAD-5; RETRY-1 and HEALTH-2 beyond the eager source's retried failures | -| Client cache and refetch | Needed | CACHE-1 to CACHE-8, REF-1 to REF-7, DIR-1 to DIR-5 | -| Source shapes | Needed | RESET-1, RESUME-1 to RESUME-3, PROG-1, MEMB-1/2, PAGE-1 to PAGE-3, OWN-1/2, META-1 | -| Local and shared stores | Needed | SELF-1 to SELF-3, STORE-1 to STORE-4, PEER-1 | -| Persistence wrapper and storage hosts | Needed | `P-*`, `S-*` | +| Family | Status | Laws | +| ------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| Eager live source | Built (`src/eager-live-source-oracle.ts`) | READY-2/3/4/8/10, HEALTH-1/2/5, PUB-1/2/3/5/7/9, FID-1/4, LIFE-2/5/10, EVID-1/2/3, RETRY-1 (eager), RECON-1, RENEW-1 | +| Mutations and confirmation | Needed | MUT-1/2/4/5/7/10/13, FID-5 | +| On-demand loading | Needed | DEM-1 to DEM-23, MAP-1 to MAP-4 | +| Reload and retry | Needed | RELOAD-1 to RELOAD-5; RETRY-1 and HEALTH-2 beyond the eager source's retried failures | +| Client cache and refetch | Needed | CACHE-1 to CACHE-8, REF-1 to REF-7, DIR-1 to DIR-5 | +| Source shapes | Needed | RESET-1, RESUME-1 to RESUME-3, PROG-1, MEMB-1/2, PAGE-1 to PAGE-3, OWN-1/2, META-1 | +| Local and shared stores | Needed | SELF-1 to SELF-3, STORE-1 to STORE-4, PEER-1 | +| Persistence wrapper and storage hosts | Startup replacement built (`src/persisted-startup-oracle.ts`); other laws needed | `P-*`, `S-*` | ## Adapters -| Adapter | Families that apply (section A) | Driver status | -| ----------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Neon Realtime | eager live source, mutations | Eager: RED (READY-4, HEALTH-1, and LIFE-10 behind them); adapter update pending | -| Electric | eager live source, mutations, on-demand, source shapes, reload and retry, persistence | Eager: GREEN (2026-10-30) after three fixes, per-batch commits, typed Postgres columns, over the installed client. Driver `electric-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.electric`. Other families: not started | -| PowerSync | eager live source, mutations, on-demand | Eager: GREEN (2026-10-30) after a fix, per-batch commits, over real local SQLite. Driver `powersync-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.powersync`. Mutations, on-demand: not started | -| RxDB | eager live source, mutations | Eager: GREEN (2026-10-29) after a fix, per-change commits, `listen-before-fetch`. Driver `rxdb-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.rxdb`. Mutations: not started | -| TrailBase | eager live source, mutations, on-demand | Eager: GREEN (2026-10-29), per-change commits. Driver `trailbase-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.trailbase`; declares `duplicate-key-delivery` and `source-failure-after-ready`. Mutations, on-demand: not started | -| Convex | eager live source, mutations, on-demand, source shapes | Eager: GREEN (2026-11-02) after a fix, per-batch commits, complete results, over the real Convex JavaScript client and a controlled WebSocket. Driver `convex-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.convex`. Mutations, on-demand, source shapes: not started | -| query-db | client cache and refetch, on-demand, mutations, reload and retry, persistence | Not started; not a push source, so the eager family does not apply | -| local-only | local and shared stores, mutations | Not started | -| localStorage | local and shared stores, mutations | Not started | -| Persisted wrapper | persistence wrapper | Not started | -| Storage hosts (8) | storage hosts | Not started | +| Adapter | Families that apply (section A) | Driver status | +| ----------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Neon Realtime | eager live source, mutations | Eager: RED (READY-4, HEALTH-1, and LIFE-10 behind them); adapter update pending | +| Electric | eager live source, mutations, on-demand, source shapes, reload and retry, persistence | Eager: GREEN (2026-10-30) after three fixes, per-batch commits, typed Postgres columns, over the installed client. Driver `electric-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.electric`. Other families: not started | +| PowerSync | eager live source, mutations, on-demand | Eager: GREEN (2026-10-30) after a fix, per-batch commits, over real local SQLite. Driver `powersync-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.powersync`. Mutations, on-demand: not started | +| RxDB | eager live source, mutations | Eager: GREEN (2026-10-29) after a fix, per-change commits, `listen-before-fetch`. Driver `rxdb-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.rxdb`. Mutations: not started | +| TrailBase | eager live source, mutations, on-demand | Eager: GREEN (2026-10-29), per-change commits. Driver `trailbase-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.trailbase`; declares `duplicate-key-delivery` and `source-failure-after-ready`. Mutations, on-demand: not started | +| Convex | eager live source, mutations, on-demand, source shapes, persistence | Eager: GREEN (2026-11-02) after a fix, per-batch commits, complete results, over the real Convex JavaScript client and a controlled WebSocket. Driver `convex-db-collection/tests/conformance-driver.ts`, property `adapter-conformance.eager-live-source.convex`. Persistence startup: GREEN (2026-11-09), complete snapshots, controlled/Node SQLite/OPFS. Mutations, on-demand, source shapes: not started | +| query-db | client cache and refetch, on-demand, mutations, reload and retry, persistence | Persistence startup: GREEN (2026-11-09), all five origins, scoped peers, controlled/Node SQLite/OPFS. Other families not started; not a push source, so the eager family does not apply | +| local-only | local and shared stores, mutations | Not started | +| localStorage | local and shared stores, mutations | Not started | +| Persisted wrapper | persistence wrapper | Startup replacement: GREEN through Query and Convex; other laws not started | +| Storage hosts (8) | storage hosts | Startup receivers: core SQLite on Node and Browser worker/OPFS; remaining host/path laws not started | ## Suggested order @@ -499,3 +499,32 @@ error/sync-give-up, accept]`). Fix: remember that give-up and fail the replay, or skip the check on the React path, are rejected. A fixed-seed campaign now runs the React client. - **Size:** convex-db-collection +26 gzip bytes for LIFE-10. + +- **2026-11-09, shared persistence startup, Query and Convex.** Maintainer + decision: move persisted startup replacement to the shared persistence family. + The bare eager family retains complete replacement, but does not generate a + persisted restore. Scope ownership remains a specialized Query law. + - **Migration parity:** Query's eight fixed histories, five response origins, + peer axis, both fetched-response schedules, exact owner comparisons, + startup public/durable checks before refetch, refetch-fulfillment check, + durable/reopen checks, and one-full-row-read law all move unchanged to the + shared composition. Fixed/random budgets stay 80 per host. The existing + `query.startup-ownership` replay ID stays registered at its new receiver. + The duplicate Query startup model/runner and former OPFS files are removed; + unrelated Query ownership and persisted-direct-write laws stay. + - **Receiving paths:** controlled storage and real Node SQLite for both + adapters; actual Chromium worker/OPFS for eight Query plus four Convex + histories. Convex uses its existing real SDK/socket driver, not a fake sync + adapter. Its collection-wide scope has no independent peer owners. + - **RED/mutants:** original Query source: 18 failures / 3 passes including + calibration on Node, seven startup violations in the OPFS Query histories. + Convex without its first-result truncate: 12 failures / 1 calibration pass + on Node, four startup violations on OPFS. These are comparison failures; + no setup failure or timeout is counted. This closes the previously recorded + no-truncate survivor at the persisted startup boundary. + - **GREEN:** both new receiving suites pass; all 12 OPFS histories pass. + Current Convex needs no production change. The existing Query production + repair is unchanged. + - **Remaining work:** other persisted adapters have no receiving driver yet. + This property does not cover on-demand persistence, reload, concurrent + writers, failed I/O, arbitrary metadata or the rest of `P-*`/`S-*`. diff --git a/packages/db-adapter-conformance/README.md b/packages/db-adapter-conformance/README.md index 62beaab299..cab57b5b64 100644 --- a/packages/db-adapter-conformance/README.md +++ b/packages/db-adapter-conformance/README.md @@ -66,6 +66,41 @@ was reached fewer than five times, so histories that miss a state the laws need do not pass silently. The in-repo campaigns use `createOracleReach` from `packages/db/tests/oracle-reach.ts`. +## Persisted startup conformance + +`src/persisted-startup-oracle.ts` owns complete-result replacement across a +persisted restore. It applies to both push adapters and client-cache adapters. +The bare eager-source family does not generate persisted rows. Replacement +uses the same READY-3/PUB-7 judgment, with scope-local ownership for sources +such as Query and persistence's durable acceptance contract. + +`tests/persisted-startup-driver.ts` composes a real adapter with persistence. +An adapter supplies its Collection, response delivery, refresh, and optional +scoped ownership projection. `tests/persisted-startup-host.ts` supplies +controlled storage and real Node SQLite. The shared suite enforces separate +reach for each host and fixed/random 80-history campaign. It compares startup +public/durable rows before refresh, public rows inside refresh fulfillment, +durable rows after refresh, and a local-only reopen. It also shares the reload +owner's one-row-read law. A scoped adapter checks exact source/peer ownership. + +Query receives the shared property with its five cache/fetch origins and peer +scope. Convex receives it over the real SDK and controlled socket. Both run +under their package's `test:oracles` command. Chromium OPFS receives eight Query +and four Convex histories through the same model, composition, and comparison: + +```sh +pnpm --filter @tanstack/query-db-collection exec vitest run --coverage.enabled=false --typecheck.enabled=false tests/persisted-startup-conformance-oracle.test.ts +pnpm --filter @tanstack/convex-db-collection exec vitest run --coverage.enabled=false --typecheck.enabled=false tests/persisted-startup-conformance-oracle.property.test.ts +pnpm --filter @tanstack/browser-db-sqlite-persistence exec playwright test --config playwright.opfs.config.ts persisted-startup-conformance-oracle.opfs.spec.ts +``` + +Restoring the original Query implementation fails startup at these receiving +boundaries. Removing Convex's first-result truncate also fails here; it survived +the bare eager grammar because restart starts with no rows. Current Convex +passes. These witnesses establish the shared replacement law on the exercised +paths, not every adapter or the rest of the persistence family. The coverage +map and [migration log](MIGRATION.md) retain the remaining work. + ## Drivers - **Reference adapter** (`tests/reference-adapter.ts`): a small push adapter diff --git a/packages/db-adapter-conformance/src/persisted-startup-oracle.ts b/packages/db-adapter-conformance/src/persisted-startup-oracle.ts new file mode 100644 index 0000000000..8226c98e9c --- /dev/null +++ b/packages/db-adapter-conformance/src/persisted-startup-oracle.ts @@ -0,0 +1,190 @@ +/** + * Persisted restore cannot weaken a complete source snapshot. At startup and + * refresh fulfillment, public and durable rows must reflect replacement, even + * when the result arrived before restore. A scoped source removes only its own + * ownership; a peer's omitted row survives. A collection-wide source replaces + * every row. Authority: the shared eager-source READY-3/PUB-7 replacement law, + * docs/collections/query-collection.md ("Direct Writes and Query Sync", "Initial + * Data"), and persistence's durable acceptance contract. The maintainer chose + * shared persistence conformance as this crossing's owner on 2026-11-09. + * + * The model abstracts source scopes to `source` and `peer`. These are ownership + * edges, not subscriptions or provider identities. Whole-Collection adapters + * expose rows only; scoped adapters also expose their ownership projection. + * The model has no cache, sync queue, or hydration algorithm. + * + * Limits: three string keys, numeric values, one eager persisted sync run and + * local-only reopen. No on-demand windows, concurrent writers, failed I/O, or + * intermediate-publication claim. Drivers and host controls live in + * ../tests/persisted-startup-driver.ts and persisted-startup-host.ts. + */ +import fc from 'fast-check' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { expectedEagerRowScans } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' + +export type PersistedStartupHistory = { + previous: number + next: number + peer: boolean + response: `before-restore` | `after-restore` +} +export type StartupRow = { id: string; value: number } +export type StartupScope = `collection` | `scoped` +export type StartupSnapshot = { + rows: Array + owners?: Array<[string, Array]> +} +export type PersistedStartupObservation = { + responseBeforeRestore: boolean + startupRowReads: number + startup: StartupSnapshot + startupDurable: StartupSnapshot + refresh: StartupSnapshot + durable: StartupSnapshot + reopened: Array +} + +export const startupRows = (mask: number, value: number): Array => + [`a`, `b`, `c`].flatMap((id, index) => + mask & (1 << index) ? [{ id, value }] : [], + ) +export const sortedStartupRows = ( + values: Iterable, +): Array => + Array.from(values, ({ id, value }) => ({ id, value })).sort((a, b) => + a.id.localeCompare(b.id), + ) + +// Replacement is set algebra: keep the complete answer, plus omitted rows +// supported by a surviving peer. An overlapping answer supplies the new value. +export function expectedPersistedStartup( + h: PersistedStartupHistory, + scope: StartupScope, +): StartupSnapshot { + if (h.peer && scope === `collection`) + throw new Error(`A collection-wide source has no independent peer scope`) + const answer = startupRows(h.next, 2) + const next = new Map(answer.map((row) => [row.id, row])) + const peerKey = h.peer ? startupRows(h.previous, 1)[0]!.id : undefined + if (peerKey && !next.has(peerKey)) + next.set(peerKey, { id: peerKey, value: 1 }) + return { + rows: sortedStartupRows(next.values()), + ...(scope === `scoped` + ? { + owners: [...next.keys()] + .sort() + .map((key): [string, Array] => [ + key, + [ + ...(answer.some((row) => row.id === key) ? [`source`] : []), + ...(key === peerKey ? [`peer`] : []), + ].sort(), + ]), + } + : {}), + } +} + +// Nonempty durable baselines and any next subset include omissions, an empty +// answer, equal membership, and changed values on overlap. Dropping the timing +// axis loses the startup race; dropping peers admits a scoped global truncate. +// A cached result is already available, so its receiving grammar restricts it +// to before-restore. Duplicate keys and collection-wide peers are not legal. +export const persistedStartupMembershipArb = (scope: StartupScope) => + fc.record({ + previous: fc.integer({ min: 1, max: 7 }), + next: fc.oneof(fc.constant(0), fc.integer({ min: 0, max: 7 })), + peer: scope === `scoped` ? fc.boolean() : fc.constant(false), + }) +export const persistedStartupHistoryArb = ( + scope: StartupScope, +): fc.Arbitrary => + fc + .tuple( + persistedStartupMembershipArb(scope), + fc.constantFrom(`before-restore` as const, `after-restore` as const), + ) + .map(([membership, response]) => ({ ...membership, response })) + +// These are inputs to the same judgment as generated histories. Receivers may +// add adapter-specific origins, but never supply hand-written expected answers. +export const persistedStartupHistories: Array = [ + { previous: 1, next: 2, peer: false, response: `before-restore` }, + { previous: 1, next: 0, peer: false, response: `before-restore` }, + { previous: 3, next: 6, peer: false, response: `before-restore` }, + { previous: 3, next: 6, peer: false, response: `after-restore` }, +] + +export class PersistedStartupViolation extends Error { + constructor(readonly violations: Array) { + super(violations.join(`\n`)) + this.name = `PersistedStartupViolation` + } +} + +// Check each cut immediately. A later refresh or cleanup cannot erase a stale +// startup result. Ownership is checked only for a declared scoped source. +export function persistedStartupCutViolations( + h: PersistedStartupHistory, + scope: StartupScope, + cut: string, + actual: StartupSnapshot, +): Array { + const expected = expectedPersistedStartup(h, scope) + return JSON.stringify(actual) === JSON.stringify(expected) + ? [] + : [ + `${cut}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + ] +} +export function persistedStartupPremiseViolations( + h: PersistedStartupHistory, + observation: Pick< + PersistedStartupObservation, + `responseBeforeRestore` | `startupRowReads` + >, +): Array { + const violations: Array = [] + if (observation.responseBeforeRestore !== (h.response === `before-restore`)) + violations.push(`response/restore premise was not reached`) + // Reuse the reload owner's work law: one initial restore, zero reloads. + const expectedReads = expectedEagerRowScans(1, 0) + if (observation.startupRowReads !== expectedReads) + violations.push( + `startup row reads: expected ${expectedReads}, got ${observation.startupRowReads}`, + ) + return violations +} +export function persistedStartupViolations( + h: PersistedStartupHistory, + scope: StartupScope, + observation: PersistedStartupObservation, +): Array { + const violations = persistedStartupPremiseViolations(h, observation) + for (const cut of [ + `startup`, + `startupDurable`, + `refresh`, + `durable`, + ] as const) + violations.push( + ...persistedStartupCutViolations(h, scope, cut, observation[cut]), + ) + const expected = expectedPersistedStartup(h, scope) + if (JSON.stringify(observation.reopened) !== JSON.stringify(expected.rows)) + violations.push( + `reopen: expected ${JSON.stringify(expected.rows)}, got ${JSON.stringify(observation.reopened)}`, + ) + return violations +} + +declareOracle({ + model: [expectedPersistedStartup], + grammar: [persistedStartupMembershipArb, persistedStartupHistoryArb], + check: [ + persistedStartupCutViolations, + persistedStartupPremiseViolations, + persistedStartupViolations, + ], +}) diff --git a/packages/db-adapter-conformance/tests/memory-persistence.ts b/packages/db-adapter-conformance/tests/memory-persistence.ts new file mode 100644 index 0000000000..6125941fd9 --- /dev/null +++ b/packages/db-adapter-conformance/tests/memory-persistence.ts @@ -0,0 +1,92 @@ +// Controlled persistence storage shared by adapter receiving drivers. This +// supplies storage facts, never expected source replacement results. +import type { + PersistedStorageTarget, + PersistedTx, +} from '../../db-sqlite-persistence-core/src' + +type Row = { id: string; value: number } + +/** Narrow a stored value and key to this oracle's row shape. */ +export function asRow(value: Record): Row { + if (typeof value.id !== `string` || typeof value.value !== `number`) { + throw new Error(`unexpected stored row: ${JSON.stringify(value)}`) + } + return { ...value, id: value.id, value: value.value } +} + +export function asKey(key: string | number): string { + if (typeof key !== `string`) throw new Error(`unexpected key: ${key}`) + return key +} + +export function createAdapter(seed: Array) { + const rows = new Map(seed.map((row) => [row.id, row])) + const rowMetadata = new Map() + const collectionMetadata = new Map() + let latestTerm = 0 + let latestSeq = 0 + let latestRowVersion = 0 + const entries = () => + Array.from(rows.values()).map((value) => ({ + key: value.id, + value, + metadata: rowMetadata.get(value.id), + })) + return { + rows, + loadSubset: async () => entries(), + loadResumeSnapshot: async ( + _target: PersistedStorageTarget, + options?: { includeRows?: boolean }, + ) => ({ + rows: options?.includeRows === false ? [] : entries(), + keySet: { status: `consistent` as const }, + collectionMetadata: Array.from( + collectionMetadata.entries(), + ([key, value]) => ({ key, value }), + ), + latestTerm, + latestSeq, + latestRowVersion, + resetEpoch: 0, + }), + loadCollectionMetadata: async () => + Array.from(collectionMetadata.entries()).map(([key, value]) => ({ + key, + value, + })), + scanRows: async () => entries(), + applyCommittedTx: async ( + _target: PersistedStorageTarget, + tx: PersistedTx, + ) => { + if (tx.truncate) { + rows.clear() + rowMetadata.clear() + } + for (const mutation of tx.mutations) { + const key = asKey(mutation.key) + if (mutation.type === `delete`) { + rows.delete(key) + rowMetadata.delete(key) + } else { + rows.set(key, asRow(mutation.value)) + } + } + for (const mutation of tx.rowMetadataMutations ?? []) { + const key = asKey(mutation.key) + if (mutation.type === `delete`) rowMetadata.delete(key) + else rowMetadata.set(key, mutation.value) + } + for (const mutation of tx.collectionMetadataMutations ?? []) { + if (mutation.type === `delete`) collectionMetadata.delete(mutation.key) + else collectionMetadata.set(mutation.key, mutation.value) + } + latestTerm = tx.term + latestSeq = tx.seq + latestRowVersion = tx.rowVersion + }, + ensureIndex: async () => {}, + } +} diff --git a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts new file mode 100644 index 0000000000..0541d4fb7b --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts @@ -0,0 +1,201 @@ +/** + * Shared production composition for persisted-startup-oracle.ts. The adapter + * supplies a real Collection, delivery control, and (for scoped sources) its + * metadata projection. The host supplies storage and holds the first row read. + * Seed through public persisted sync, then compare before any refresh, inside + * refresh fulfillment, after its durable read, and after a local-only reopen. + */ +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { + PersistedStartupViolation, + persistedStartupCutViolations, + persistedStartupPremiseViolations, + persistedStartupViolations, + sortedStartupRows, + startupRows, +} from '../src/persisted-startup-oracle' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' +import type { HeldCall } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' +import type { + PersistedStartupHistory, + PersistedStartupObservation, + StartupRow, + StartupScope, + StartupSnapshot, +} from '../src/persisted-startup-oracle' + +export function isStartupRowRead(sql: string): boolean { + return sql + .replace(/\s+/g, ` `) + .trim() + .startsWith(`SELECT key, value, metadata, row_version FROM`) +} + +export interface PersistedStartupSource { + collection: { + preload: () => Promise + cleanup: () => Promise + readonly size: number + values: () => IterableIterator + } + snapshot: () => StartupSnapshot + deliver: () => Promise + responseAvailable: () => boolean + refresh: () => Promise + dispose: () => unknown + checkStartup?: () => Array +} +export interface PersistedStartupAdapter { + scope: StartupScope + seedMetadata?: (h: H, id: string, row: StartupRow) => Record + owners?: (metadata: unknown, id: string) => Array + create: ( + persistence: PersistedCollectionPersistence, + h: H, + id: string, + ) => PersistedStartupSource | Promise +} +export const startupTurn = () => + new Promise((resolve) => setTimeout(resolve, 0)) +export async function untilStartup(premise: () => boolean): Promise { + for (let attempt = 0; attempt < 2_000; attempt++) { + if (premise()) return + await startupTurn() + } + throw new Error(`Persisted startup driver did not reach its premise`) +} + +export async function observePersistedStartup< + H extends PersistedStartupHistory, +>( + adapter: PersistedStartupAdapter, + persistence: PersistedCollectionPersistence, + holdBaseline: (id: string) => HeldCall, + h: H, + id: string, + readRowCount: () => number, + onStartup: () => void = () => {}, +): Promise { + const seed = createCollection( + persistedCollectionOptions({ + id, + getKey: (row) => row.id, + persistence, + sync: { + sync: ({ begin, write, metadata, commit, markReady }) => { + begin() + for (const row of startupRows(h.previous, 1)) { + write({ type: `insert`, value: row }) + if (adapter.seedMetadata) + metadata!.row.set(row.id, adapter.seedMetadata(h, id, row)) + } + commit() + markReady() + }, + }, + }), + ) + await withOracleCleanup(() => seed.preload(), [() => seed.cleanup()]) + const initialReadCount = readRowCount() + const hold = holdBaseline(id) + let source: PersistedStartupSource | undefined + return withOracleCleanup(async () => { + source = await adapter.create(persistence, h, id) + const current = source + const assertCut = (cut: string, actual: StartupSnapshot) => { + const violations = persistedStartupCutViolations( + h, + adapter.scope, + cut, + actual, + ) + if (violations.length) throw new PersistedStartupViolation(violations) + } + const durableSnapshot = async (): Promise => { + const stored = await persistence.adapter.loadResumeSnapshot( + { kind: `eager`, collectionId: id }, + { includeRows: true }, + ) + return { + rows: sortedStartupRows( + stored.rows.map((row) => row.value as StartupRow), + ), + ...(adapter.scope === `scoped` + ? { + owners: [...stored.rows] + .sort((a, b) => String(a.key).localeCompare(String(b.key))) + .map((row): [string, Array] => [ + String(row.key), + adapter.owners!(row.metadata, id), + ]), + } + : {}), + } + } + const ready = current.collection.preload() + await untilStartup(hold.isHeld) + if (h.response === `before-restore`) await current.deliver() + await startupTurn() + const responseBeforeRestore = current.responseAvailable() && hold.isHeld() + hold.release() + if (h.response === `after-restore`) { + await untilStartup(() => current.collection.size > 0) + await current.deliver() + } + await ready + const startup = current.snapshot() + if (responseBeforeRestore !== (h.response === `before-restore`)) + throw new Error(`Startup response/restore premise was not reached`) + onStartup() + assertCut(`startup`, startup) + // Work is captured before the observation's own durable read. + const startupRowReads = readRowCount() - initialReadCount + const premiseViolations = [ + ...persistedStartupPremiseViolations(h, { + responseBeforeRestore, + startupRowReads, + }), + ...(current.checkStartup?.() ?? []), + ] + if (premiseViolations.length) + throw new PersistedStartupViolation(premiseViolations) + const startupDurable = await durableSnapshot() + assertCut(`startupDurable`, startupDurable) + const refresh = await current.refresh().then(() => { + const captured = current.snapshot() + assertCut(`refresh`, captured) + return captured + }) + const durable = await durableSnapshot() + assertCut(`durable`, durable) + await current.collection.cleanup() + const reopened = createCollection( + persistedCollectionOptions({ + id, + getKey: (row) => row.id, + persistence, + }), + ) + return withOracleCleanup(async () => { + await reopened.preload() + const observation = { + responseBeforeRestore, + startupRowReads, + startup, + startupDurable, + refresh, + durable, + reopened: sortedStartupRows(reopened.values()), + } + const violations = persistedStartupViolations( + h, + adapter.scope, + observation, + ) + if (violations.length) throw new PersistedStartupViolation(violations) + return observation + }, [() => reopened.cleanup()]) + }, [() => hold.uninstall(), () => source?.dispose()]) +} diff --git a/packages/db-adapter-conformance/tests/persisted-startup-host.ts b/packages/db-adapter-conformance/tests/persisted-startup-host.ts new file mode 100644 index 0000000000..b83e1d2cd4 --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-startup-host.ts @@ -0,0 +1,96 @@ +// Controlled and real Node SQLite hosts for the same startup production path. +// Full row reads are counted before the driver's own diagnostic durable read. +import { DatabaseSync } from 'node:sqlite' +import { createSQLiteCorePersistenceAdapter } from '../../db-sqlite-persistence-core/src' +import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' +import { BrowserWASQLiteDriver } from '../../browser-db-sqlite-persistence/src/wa-sqlite-driver' +import { createAdapter } from './memory-persistence' +import { isStartupRowRead } from './persisted-startup-driver' +import type { SQLInputValue } from 'node:sqlite' + +export function createStartupHost(host: `controlled` | `node-sqlite`) { + const database = + host === `node-sqlite` ? new DatabaseSync(`:memory:`) : undefined + let rowReads = 0 + const adapter = createAdapter([]) + const loadSnapshot = adapter.loadResumeSnapshot + adapter.loadResumeSnapshot = (...args) => { + if (args[1]?.includeRows !== false) rowReads++ + return loadSnapshot(...args) + } + const scanRows = adapter.scanRows + adapter.scanRows = (...args) => { + rowReads++ + return scanRows(...args) + } + const persistence = database + ? { + adapter: createSQLiteCorePersistenceAdapter({ + driver: new BrowserWASQLiteDriver({ + database: { + execute: ( + sql: string, + params: ReadonlyArray = [], + ) => { + if (isStartupRowRead(sql)) rowReads++ + if ( + !/^\s*(select|with|pragma)/i.test(sql) && + params.length === 0 + ) { + database.exec(sql) + return Promise.resolve([]) + } + return Promise.resolve( + database + .prepare(sql) + .all( + ...(params.map((value) => + value === undefined + ? null + : typeof value === `boolean` + ? Number(value) + : value, + ) as Array), + ) as Array, + ) + }, + }, + }), + }), + } + : { adapter } + const holdBaseline = database + ? inProcessSeams.holdFirstRowSnapshot + : () => { + const original = adapter.loadResumeSnapshot + let release!: () => void + const gate = new Promise((resolve) => { + release = resolve + }) + let held = false + let used = false + adapter.loadResumeSnapshot = async (...args) => { + if (!used && args[1]?.includeRows !== false) { + used = true + held = true + await gate + held = false + } + return original(...args) + } + return { + isHeld: () => held, + release, + uninstall: () => { + release() + adapter.loadResumeSnapshot = original + }, + } + } + return { + persistence, + holdBaseline, + readRowCount: () => rowReads, + close: () => database?.close(), + } +} diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts new file mode 100644 index 0000000000..271e0c0c4c --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts @@ -0,0 +1,163 @@ +/** + * Receiving registration for persisted-startup-oracle.ts. Every adapter runs + * its legal grammar through both storage hosts, at the same four cuts and + * reopen. Fixed histories and generated histories share driver and judgment. + * Each host/campaign enforces its own reach; one cannot cover another's gap. + */ +import fc from 'fast-check' +import { afterAll, describe, expect, it } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { + oracleFixedSeed, + oraclePropertyOptions, + oracleReplayTest, + oracleRoute, + oracleRouteOf, + oracleRuns, +} from '../../db/tests/oracle-config' +import { + expectedPersistedStartup, + persistedStartupCutViolations, + persistedStartupViolations, +} from '../src/persisted-startup-oracle' +import { observePersistedStartup } from './persisted-startup-driver' +import { createStartupHost } from './persisted-startup-host' +import type { Arbitrary } from 'fast-check' +import type { PersistedStartupHistory } from '../src/persisted-startup-oracle' +import type { PersistedStartupAdapter } from './persisted-startup-driver' + +declareOracle({ + check: [persistedStartupCutViolations, persistedStartupViolations], +}) + +export function registerPersistedStartupConformance< + H extends PersistedStartupHistory, +>({ + property, + adapter, + histories, + arbitrary, + origins = [], + origin = () => undefined, +}: { + property: string + adapter: PersistedStartupAdapter + histories: Array + arbitrary: Arbitrary + origins?: Array + origin?: (history: H) => string | undefined +}) { + // A later refresh must not make a stale startup durable result acceptable. + oracleReplayTest(it)( + `rejects stale durability before a later refresh`, + () => { + const history = histories[0]! + const stale = expectedPersistedStartup( + { ...history, next: history.previous }, + adapter.scope, + ) + expect( + persistedStartupCutViolations( + history, + adapter.scope, + `startupDurable`, + stale, + ), + ).not.toEqual([]) + }, + ) + const states = [ + `response-before-restore`, + `restore-before-response`, + `empty-result`, + `overlapping-update`, + `omitted-row`, + ...(adapter.scope === `scoped` ? [`peer-owner`] : []), + ...origins, + ] + for (const host of [`controlled`, `node-sqlite`] as const) { + describe(`${property} through ${host}`, () => { + const reach = createOracleReach(`${property}.${host}`, states) + afterAll(() => { + reach.assertReached() + process.stdout.write( + `${property}.${host} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }) + let sequence = 0 + async function check(history: H, historyReach = reach) { + historyReach.beginHistory() + const storage = createStartupHost(host) + try { + const observation = await observePersistedStartup( + adapter, + storage.persistence, + storage.holdBaseline, + history, + `startup-${host}-${sequence++}`, + storage.readRowCount, + () => { + historyReach.reach( + history.response === `before-restore` + ? `response-before-restore` + : `restore-before-response`, + ) + if (history.next === 0) historyReach.reach(`empty-result`) + if (history.next & history.previous) + historyReach.reach(`overlapping-update`) + if (history.previous & ~history.next) + historyReach.reach(`omitted-row`) + if (history.peer) historyReach.reach(`peer-owner`) + const responseOrigin = origin(history) + if (responseOrigin) historyReach.reach(responseOrigin) + }, + ) + expect( + persistedStartupViolations(history, adapter.scope, observation), + ).toEqual([]) + } finally { + storage.close() + } + } + oracleReplayTest(it).each(histories)( + `reconciles persisted startup: %j`, + async (history) => { + oracleRoute(`fixed history`) + await check(history) + }, + ) + for (const campaign of [`fixed`, `random`] as const) { + const register = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + register( + `reconciles generated startup histories (${campaign})`, + async () => { + const parameters = + campaign === `fixed` + ? oracleFixedSeed(2367, { numRuns: oracleRuns(80) }) + : oraclePropertyOptions(80, property) + oracleRoute(oracleRouteOf(parameters)) + const generatedReach = createOracleReach( + `${property}.${host}.${campaign}`, + states, + ) + await fc.assert( + fc.asyncProperty(arbitrary, (history) => + check(history, generatedReach), + ), + parameters, + ) + generatedReach.assertReached() + process.stdout.write( + `${property}.${host}.${campaign} ${JSON.stringify(Object.fromEntries(generatedReach.counts()))}\n`, + ) + }, + 60_000, + ) + } + }) + } +} diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 569ca6fc1e..88492996a3 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -330,6 +330,7 @@ const staticOracleProperties = [ `persisted-demand-coverage.required`, `sqlite-reload.class`, `query.startup-ownership`, + `adapter.convex.persisted-startup`, `browser.multi-tab-reserved-history`, `browser.electric-source-prefix`, `sqlite-reload.class.electron`, diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index 5b9a4135f8..4448c6b709 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -420,7 +420,12 @@ const ownerGroups: ReadonlyArray< // Owners of properties that a module names and a host file runs carry // that module as the fourth element. [ - `query-db-collection/tests/ownership-lifecycle.oracle.test.ts`, + `convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts`, + `adapter.convex`, + `persisted-startup`, + ], + [ + `query-db-collection/tests/persisted-startup-conformance-oracle.test.ts`, `query`, `startup-ownership`, ], @@ -956,10 +961,13 @@ export type OracleReplayManifestEntry = { // Pagination cells carry their property name in the test title so the guard // can select one cell without running the rest of the matrix. const replayTestNames = new Map([ - [ - `query.startup-ownership`, - `reconciles generated startup histories \\(random\\)`, - ], + ...[`query.startup-ownership`, `adapter.convex.persisted-startup`].map( + (property) => + [ + property, + `reconciles generated startup histories \\(random\\)`, + ] as const, + ), [ `includes-work.projection-payload`, `nested source payloads need no metadata descriptor scans \\(random or replayed\\)`, diff --git a/packages/query-db-collection/package.json b/packages/query-db-collection/package.json index d5737b0c09..6d2da5ab31 100644 --- a/packages/query-db-collection/package.json +++ b/packages/query-db-collection/package.json @@ -21,7 +21,7 @@ "dev": "vite build --watch", "lint": "eslint . --fix", "test": "vitest run", - "test:oracles": "vitest run --coverage.enabled=false tests/includes-work-counter-oracle.test.ts tests/ownership-lifecycle.oracle.test.ts tests/persisted-direct-write-window.oracle.test.ts tests/ssr-dehydration-oracle.test.ts tests/cursor-pagination.oracle.test.ts tests/cursor-pagination.cache-oracle.test.ts tests/cursor-pagination.publication-oracle.test.ts tests/cursor-pagination.boundary-oracle.test.ts tests/cursor-pagination-oracle.integration.test.ts", + "test:oracles": "vitest run --coverage.enabled=false tests/includes-work-counter-oracle.test.ts tests/ownership-lifecycle.oracle.test.ts tests/persisted-direct-write-window.oracle.test.ts tests/ssr-dehydration-oracle.test.ts tests/cursor-pagination.oracle.test.ts tests/cursor-pagination.cache-oracle.test.ts tests/cursor-pagination.publication-oracle.test.ts tests/cursor-pagination.boundary-oracle.test.ts tests/cursor-pagination-oracle.integration.test.ts tests/persisted-startup-conformance-oracle.test.ts", "test:e2e": "vitest run --config vitest.e2e.config.ts" }, "type": "module", diff --git a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts index 70698ccf94..72084d0978 100644 --- a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts +++ b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts @@ -1,6 +1,4 @@ -import { DatabaseSync } from 'node:sqlite' import { hashKey, isCancelledError } from '@tanstack/query-core' -import fc from 'fast-check' import { DbClient, IR, @@ -13,14 +11,7 @@ import { } from '@tanstack/db' import { afterAll, afterEach, describe, expect, it, vi } from 'vitest' import { declareOracle } from '../../db/tests/oracle-declaration.js' -import { - oracleFixedSeed, - oraclePropertyOptions, - oracleReplayTest, - oracleRoute, - oracleRouteOf, - oracleRuns, -} from '../../db/tests/oracle-config.js' +import { oracleRoute } from '../../db/tests/oracle-config.js' import { createOracleReach } from '../../db/tests/oracle-reach' import { withOracleCleanup } from '../../db/tests/utils' import { atMutationCheckpoint } from '../../db/tests/mutation-confirmation-support' @@ -38,21 +29,7 @@ import { import { InvalidQueryResultError } from '../src/errors.js' import { queryCollectionOptions } from '../src/query.js' -import { - createSQLiteCorePersistenceAdapter, - persistedCollectionOptions, -} from '../../db-sqlite-persistence-core/src' -import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' -import { BrowserWASQLiteDriver } from '../../browser-db-sqlite-persistence/src/wa-sqlite-driver' -import { createAdapter } from './persisted-direct-write-support' -import { - expectedStartupOwnership, - isStartupRowRead, - observeStartupOwnership, - startupOwnershipCutViolations, - startupOwnershipHistories, - startupOwnershipViolations, -} from './persisted-startup-ownership-oracle' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' import { categorySubset, cleanups, @@ -70,8 +47,6 @@ import { shared, storedItems, } from './ownership-lifecycle-support.js' -import type { StartupOwnershipHistory } from './persisted-startup-ownership-oracle' -import type { SQLInputValue } from 'node:sqlite' import type { MutationConfirmationObservation, MutationConfirmationRow, @@ -6016,234 +5991,3 @@ describe(`query collection ownership lifecycle`, () => { expect(publications).not.toContainEqual([first.id]) }) }) - -// Persisted restore and first Query response are independent participants. This -// generated dimension of the ownership graph crosses their delivery order. -const startupMembershipArb = fc.record({ - previous: fc.integer({ min: 1, max: 7 }), - next: fc.oneof(fc.constant(0), fc.integer({ min: 0, max: 7 })), - peer: fc.boolean(), -}) -const startupOwnershipHistoryArb: fc.Arbitrary = fc - .oneof( - { - weight: 3, - arbitrary: fc.record({ - membership: startupMembershipArb, - origin: fc.constant(`fetch` as const), - response: fc.constantFrom( - `before-restore` as const, - `after-restore` as const, - ), - }), - }, - { - weight: 4, - arbitrary: fc.record({ - membership: startupMembershipArb, - origin: fc.constantFrom( - `initial-disabled` as const, - `initial-pending` as const, - `cached-disabled` as const, - `cached-pending` as const, - ), - response: fc.constant(`before-restore` as const), - }), - }, - ) - .map(({ membership, ...schedule }) => ({ ...membership, ...schedule })) - -declareOracle({ - grammar: [startupOwnershipHistoryArb], - check: [startupOwnershipViolations], -}) - -// Calibration supplies a stale durable answer to the same cut comparison. -// A later refetch cannot erase an already checked startup mismatch. -oracleReplayTest(it)(`rejects stale durability before a later refetch`, () => { - const history = startupOwnershipHistories[0]! - const stale = expectedStartupOwnership({ ...history, next: history.previous }) - expect( - startupOwnershipCutViolations(history, `startupDurable`, stale), - ).not.toEqual([]) -}) - -for (const host of [`controlled`, `node-sqlite`] as const) { - describe(`persisted startup ownership through ${host}`, () => { - const reach = createOracleReach(`query.startup-ownership.${host}`, [ - `response-before-restore`, - `restore-before-response`, - `empty-result`, - `peer-owner`, - `fetch`, - `initial-disabled`, - `initial-pending`, - `cached-disabled`, - `cached-pending`, - ]) - afterAll(() => { - reach.assertReached() - console.info( - `query.startup-ownership.${host}`, - Object.fromEntries(reach.counts()), - ) - }) - let sequence = 0 - async function check( - history: StartupOwnershipHistory, - historyReach = reach, - ) { - historyReach.beginHistory() - const id = `startup-${host}-${sequence++}` - const database = - host === `node-sqlite` ? new DatabaseSync(`:memory:`) : undefined - let rowReads = 0 - const adapter = createAdapter([]) - const loadSnapshot = adapter.loadResumeSnapshot - adapter.loadResumeSnapshot = (...args) => { - if (args[1]?.includeRows !== false) rowReads++ - return loadSnapshot(...args) - } - const scanRows = adapter.scanRows - adapter.scanRows = (...args) => { - rowReads++ - return scanRows(...args) - } - const persistence = database - ? { - adapter: createSQLiteCorePersistenceAdapter({ - driver: new BrowserWASQLiteDriver({ - database: { - execute: ( - sql: string, - params: ReadonlyArray = [], - ) => { - if (isStartupRowRead(sql)) rowReads++ - if ( - !/^\s*(select|with|pragma)/i.test(sql) && - params.length === 0 - ) { - database.exec(sql) - return Promise.resolve([]) - } - return Promise.resolve( - database - .prepare(sql) - .all( - ...(params.map((value) => - value === undefined - ? null - : typeof value === `boolean` - ? Number(value) - : value, - ) as Array), - ) as Array, - ) - }, - }, - }), - }), - } - : { adapter } - const holdBaseline = database - ? inProcessSeams.holdFirstRowSnapshot - : () => { - const original = adapter.loadResumeSnapshot - let release!: () => void - const gate = new Promise((resolve) => { - release = resolve - }) - let held = false - let used = false - adapter.loadResumeSnapshot = async (...args) => { - if (!used && args[1]?.includeRows !== false) { - used = true - held = true - await gate - held = false - } - return original(...args) - } - return { - isHeld: () => held, - release, - uninstall: () => { - release() - adapter.loadResumeSnapshot = original - }, - } - } - try { - const observation = await observeStartupOwnership( - persistence, - holdBaseline, - history, - id, - () => rowReads, - () => { - historyReach.reach(history.origin) - historyReach.reach( - history.response === `before-restore` - ? `response-before-restore` - : `restore-before-response`, - ) - if (history.next === 0) historyReach.reach(`empty-result`) - if (history.peer) historyReach.reach(`peer-owner`) - }, - ) - expect(startupOwnershipViolations(history, observation)).toEqual([]) - } finally { - database?.close() - } - } - oracleReplayTest(it).each(startupOwnershipHistories)( - `reconciles persisted owners: %j`, - async (history) => { - oracleRoute(`fixed history`) - await check(history) - }, - ) - for (const campaign of [`fixed`, `random`] as const) { - const register = - campaign === `fixed` - ? oracleReplayTest(it) - : oracleReplayTest(it, `query.startup-ownership`) - register( - `reconciles generated startup histories (${campaign})`, - async () => { - const parameters = - campaign === `fixed` - ? oracleFixedSeed(2367, { numRuns: oracleRuns(80) }) - : oraclePropertyOptions(80, `query.startup-ownership`) - oracleRoute(oracleRouteOf(parameters)) - const generatedReach = createOracleReach( - `query.startup-ownership.${host}.${campaign}`, - [ - `response-before-restore`, - `restore-before-response`, - `empty-result`, - `peer-owner`, - `fetch`, - `initial-disabled`, - `initial-pending`, - `cached-disabled`, - `cached-pending`, - ], - ) - await fc.assert( - fc.asyncProperty(startupOwnershipHistoryArb, (history) => - check(history, generatedReach), - ), - parameters, - ) - generatedReach.assertReached() - console.info( - `query.startup-ownership.${host}.${campaign}`, - Object.fromEntries(generatedReach.counts()), - ) - }, - 60_000, - ) - } - }) -} diff --git a/packages/query-db-collection/tests/persisted-direct-write-support.ts b/packages/query-db-collection/tests/persisted-direct-write-support.ts index 73a20d1946..1f47212a33 100644 --- a/packages/query-db-collection/tests/persisted-direct-write-support.ts +++ b/packages/query-db-collection/tests/persisted-direct-write-support.ts @@ -5,11 +5,15 @@ import { QueryClient } from '@tanstack/query-core' import { createNodeSQLitePersistence } from '../../node-db-sqlite-persistence/src' import { BetterSqlite3SQLiteDriver } from '../../node-db-sqlite-persistence/src/node-driver' +import { createAdapter } from '../../db-adapter-conformance/tests/memory-persistence' import type { QueryCollectionUtils } from '../src/query' -import type { - PersistedStorageTarget, - PersistedTx, -} from '../../db-sqlite-persistence-core/src' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' + +export { + asRow, + asKey, + createAdapter, +} from '../../db-adapter-conformance/tests/memory-persistence' export type Row = { id: string; value: number } export type Op = `insert` | `update` | `delete` | `upsert` @@ -98,90 +102,6 @@ export async function checked( } } -/** Narrow a stored value and key to this oracle's row shape. */ -export function asRow(value: Record): Row { - if (typeof value.id !== `string` || typeof value.value !== `number`) { - throw new Error(`unexpected stored row: ${JSON.stringify(value)}`) - } - return { ...value, id: value.id, value: value.value } -} - -export function asKey(key: string | number): string { - if (typeof key !== `string`) throw new Error(`unexpected key: ${key}`) - return key -} - -export function createAdapter(seed: Array) { - const rows = new Map(seed.map((row) => [row.id, row])) - const rowMetadata = new Map() - const collectionMetadata = new Map() - let latestTerm = 0 - let latestSeq = 0 - let latestRowVersion = 0 - const entries = () => - Array.from(rows.values()).map((value) => ({ - key: value.id, - value, - metadata: rowMetadata.get(value.id), - })) - return { - rows, - loadSubset: async () => entries(), - loadResumeSnapshot: async ( - _target: PersistedStorageTarget, - options?: { includeRows?: boolean }, - ) => ({ - rows: options?.includeRows === false ? [] : entries(), - keySet: { status: `consistent` as const }, - collectionMetadata: Array.from( - collectionMetadata.entries(), - ([key, value]) => ({ key, value }), - ), - latestTerm, - latestSeq, - latestRowVersion, - resetEpoch: 0, - }), - loadCollectionMetadata: async () => - Array.from(collectionMetadata.entries()).map(([key, value]) => ({ - key, - value, - })), - scanRows: async () => entries(), - applyCommittedTx: async ( - _target: PersistedStorageTarget, - tx: PersistedTx, - ) => { - if (tx.truncate) { - rows.clear() - rowMetadata.clear() - } - for (const mutation of tx.mutations) { - const key = asKey(mutation.key) - if (mutation.type === `delete`) { - rows.delete(key) - rowMetadata.delete(key) - } else { - rows.set(key, asRow(mutation.value)) - } - } - for (const mutation of tx.rowMetadataMutations ?? []) { - const key = asKey(mutation.key) - if (mutation.type === `delete`) rowMetadata.delete(key) - else rowMetadata.set(key, mutation.value) - } - for (const mutation of tx.collectionMetadataMutations ?? []) { - if (mutation.type === `delete`) collectionMetadata.delete(mutation.key) - else collectionMetadata.set(mutation.key, mutation.value) - } - latestTerm = tx.term - latestSeq = tx.seq - latestRowVersion = tx.rowVersion - }, - ensureIndex: async () => {}, - } -} - export type StorageKind = `fake` | `sqlite` /** @@ -193,7 +113,12 @@ export function createStorage( kind: StorageKind, seed: Array, collectionId: string, -) { +): { + adapter: ReturnType + persistence: PersistedCollectionPersistence + storedRows: (id: string) => Promise> + close: () => void | Promise +} { if (kind === `fake`) { const adapter = createAdapter(seed) return { diff --git a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts new file mode 100644 index 0000000000..ab7e2840df --- /dev/null +++ b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts @@ -0,0 +1,65 @@ +/** Query receives the shared persistence law, including its cache origins. + * Already-available initial/cache results cannot be delivered after restore; + * fetched answers exercise both schedules. Pending origins hold queryFn until + * after the startup durable comparison, so refetch cannot conceal the defect. + */ +import { describe } from 'vitest' +import fc from 'fast-check' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { persistedStartupMembershipArb } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' +import { + queryStartupAdapter, + queryStartupHistories, +} from './persisted-startup-driver' +import type { QueryStartupHistory } from './persisted-startup-driver' + +const startupMembershipArb = persistedStartupMembershipArb(`scoped`) +const queryStartupHistoryArb: fc.Arbitrary = fc + .oneof( + { + weight: 3, + arbitrary: fc.record({ + membership: startupMembershipArb, + origin: fc.constant(`fetch` as const), + response: fc.constantFrom( + `before-restore` as const, + `after-restore` as const, + ), + }), + }, + { + weight: 4, + arbitrary: fc.record({ + membership: startupMembershipArb, + origin: fc.constantFrom( + `initial-disabled` as const, + `initial-pending` as const, + `cached-disabled` as const, + `cached-pending` as const, + ), + response: fc.constant(`before-restore` as const), + }), + }, + ) + .map(({ membership, ...schedule }) => ({ ...membership, ...schedule })) + +declareOracle({ grammar: [queryStartupHistoryArb] }) +// Keep registration visible to Vitest's static typecheck collector as well +// as its runtime collector; the shared suite registers the actual law checks. +describe(`persisted startup conformance: Query`, () => { + registerPersistedStartupConformance({ + property: `query.startup-ownership`, + adapter: queryStartupAdapter, + histories: queryStartupHistories, + arbitrary: queryStartupHistoryArb, + origins: [ + `fetch`, + `initial-disabled`, + `initial-pending`, + `cached-disabled`, + `cached-pending`, + ], + origin: (history) => history.origin, + }) +}) diff --git a/packages/query-db-collection/tests/persisted-startup-driver.ts b/packages/query-db-collection/tests/persisted-startup-driver.ts new file mode 100644 index 0000000000..65e39c61ed --- /dev/null +++ b/packages/query-db-collection/tests/persisted-startup-driver.ts @@ -0,0 +1,204 @@ +/** Query receiving path for shared persisted-startup conformance. + * The real QueryClient supplies fetch, initialData, and cached success results. + * Only this driver knows Query hashes, row metadata, and fetching premises. + * The shared model and comparison own replacement, peer survival and durability. + */ +import { QueryClient, hashKey } from '@tanstack/query-core' +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { queryCollectionOptions } from '../src/query' +import { + sortedStartupRows, + startupRows, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import type { + PersistedStartupHistory, + StartupRow, + StartupSnapshot, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' + +export type QueryStartupHistory = PersistedStartupHistory & { + origin: + | `fetch` + | `initial-disabled` + | `initial-pending` + | `cached-disabled` + | `cached-pending` +} +export const queryStartupHistories: Array = [ + { + origin: `fetch`, + previous: 1, + next: 2, + peer: false, + response: `before-restore`, + }, + { + origin: `fetch`, + previous: 1, + next: 0, + peer: false, + response: `before-restore`, + }, + { + origin: `fetch`, + previous: 3, + next: 6, + peer: true, + response: `before-restore`, + }, + { + origin: `fetch`, + previous: 3, + next: 6, + peer: false, + response: `after-restore`, + }, + { + origin: `initial-disabled`, + previous: 1, + next: 2, + peer: false, + response: `before-restore`, + }, + { + origin: `initial-pending`, + previous: 3, + next: 6, + peer: true, + response: `before-restore`, + }, + { + origin: `cached-disabled`, + previous: 1, + next: 0, + peer: false, + response: `before-restore`, + }, + { + origin: `cached-pending`, + previous: 3, + next: 6, + peer: true, + response: `before-restore`, + }, +] + +type QueryMetadata = + { queryCollection?: { owners?: Record } } | undefined +const owners = (metadata: unknown, id: string) => + Object.keys((metadata as QueryMetadata)?.queryCollection?.owners ?? {}) + .map((owner) => (owner === hashKey([id]) ? `source` : owner)) + .sort() + +export const queryStartupAdapter: PersistedStartupAdapter = + { + scope: `scoped`, + seedMetadata: (h, id, row) => ({ + queryCollection: { + owners: { + [hashKey([id])]: true, + ...(h.peer && row.id === startupRows(h.previous, 1)[0]!.id + ? { peer: true } + : {}), + }, + }, + }), + owners, + async create(persistence, h, id) { + let releaseResponse!: () => void + const response = new Promise((resolve) => { + releaseResponse = resolve + }) + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: Infinity, + staleTime: Infinity, + }, + }, + }) + const pending = h.origin.endsWith(`pending`) + const disabled = h.origin.endsWith(`disabled`) + if (h.origin.startsWith(`cached`)) + await client.fetchQuery({ + queryKey: [id], + queryFn: () => Promise.resolve(startupRows(h.next, 2)), + }) + const fetch = { completed: false, calls: 0 } + const unsubscribe = client.getQueryCache().subscribe((event) => { + if (event.type === `updated` && event.action.type === `success`) + fetch.completed = true + }) + const collection = createCollection( + persistedCollectionOptions({ + ...queryCollectionOptions({ + id, + queryKey: [id], + queryClient: client, + getKey: (row) => row.id, + enabled: !disabled, + staleTime: pending ? 0 : Infinity, + ...(h.origin.startsWith(`initial`) + ? { initialData: startupRows(h.next, 2) } + : {}), + queryFn: async () => { + fetch.calls++ + if (pending || h.response === `after-restore`) await response + return startupRows(h.next, 2) + }, + }), + persistence, + }), + ) + const snapshot = (): StartupSnapshot => ({ + rows: sortedStartupRows(collection.values()), + owners: [...collection.keys()] + .sort() + .map((key) => [ + String(key), + owners(collection._state.syncedMetadata.get(key), id), + ]), + }) + return { + collection, + snapshot, + deliver: async () => { + if (h.origin === `fetch`) { + if (h.response === `after-restore`) releaseResponse() + await untilStartup(() => fetch.completed) + } + }, + responseAvailable: () => + h.origin === `fetch` + ? fetch.completed + : client.getQueryData([id]) !== undefined, + checkStartup: () => { + const violations: Array = [] + if (fetch.calls !== (disabled ? 0 : 1)) + violations.push(`startup fetch count did not match ${h.origin}`) + if ( + (client.getQueryState([id])?.fetchStatus === `fetching`) !== + pending + ) + violations.push(`pending fetch premise did not match ${h.origin}`) + return violations + }, + refresh: () => { + releaseResponse() + return collection.utils.refetch() + }, + dispose: () => + withOracleCleanup(async () => {}, [ + releaseResponse, + unsubscribe, + () => collection.cleanup(), + () => client.clear(), + ]), + } + }, + } diff --git a/packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts b/packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts deleted file mode 100644 index 8a094a9474..0000000000 --- a/packages/query-db-collection/tests/persisted-startup-ownership-oracle.ts +++ /dev/null @@ -1,454 +0,0 @@ -/** - * A complete Query result replaces that Query's persisted ownership, including - * rows whose persisted restore has not published yet. Other owners survive. - * Authority: docs/collections/query-collection.md, "Direct Writes and Query - * Sync" (a successful fetch reconciles ownership), and the ownership lifecycle - * oracle. Complete initialData/cache responses have the same materialization - * authority ("Initial Data" and "How Query Collections Work"). Persistence - * changes neither that judgment nor its durable result. Startup work also - * follows reload-class-oracle's one-row-read law; that model is shared below. - * - * This is a dimension of the ownership oracle, not a new ownership policy. - * The model is set replacement: remove the old Query edge, add the response's - * edges, and keep rows with another owner. It has no Query cache or hydration - * machinery. A peer is a persisted owner that this run has not revalidated. - * - * Histories use nonempty prior subsets of three keys, any next subset (including - * empty), optional peer ownership of the first prior key, and response delivery - * before or after restore. Complete initialData and cached responses are - * already available before restore, with fetching disabled or held pending. - * Values change on overlapping keys. Removing timing - * loses the reported race; removing overlap misses stale values; removing peers - * admits truncation. Duplicate keys and a response-before-fetch are excluded. - * - * The shared driver runs through persistedCollectionOptions and real QueryClient. - * A host holds the first baseline read, before its rows reach the wrapper. Query - * cache success proves response delivery while the hold remains active. Public - * rows and durable rows/owners are compared immediately after preload, before - * refetch can repair storage, and again inside/after refetch's fulfillment. - * The work recorder counts full row reads before its observation's own read. A final local-only reopen checks durable removal independently. - * Node SQLite and Chromium OPFS supply the same driver and comparison. This - * does not cover on-demand windows, multi-tab writes, or failed SQLite I/O. - */ -import { QueryClient, hashKey } from '@tanstack/query-core' -import { createCollection } from '@tanstack/db' -import { declareOracle } from '../../db/tests/oracle-declaration' -import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' -import { queryCollectionOptions } from '../src/query' -import { withOracleCleanup } from '../../db/tests/oracle-cleanup' -import { expectedEagerRowScans } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' -import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' -import type { HeldCall } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' - -export function isStartupRowRead(sql: string): boolean { - return sql - .replace(/\s+/g, ` `) - .trim() - .startsWith(`SELECT key, value, metadata, row_version FROM`) -} - -export class StartupOwnershipViolationError extends Error { - constructor(readonly violations: Array) { - super(violations.join(`\n`)) - this.name = `StartupOwnershipViolationError` - } -} - -export type StartupOwnershipHistory = { - origin: - | `fetch` - | `initial-disabled` - | `initial-pending` - | `cached-disabled` - | `cached-pending` - previous: number - next: number - peer: boolean - response: `before-restore` | `after-restore` -} -type Row = { id: string; value: number } -type Snapshot = { rows: Array; owners: Array<[string, Array]> } -export type StartupOwnershipObservation = { - responseBeforeRestore: boolean - startupRowReads: number - startupFetches: number - pendingFetchAtStartup: boolean - startupDurable: Snapshot - startup: Snapshot - refetch: Snapshot - durable: Snapshot - reopened: Array -} - -const rows = (mask: number, value: number): Array => - [`a`, `b`, `c`].flatMap((id, index) => - mask & (1 << index) ? [{ id, value }] : [], - ) -const sorted = (values: Iterable): Array => - Array.from(values, ({ id, value }) => ({ id, value })).sort((a, b) => - a.id.localeCompare(b.id), - ) - -export function expectedStartupOwnership(h: StartupOwnershipHistory): Snapshot { - const next = new Map(rows(h.next, 2).map((row) => [row.id, row])) - const peerKey = h.peer ? rows(h.previous, 1)[0]!.id : undefined - if (peerKey && !next.has(peerKey)) - next.set(peerKey, { id: peerKey, value: 1 }) - return { - rows: sorted(next.values()), - owners: [...next.keys()] - .sort() - .map((key) => [ - key, - [ - ...(rows(h.next, 2).some((row) => row.id === key) ? [`query`] : []), - ...(key === peerKey ? [`peer`] : []), - ].sort(), - ]), - } -} - -// Each captured cut is checked before cleanup can throw. The final receiver -// comparison reuses the same judgment; no fixed history carries an answer. -export function startupOwnershipCutViolations( - h: StartupOwnershipHistory, - cut: string, - actual: Snapshot, -): Array { - const expected = expectedStartupOwnership(h) - return JSON.stringify(actual) === JSON.stringify(expected) - ? [] - : [ - `${cut}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, - ] -} - -export function startupOwnershipPremiseViolations( - h: StartupOwnershipHistory, - observation: Pick< - StartupOwnershipObservation, - | `responseBeforeRestore` - | `startupRowReads` - | `startupFetches` - | `pendingFetchAtStartup` - >, -): Array { - const violations: Array = [] - if (observation.responseBeforeRestore !== (h.response === `before-restore`)) - violations.push(`response/restore premise was not reached`) - const expectedReads = expectedEagerRowScans(1, 0) - if (observation.startupRowReads !== expectedReads) - violations.push( - `startup row reads: expected ${expectedReads}, got ${observation.startupRowReads}`, - ) - const disabled = h.origin.endsWith(`disabled`) - if (observation.startupFetches !== (disabled ? 0 : 1)) - violations.push(`startup fetch count did not match ${h.origin}`) - if (observation.pendingFetchAtStartup !== h.origin.endsWith(`pending`)) - violations.push(`pending fetch premise did not match ${h.origin}`) - return violations -} - -export function startupOwnershipViolations( - h: StartupOwnershipHistory, - observation: StartupOwnershipObservation, -): Array { - const violations = startupOwnershipPremiseViolations(h, observation) - for (const cut of [ - `startup`, - `startupDurable`, - `refetch`, - `durable`, - ] as const) - violations.push(...startupOwnershipCutViolations(h, cut, observation[cut])) - const expected = expectedStartupOwnership(h) - if (JSON.stringify(observation.reopened) !== JSON.stringify(expected.rows)) - violations.push( - `reopen: expected ${JSON.stringify(expected.rows)}, got ${JSON.stringify(observation.reopened)}`, - ) - return violations -} - -export const startupOwnershipHistories: Array = [ - { - origin: `fetch`, - previous: 1, - next: 2, - peer: false, - response: `before-restore`, - }, - { - origin: `fetch`, - previous: 1, - next: 0, - peer: false, - response: `before-restore`, - }, - { - origin: `fetch`, - previous: 3, - next: 6, - peer: true, - response: `before-restore`, - }, - { - origin: `fetch`, - previous: 3, - next: 6, - peer: false, - response: `after-restore`, - }, - { - origin: `initial-disabled`, - previous: 1, - next: 2, - peer: false, - response: `before-restore`, - }, - { - origin: `initial-pending`, - previous: 3, - next: 6, - peer: true, - response: `before-restore`, - }, - { - origin: `cached-disabled`, - previous: 1, - next: 0, - peer: false, - response: `before-restore`, - }, - { - origin: `cached-pending`, - previous: 3, - next: 6, - peer: true, - response: `before-restore`, - }, -] - -const turn = () => new Promise((resolve) => setTimeout(resolve, 0)) -async function until(premise: () => boolean): Promise { - for (let attempt = 0; attempt < 2_000; attempt++) { - if (premise()) return - await turn() - } - throw new Error(`Startup ownership driver did not reach its premise`) -} - -export async function observeStartupOwnership( - persistence: PersistedCollectionPersistence, - holdBaseline: (id: string) => HeldCall, - h: StartupOwnershipHistory, - id: string, - readRowCount: () => number, - onStartup: () => void = () => {}, -): Promise { - const queryHash = hashKey([id]) - const peerKey = h.peer ? rows(h.previous, 1)[0]!.id : undefined - // Seed a valid durable row/owner snapshot through the public sync boundary. - const seed = createCollection( - persistedCollectionOptions({ - id, - getKey: (row) => row.id, - persistence, - sync: { - sync: ({ begin, write, metadata, commit, markReady }) => { - begin() - for (const row of rows(h.previous, 1)) { - write({ type: `insert`, value: row }) - metadata!.row.set(row.id, { - queryCollection: { - owners: { - [queryHash]: true, - ...(row.id === peerKey ? { peer: true } : {}), - }, - }, - }) - } - commit() - markReady() - }, - }, - }), - ) - await seed.preload() - await seed.cleanup() - - const initialReadCount = readRowCount() - const hold = holdBaseline(id) - let releaseResponse!: () => void - const response = new Promise((resolve) => { - releaseResponse = resolve - }) - const client = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - gcTime: Infinity, - staleTime: Infinity, - }, - }, - }) - const pendingOrigin = h.origin.endsWith(`pending`) - const disabledOrigin = h.origin.endsWith(`disabled`) - if (h.origin.startsWith(`cached`)) - await client.fetchQuery({ - queryKey: [id], - queryFn: () => Promise.resolve(rows(h.next, 2)), - }) - const fetch = { completed: false, calls: 0 } - const unsubscribe = client.getQueryCache().subscribe((event) => { - if (event.type === `updated` && event.action.type === `success`) - fetch.completed = true - }) - const collection = createCollection( - persistedCollectionOptions({ - ...queryCollectionOptions({ - id, - queryKey: [id], - queryClient: client, - getKey: (row) => row.id, - enabled: !disabledOrigin, - staleTime: pendingOrigin ? 0 : Infinity, - ...(h.origin.startsWith(`initial`) - ? { initialData: rows(h.next, 2) } - : {}), - queryFn: async () => { - fetch.calls++ - if (pendingOrigin || h.response === `after-restore`) await response - return rows(h.next, 2) - }, - }), - persistence, - }), - ) - const snapshot = (): Snapshot => ({ - rows: sorted(collection.values()), - owners: [...collection.keys()].sort().map((key) => { - const metadata = collection._state.syncedMetadata.get(key) as - { queryCollection?: { owners?: Record } } | undefined - return [ - String(key), - Object.keys(metadata?.queryCollection?.owners ?? {}) - .map((owner) => (owner === queryHash ? `query` : owner)) - .sort(), - ] - }), - }) - const assertCut = (cut: string, actual: Snapshot) => { - const violations = startupOwnershipCutViolations(h, cut, actual) - if (violations.length) throw new StartupOwnershipViolationError(violations) - } - const durableSnapshot = async (): Promise => { - const stored = await persistence.adapter.loadResumeSnapshot( - { kind: `eager`, collectionId: id }, - { includeRows: true }, - ) - return { - rows: sorted(stored.rows.map((row) => row.value as Row)), - owners: [...stored.rows] - .sort((a, b) => String(a.key).localeCompare(String(b.key))) - .map((row) => [ - String(row.key), - Object.keys( - ( - row.metadata as - | { queryCollection?: { owners?: Record } } - | undefined - )?.queryCollection?.owners ?? {}, - ) - .map((owner) => (owner === queryHash ? `query` : owner)) - .sort(), - ]), - } - } - return withOracleCleanup(async () => { - const ready = collection.preload() - await until(hold.isHeld) - if (h.response === `before-restore` && h.origin === `fetch`) - await until(() => fetch.completed) - await turn() - const responseAvailable = - h.origin === `fetch` - ? fetch.completed - : client.getQueryData([id]) !== undefined - const responseBeforeRestore = responseAvailable && hold.isHeld() - hold.release() - if (h.response === `after-restore`) { - await until(() => collection.size > 0) - releaseResponse() - } - await ready - const startup = snapshot() - if (responseBeforeRestore !== (h.response === `before-restore`)) - throw new Error(`Startup response/restore premise was not reached`) - onStartup() - assertCut(`startup`, startup) - // Count startup work before the observation's own durable read. - const startupRowReads = readRowCount() - initialReadCount - const startupFetches = fetch.calls - const pendingFetchAtStartup = - client.getQueryState([id])?.fetchStatus === `fetching` - const premiseViolations = startupOwnershipPremiseViolations(h, { - responseBeforeRestore, - startupRowReads, - startupFetches, - pendingFetchAtStartup, - }) - if (premiseViolations.length) - throw new StartupOwnershipViolationError(premiseViolations) - const startupDurable = await durableSnapshot() - assertCut(`startupDurable`, startupDurable) - releaseResponse() - const refetch = await collection.utils.refetch().then(() => { - const captured = snapshot() - assertCut(`refetch`, captured) - return captured - }) - const durable = await durableSnapshot() - assertCut(`durable`, durable) - await collection.cleanup() - const reopened = createCollection( - persistedCollectionOptions({ - id, - getKey: (row) => row.id, - persistence, - }), - ) - return withOracleCleanup(async () => { - await reopened.preload() - const observation: StartupOwnershipObservation = { - responseBeforeRestore, - startupRowReads, - startupFetches, - pendingFetchAtStartup, - startup, - startupDurable, - refetch, - durable, - reopened: sorted(reopened.values()), - } - const violations = startupOwnershipViolations(h, observation) - if (violations.length) - throw new StartupOwnershipViolationError(violations) - return observation - }, [() => reopened.cleanup()]) - }, [ - () => hold.uninstall(), - releaseResponse, - unsubscribe, - () => collection.cleanup(), - () => client.clear(), - ]) -} - -declareOracle({ - model: [expectedStartupOwnership], - check: [ - startupOwnershipCutViolations, - startupOwnershipPremiseViolations, - startupOwnershipViolations, - ], -}) From 736b167ec196f878288f631aff4e82180d9827a5 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 9 Nov 2026 20:35:33 -0700 Subject: [PATCH 06/24] Record the validated shared startup implementation --- docs/contributing/oracle-reviews/2026-11-09-issue-2367.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md index 16f9c16aba..643722d0ad 100644 --- a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md +++ b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md @@ -613,3 +613,9 @@ pnpm --filter @tanstack/query-db-collection exec vitest run --maxWorkers=2 --cov pnpm --filter @tanstack/convex-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false tests/persisted-startup-conformance-oracle.property.test.ts pnpm --filter @tanstack/browser-db-sqlite-persistence exec playwright test --config playwright.opfs.config.ts persisted-startup-conformance-oracle.opfs.spec.ts ``` + + +The reviewed migration implementation is commit `662a41ad68a0847e8123e80cdc37bf2e17910a2a`. +All final evidence above applies to that implementation. The following record +update changes prose only. Query production remains blob +`48b4624f5b179dd5b5e380b01161d510827c318c`; no Convex production file changed. From a0d7d49e54d820ee342443fdb10f74dab23a55db Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 07:44:23 -0700 Subject: [PATCH 07/24] Fix persisted eager startup across collection adapters --- .changeset/fix-persisted-query-startup.md | 5 +- docs/contributing/oracle-coverage.md | 42 ++-- .../2026-11-10-persisted-startup-adapters.md | 199 ++++++++++++++++++ ...ed-startup-conformance-oracle.opfs.spec.ts | 13 +- .../e2e/persisted-startup-conformance.opfs.ts | 16 ++ packages/db-adapter-conformance/MIGRATION.md | 22 ++ packages/db/tests/oracle-config.ts | 4 + packages/db/tests/oracle-replay-manifest.ts | 15 +- packages/electric-db-collection/package.json | 2 +- ...tartup-conformance-oracle.property.test.ts | 23 ++ .../tests/persisted-startup-driver.ts | 130 ++++++++++++ packages/powersync-db-collection/package.json | 1 + .../powersync-db-collection/src/powersync.ts | 8 +- .../tests/persisted-startup-browser.ts | 23 ++ .../tests/persisted-startup-common.ts | 85 ++++++++ ...tartup-conformance-oracle.property.test.ts | 23 ++ .../tests/persisted-startup-driver.ts | 36 ++++ packages/rxdb-db-collection/src/rxdb.ts | 10 +- ...tartup-conformance-oracle.property.test.ts | 23 ++ .../tests/persisted-startup-driver.ts | 98 +++++++++ packages/trailbase-db-collection/package.json | 2 +- .../trailbase-db-collection/src/trailbase.ts | 10 + ...tartup-conformance-oracle.property.test.ts | 23 ++ .../tests/persisted-startup-driver.ts | 72 +++++++ pnpm-lock.yaml | 32 +++ 25 files changed, 890 insertions(+), 27 deletions(-) create mode 100644 docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md create mode 100644 packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts create mode 100644 packages/electric-db-collection/tests/persisted-startup-driver.ts create mode 100644 packages/powersync-db-collection/tests/persisted-startup-browser.ts create mode 100644 packages/powersync-db-collection/tests/persisted-startup-common.ts create mode 100644 packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts create mode 100644 packages/powersync-db-collection/tests/persisted-startup-driver.ts create mode 100644 packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts create mode 100644 packages/rxdb-db-collection/tests/persisted-startup-driver.ts create mode 100644 packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts create mode 100644 packages/trailbase-db-collection/tests/persisted-startup-driver.ts diff --git a/.changeset/fix-persisted-query-startup.md b/.changeset/fix-persisted-query-startup.md index 4631a5de0a..f6614bf3ee 100644 --- a/.changeset/fix-persisted-query-startup.md +++ b/.changeset/fix-persisted-query-startup.md @@ -1,5 +1,8 @@ --- '@tanstack/query-db-collection': patch +'@tanstack/rxdb-db-collection': patch +'@tanstack/powersync-db-collection': patch +'@tanstack/trailbase-db-collection': patch --- -Remove obsolete persisted Query-owned rows on eager startup, including when initial data or cached results arrive before persisted restore. Preserve rows owned by other Queries. +Remove obsolete persisted rows when an eager source's first complete result overlaps SQLite restore. Query Collection also handles initial data and cached results while preserving rows owned by other Queries. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index f3350daab7..affbab3a38 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -7731,10 +7731,17 @@ complete-result and scope-local omission contract, and persistence's durable acceptance contract. The maintainer selected this shared owner on 2026-11-09. The independent set model has no Query cache, source diff, or restore algorithm. -[Query](../../packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts) -and [Convex](../../packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts) -are real receiving paths: a QueryClient and a Convex JavaScript SDK over a -controlled protocol socket. Each uses the same +[Query](../../packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts), +[Convex](../../packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts), +[Electric](../../packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts), +[TrailBase](../../packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts), +[RxDB](../../packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts) +and [PowerSync](../../packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts) +supply receiving paths. Query uses QueryClient; Convex uses its SDK and a +controlled socket; Electric uses ShapeStream and controlled HTTP responses; +TrailBase uses a controlled RecordApi and native stream; RxDB uses real memory +storage; PowerSync uses real local SQLite through its Node and browser SDKs. +Each uses the same [composition driver](../../packages/db-adapter-conformance/tests/persisted-startup-driver.ts), judgment, controlled storage, and real Node SQLite hosts. Per host, fixed and random campaigns each run 80 generated histories with separate enforced reach. @@ -7751,24 +7758,33 @@ snapshots also check exact ownership at all four snapshot cuts. Startup checks the reload owner's one-full-row-read law before its own durable observation. Query's receiving driver preserves exact fetch-count and pending-fetch premises. The [Chromium OPFS receiver](../../packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts) -runs all eight previous Query histories and four complete-snapshot Convex -histories through the same composition and check on the real worker/OPFS host. +runs eight Query histories and four complete-result histories for each of the +other five adapters through the same composition and check on real worker/OPFS. The original Query omission fails startup on both Node hosts and OPFS. Convex without its first-result truncate, a survivor in the bare eager-source grammar, now fails the same comparison on all three hosts. Current Convex passes; this -migration confirms no new Convex defect. The original Query baseline-await -repair stays at its adapter boundary. The old Query-only startup model/runner +migration confirms no new Convex defect. Electric also passes unchanged. +The new receiving campaigns expose startup violations in RxDB, PowerSync and +TrailBase before production edits, on both Node hosts and real OPFS. RxDB and +PowerSync select snapshots for pending persisted baselines; TrailBase hydrates +before its complete eager-list omission sweep. Unwrapped snapshot selection +and TrailBase's event fence remain. The original Query baseline-await repair +stays at its adapter boundary. The old Query-only startup model/runner and OPFS files are retired after receiving parity; Query-specific ownership laws and the unrelated direct-write/failure-fidelity repairs remain. Limits: eager startup, three string keys and numeric values, one persistence -instance, one source scope plus an optional persisted peer. No on-demand window, +instance, one source scope plus an optional persisted peer, and no mutation +during the initial fetch. No on-demand window, concurrent tab writer, arbitrary metadata namespace, failed-I/O, or every -intermediate-publication claim. Other adapters' persisted receiving paths remain -open under this shared owner; their bare eager conformance does not establish -persisted startup. See [migration status](../../packages/db-adapter-conformance/MIGRATION.md) -and the [versioned evidence](oracle-reviews/2026-11-09-issue-2367.md). +intermediate-publication claim. Electric's resumed delta-only streams belong to +its recovery owner; a fresh full shape supplies this family's replacement +premise. TrailBase has no deployed-server or SDK-wire claim, and the local RxDB +and PowerSync sources supply no remote-replication claim. See +[migration status](../../packages/db-adapter-conformance/MIGRATION.md), +[original evidence](oracle-reviews/2026-11-09-issue-2367.md) and the +[all-adapter amendment](oracle-reviews/2026-11-10-persisted-startup-adapters.md). ## Adapter conformance: eager live sources (#2118) diff --git a/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md new file mode 100644 index 0000000000..f034129737 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md @@ -0,0 +1,199 @@ +# Persisted startup: all published eager source adapters + +Review base: `736b167ec196f878288f631aff4e82180d9827a5`. +This amendment extends the [prior Query/Convex evidence](2026-11-09-issue-2367.md#shared-persistence-conformance-migration-2026-11-09). +The shared [persisted-startup oracle](../../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts) +keeps its law, independent set model, history grammar, comparisons and 80-history +run budget. Four receiving adapters now join Query and Convex. The earlier +statement that they were optional remaining work was incomplete: the public +SQLite guide supports wrapping another sync adapter in the same way. + +## Law and receiving inventory + +A complete eager source result replaces its persisted baseline at the source's +startup readiness cut. A scoped source replaces only its ownership and preserves +peer-supported rows. The SQLite guide's "Add persistence to a synced Collection" +section promises that the wrapper keeps the adapter's sync behavior. Without +`initialRender`, readiness waits for the source's `markReady()`. READY-3/PUB-7, +Query's scope-local omission law and persistence's durable acceptance contract +therefore also apply when persisted restore overlaps the first complete result. + +| Adapter | Real receiving boundary | Result | +| --- | --- | --- | +| Query | QueryClient, including initialData/cache and scoped ownership | Existing repair retained | +| Convex | Installed JavaScript SDK over the controlled protocol socket | Passes unchanged | +| Electric | Installed ShapeStream over controlled HTTP responses, fresh offset -1 shape ending with up-to-date | Passes unchanged | +| TrailBase | Controlled RecordApi and native ReadableStream, complete eager list | Parent retains omitted persisted rows | +| RxDB | Real RxDB memory storage and its initial paged query | Parent retains omitted persisted rows | +| PowerSync | Real local PowerSync SQLite table, Node SDK and browser SDK | Parent retains omitted persisted rows | + +All six use controlled wrapper storage and real Node SQLite. All six also run +through the same composition and comparison on real Chromium worker/OPFS. +PowerSync's source SQLite database is separate from the wrapper's OPFS database. +Its browser SDK is a test-only dependency. Fresh Electric shape replacement is +separate from resumed delta streams, which need not delete unmentioned rows. +TrailBase has no SDK-wire or deployed-server claim. Remote RxDB replication and +remote PowerSync replication are outside the local-source boundaries tested. +The external vendored Neon compatibility fixture is not a published TanStack +adapter. Built-in local-only/storage Collections are not another upstream source +for this complete-result family; their own persistence laws retain their owners. + +## Experiment and repair + +Before changing the three production adapters, the new generated receivers ran +against unchanged production on both Node hosts. The initial four-adapter run +had 32 comparison failures and 20 passes: RxDB failed 12 of 13 checks, +TrailBase 10 of 13, and PowerSync 10 of 13. Electric passed all 13. The real +OPFS receiver then ran before the production repairs, including PowerSync's +browser SDK, and rejected 10 startup snapshots: RxDB four, TrailBase three, +PowerSync three. Setup failures during dependency recovery are not RED evidence. + +The smallest fixed history retains durable `a`, receives complete `b` before +restore, then finds both `a` and `b` at startup. Empty answers leave `a` too. +The generated grammar also varies overlap, omissions and both restore orders. +Every failure above is a shared `PersistedStartupViolation` at startup, not a +focused probe's private expectation. The parent sources are retained as the +original-defect mutants. Reintroducing their snapshot/sweep decisions is the +wrong implementation rejected by those comparisons. + +RxDB and PowerSync previously chose snapshot mode from currently visible rows. +A persisted baseline can still be loading at that decision. Both now choose a +snapshot when the persistence capability exists, while retaining their prior +condition for unwrapped Collections. Metadata uses `null` for an absent +capability. An initial unconditional snapshot attempt broke RxDB's existing +pending-mutation receipt test; the final presence condition preserves that +unwrapped behavior, and its full suite passes without changing expectations. +TrailBase hydrates the baseline before its complete eager-list omission sweep, +keeps its change-event fence active, and rechecks cancellation after the await. +No new lifecycle state or persistent format is introduced. + +The shared comparison checks public rows and durable rows after preload, before +a refresh can repair them; public rows inside refresh fulfillment; durability +afterward; and a local-only reopen. Each startup uses one full wrapper-row read. +Query additionally compares ownership and cache/fetch premises. Fixed histories +still contain no expected answers of their own. + +## Measured reach and GREEN + +Each adapter/host/campaign runs 80 generated histories. Every required state has +its own enforced reach per host and per campaign. The all-adapter run passes +86 checks in six files: 1,920 generated histories, 56 fixed histories, and six +checker calibrations. The four added adapters' final counts follow. Query and +Convex retain their separately measured counters from the same run. + +| Adapter / host / campaign | Response before restore | Restore before response | Empty | Changed overlap | Omission | +| --- | ---: | ---: | ---: | ---: | ---: | +| rxdb / controlled / fixed | 37 | 43 | 46 | 27 | 64 | +| powersync / controlled / fixed | 37 | 43 | 46 | 27 | 64 | +| rxdb / controlled / random | 43 | 37 | 39 | 30 | 65 | +| rxdb / node-sqlite / fixed | 37 | 43 | 46 | 27 | 64 | +| rxdb / node-sqlite / random | 39 | 41 | 43 | 28 | 65 | +| electric / controlled / fixed | 37 | 43 | 46 | 27 | 64 | +| electric / controlled / random | 33 | 47 | 51 | 21 | 68 | +| electric / node-sqlite / fixed | 37 | 43 | 46 | 27 | 64 | +| electric / node-sqlite / random | 37 | 43 | 43 | 27 | 66 | +| trailbase / controlled / fixed | 37 | 43 | 46 | 27 | 64 | +| trailbase / controlled / random | 48 | 32 | 42 | 26 | 69 | +| trailbase / node-sqlite / fixed | 37 | 43 | 46 | 27 | 64 | +| trailbase / node-sqlite / random | 35 | 45 | 55 | 12 | 72 | +| powersync / controlled / random | 36 | 44 | 44 | 25 | 66 | +| powersync / node-sqlite / fixed | 37 | 43 | 46 | 27 | 64 | +| powersync / node-sqlite / random | 38 | 42 | 49 | 22 | 67 | + +Each added adapter's four fixed histories separately reaches response first +three times, restore first once, empty once, changed overlap twice, and omission +four times on each Node host. Chromium receives eight Query histories and four +for each of the five other adapters: all 28 pass. Its comparison checks result +counts per adapter so one adapter cannot supply another's witness. + +Surrounding validation preserves page/event, reload, cleanup and readiness laws. +RxDB passes all 36 runtime checks. TrailBase passes all 164. Electric passes +1,617 runtime checks; its initial full command also reported missing local +workspace type dependencies. After restoring those dependency links and building +Electric, its normal startup command and complete `tsc --noEmit` pass. PowerSync +passes 208 runtime checks initially; its declaration check first failed because +built artifacts were absent. Building the package and rerunning the declaration +and startup files passes all 14 checks. Its package typecheck also passes. +RxDB and TrailBase package typechecks pass. The replay-registry and cleanup +suites pass 119 checks. + +All four new guarded replay IDs execute one successful comparison on each Node +host. Electric and PowerSync use seed 2367/path 0. RxDB replays its parent +counterexample with seed 73381209/path 0:0:0, and TrailBase with seed +247682822/path 0:0. The guard records exactly two executions for each. Fixed +histories and fixed campaigns stand aside during replay. Electric and TrailBase +also register the new receiving suites in their existing `test:oracles` commands. + +## Review, calibration and limits + +Independent review confirmed two issues: testing an absent null capability as +present, and missing oracle-command registrations. Both are repaired. A +simplifier also found cleanup chains that could mask a failure or skip a later +release. Executing the actual four teardown closures with rejecting resource +methods reproduced all four failures. Reusing `withOracleCleanup` makes all four +controls pass: each release runs and all errors survive in order. The ordinary +cleanup-helper tests remain the primary law owner. No user-visible law changed. +The final loss audit accounts for all three review items as fixed, with no +confirmed-open item. Reviewers found useful bounded issues and supplied valid +receiving/replay evidence. Hire recommendation: yes for this review scope. + +Independent timing observations confirmed real RxDB and PowerSync source reads +completed before the held baseline released for empty, single-row and three-row +answers. PowerSync supplied the same premise in Node and Chromium. Gate release +alone is not presented as universal control over every provider schedule. + +ORC-001/002/003/004: the accepted authority, independent set model and literate +shared grammar are unchanged. The new receivers cross the same legal histories. +Timing ablation loses the startup race, emptiness admits a nonempty-only repair, +overlap misses stale values, and the startup durable cut prevents refresh from +masking stale storage. Duplicate keys and peers for a whole-Collection source +remain excluded. ORC-005/006/013/014: the real receiving boundaries and original +comparison REDs above distinguish the repairs. ORC-007/015: fixed/random parity, +measured per-host reach and guarded replay pass. ORC-008 is inapplicable because +no model state changed. ORC-009 retains the existing source/peer abstraction; +whole-Collection sources expose no Query ownership. ORC-010 has teardown failure +controls and the shared failure-preserving cleanup. ORC-011 has no identified +shared semantic fault that needs a second formulation. ORC-012 is this versioned +record and the final implementation reference. ORC-016: the receiving wrappers +register only shared model/grammar/check executions, with no separate answers. + +Limits remain eager initial complete replacement, three string keys and numeric +values, nonempty durable baselines, one wrapper persistence instance, no mutation +during initial fetch, and a refresh confirming the same answer. No on-demand +window, concurrent writer, failed-I/O, arbitrary metadata, remote provider +replication, or every intermediate-publication claim follows. Other lifecycle, +optimistic settlement and provider-resume laws retain their existing oracle +owners. Sampling is not a universal proof. No known reachable counterexample +remains in the declared complete-result startup domain. + +The only local registry recovery overrides remain Query Core 5.107.1 to 5.106.0 +and fake-indexeddb 6.2.8 to 6.2.7. Their override and lockfile changes stay +uncommitted. The committed lockfile adds only the test-only PowerSync browser +SDK and its dependencies, preserving every previous resolution. + +Production gzip growth against merge-base ea3c920c1: Query +26 bytes, PowerSync ++18, RxDB +19, TrailBase +63. Combined package growth is 126 gzip bytes and +112 brotli bytes. The ordinary Query app scenario grows 47 gzip bytes. Source +comments and receiving tests do not ship in these package bundles. + +Commands from the repository root: + +```sh +pnpm --filter @tanstack/electric-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false tests/persisted-startup-conformance-oracle.property.test.ts +pnpm --filter @tanstack/trailbase-db-collection test --maxWorkers=2 --coverage.enabled=false +pnpm --filter @tanstack/rxdb-db-collection test --maxWorkers=2 --coverage.enabled=false +pnpm --filter @tanstack/powersync-db-collection build +pnpm --filter @tanstack/powersync-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false tests/portable-declarations.test.ts tests/persisted-startup-conformance-oracle.property.test.ts +pnpm --filter @tanstack/browser-db-sqlite-persistence exec playwright test --config playwright.opfs.config.ts persisted-startup-conformance-oracle.opfs.spec.ts +pnpm --filter @tanstack/db exec vitest run --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false tests/oracle-replay.test.ts tests/utils-cleanup.test.ts +pnpm size:compare --packages query-db-collection,powersync-db-collection,rxdb-db-collection,trailbase-db-collection --files +pnpm lint:changed +``` + +Final normal-command validation after dependency setup: Electric passes all +1,617 checks in 46 files and PowerSync passes all 209 checks in 14 files, with +no type errors or unhandled errors. Combined with RxDB's 36 and TrailBase's 164, +all four surrounding package commands pass 2,026 checks. Explicit ESLint has +zero errors and two require-await warnings in the Promise-returning PowerSync +host factories. Formatting, `git diff --check` and staged `pnpm lint:changed` +pass. No lowered or removed oracle run budget is reported. diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts index 1eae73d4ee..587e3d8a91 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts @@ -11,9 +11,16 @@ function checkStartupConformance(result: StartupConformanceResult) { expect( result.results.filter((item) => item.adapter === `query`), ).toHaveLength(queryStartupHistories.length) - expect( - result.results.filter((item) => item.adapter === `convex`), - ).toHaveLength(persistedStartupHistories.length) + for (const adapter of [ + `convex`, + `electric`, + `trailbase`, + `rxdb`, + `powersync`, + ]) + expect( + result.results.filter((item) => item.adapter === adapter), + ).toHaveLength(persistedStartupHistories.length) expect(result.results.flatMap(({ violations }) => violations)).toEqual([]) } declareOracle({ check: [checkStartupConformance] }) diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts index cab07f5b46..16b3c9b8b4 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts @@ -15,6 +15,10 @@ import { queryStartupHistories, } from '../../query-db-collection/tests/persisted-startup-driver' import { convexStartupAdapter } from '../../convex-db-collection/tests/persisted-startup-driver' +import { electricStartupAdapter } from '../../electric-db-collection/tests/persisted-startup-driver' +import { trailBaseStartupAdapter } from '../../trailbase-db-collection/tests/persisted-startup-driver' +import { rxdbStartupAdapter } from '../../rxdb-db-collection/tests/persisted-startup-driver' +import { powerSyncBrowserStartupAdapter } from '../../powersync-db-collection/tests/persisted-startup-browser' import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' import { createBrowserWASQLitePersistence, @@ -86,6 +90,18 @@ async function run() { try { await receive(`query`, queryStartupAdapter, queryStartupHistories) await receive(`convex`, convexStartupAdapter, persistedStartupHistories) + await receive(`electric`, electricStartupAdapter, persistedStartupHistories) + await receive( + `trailbase`, + trailBaseStartupAdapter, + persistedStartupHistories, + ) + await receive(`rxdb`, rxdbStartupAdapter, persistedStartupHistories) + await receive( + `powersync`, + powerSyncBrowserStartupAdapter, + persistedStartupHistories, + ) window.__persistedStartupConformance = { results } } catch (error) { window.__persistedStartupConformance = { diff --git a/packages/db-adapter-conformance/MIGRATION.md b/packages/db-adapter-conformance/MIGRATION.md index 9d8e8fe7b1..882beb12f2 100644 --- a/packages/db-adapter-conformance/MIGRATION.md +++ b/packages/db-adapter-conformance/MIGRATION.md @@ -528,3 +528,25 @@ error/sync-give-up, accept]`). Fix: remember that give-up and fail the - **Remaining work:** other persisted adapters have no receiving driver yet. This property does not cover on-demand persistence, reload, concurrent writers, failed I/O, arbitrary metadata or the rest of `P-*`/`S-*`. + +- **2026-11-10, persisted startup receiving audit.** The maintainer required + checking every applicable published source adapter. Electric, TrailBase, + RxDB and PowerSync now receive the same shared startup law and composition. + - **RED before production:** both Node hosts reject the original RxDB, + PowerSync and TrailBase implementations (32 failed comparisons total). + Actual worker/OPFS rejects 10 startup snapshots, including a real browser + PowerSync source. Electric and Convex pass without production changes. + - **Repairs:** RxDB and PowerSync select snapshots when a persistence + capability is present, including before its rows restore; their prior + unwrapped conditions remain. TrailBase hydrates before the complete eager + list's omission sweep while retaining its event fence and cancellation check. + - **GREEN:** 80 histories in each fixed/random campaign per adapter and Node + host, with separate enforced reach. All six pass 86 checks, comprising + 1,920 generated histories, 56 fixed histories and six checker calibrations. + All 28 OPFS histories pass. Every new replay ID has two successful guarded + witnesses, one per Node host. Existing run budgets are unchanged. + - **Limits:** fresh complete source results, no mutation during initial + fetch, one wrapper persistence instance and the shared three-key domain. + Electric resume, deployed TrailBase service delivery, remote RxDB/PowerSync + replication, on-demand windows and concurrent writers retain other owners. + See the [versioned amendment](../../docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md). diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 88492996a3..70fc97cc4d 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -331,6 +331,10 @@ const staticOracleProperties = [ `sqlite-reload.class`, `query.startup-ownership`, `adapter.convex.persisted-startup`, + `adapter.electric.persisted-startup`, + `adapter.trailbase.persisted-startup`, + `adapter.rxdb.persisted-startup`, + `adapter.powersync.persisted-startup`, `browser.multi-tab-reserved-history`, `browser.electric-source-prefix`, `sqlite-reload.class.electron`, diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index 4448c6b709..c354a69c6f 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -419,6 +419,14 @@ const ownerGroups: ReadonlyArray< ], // Owners of properties that a module names and a host file runs carry // that module as the fourth element. + ...[`electric`, `trailbase`, `rxdb`, `powersync`].map( + (adapter) => + [ + `${adapter}-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts`, + `adapter.${adapter}`, + `persisted-startup`, + ] as const, + ), [ `convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts`, `adapter.convex`, @@ -961,7 +969,12 @@ export type OracleReplayManifestEntry = { // Pagination cells carry their property name in the test title so the guard // can select one cell without running the rest of the matrix. const replayTestNames = new Map([ - ...[`query.startup-ownership`, `adapter.convex.persisted-startup`].map( + ...[ + `query.startup-ownership`, + ...[`convex`, `electric`, `trailbase`, `rxdb`, `powersync`].map( + (adapter) => `adapter.${adapter}.persisted-startup`, + ), + ].map( (property) => [ property, diff --git a/packages/electric-db-collection/package.json b/packages/electric-db-collection/package.json index 517dcde6ba..2fadebcad6 100644 --- a/packages/electric-db-collection/package.json +++ b/packages/electric-db-collection/package.json @@ -21,7 +21,7 @@ "dev": "vite build --watch", "lint": "eslint . --fix", "test": "vitest run", - "test:oracles": "vitest run tests/pg-serializer-oracle.property.test.ts tests/electric-oracle.property.test.ts tests/electric-recovery-oracle.test.ts tests/electric-descriptor-isolation-oracle.test.ts tests/electric-sdk-framing.test.ts tests/electric-sdk-delivery-oracle.property.test.ts", + "test:oracles": "vitest run tests/pg-serializer-oracle.property.test.ts tests/electric-oracle.property.test.ts tests/electric-recovery-oracle.test.ts tests/electric-descriptor-isolation-oracle.test.ts tests/electric-sdk-framing.test.ts tests/electric-sdk-delivery-oracle.property.test.ts tests/persisted-startup-conformance-oracle.property.test.ts", "test:e2e": "vitest run --config vitest.e2e.config.ts" }, "type": "module", diff --git a/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts new file mode 100644 index 0000000000..aad9476ff3 --- /dev/null +++ b/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -0,0 +1,23 @@ +/** Electric's complete eager initial result receives the shared persistence + * replacement law. The shared suite crosses old/new membership, empty results, + * changed overlap values and both restore orders on each storage host. + */ +import { describe } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { + persistedStartupHistories, + persistedStartupHistoryArb, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' +import { electricStartupAdapter } from './persisted-startup-driver' + +const historyArb = persistedStartupHistoryArb(`collection`) +declareOracle({ grammar: [historyArb] }) +describe(`persisted startup conformance: Electric`, () => { + registerPersistedStartupConformance({ + property: `adapter.electric.persisted-startup`, + adapter: electricStartupAdapter, + histories: persistedStartupHistories, + arbitrary: historyArb, + }) +}) diff --git a/packages/electric-db-collection/tests/persisted-startup-driver.ts b/packages/electric-db-collection/tests/persisted-startup-driver.ts new file mode 100644 index 0000000000..45a3a68948 --- /dev/null +++ b/packages/electric-db-collection/tests/persisted-startup-driver.ts @@ -0,0 +1,130 @@ +/** + * The installed Electric ShapeStream parses a fresh complete shape over a + * controlled HTTP boundary. A snapshot starts at offset -1 and ends with + * up-to-date; it replaces persisted rows even when restore is still held. + * Reload may resume its cursor, so refresh answers must-refetch before the + * same complete shape. Resumed delta-only startup belongs to Electric's + * recovery oracle and must not be judged as complete replacement. + */ +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { + sortedStartupRows, + startupRows, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { electricCollectionOptions } from '../src/electric' +import type { + PersistedStartupHistory, + StartupRow, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' + +type Request = { url: URL; respond: (response: Response) => void } +export const electricStartupAdapter: PersistedStartupAdapter = + { + scope: `collection`, + create(persistence, history, id) { + const requests: Array = [] + let available = false + let offset = 0 + const fetchClient: typeof fetch = (input, init) => + new Promise((resolve, reject) => { + const signal = init?.signal + const request = { + url: new URL(String(input)), + respond: (response: Response) => { + signal?.removeEventListener(`abort`, abort) + resolve(response) + }, + } + const abort = () => { + const index = requests.indexOf(request) + if (index >= 0) requests.splice(index, 1) + reject(signal?.reason ?? new DOMException(`aborted`, `AbortError`)) + } + if (signal?.aborted) { + abort() + return + } + signal?.addEventListener(`abort`, abort, { once: true }) + requests.push(request) + }) + const collection = createCollection( + persistedCollectionOptions({ + ...electricCollectionOptions({ + id, + shapeOptions: { + url: `http://startup/shape-${id}`, + params: { table: `rows` }, + fetchClient, + }, + getKey: (row) => row.id, + }), + persistence, + }), + ) + const take = async () => { + await untilStartup(() => requests.length > 0) + return requests.shift()! + } + const deliver = async () => { + let request = await take() + if (request.url.searchParams.get(`offset`) !== `-1`) { + request.respond( + new Response( + JSON.stringify([{ headers: { control: `must-refetch` } }]), + { + status: 409, + headers: { 'electric-handle': `startup-${++offset}` }, + }, + ), + ) + request = await take() + } + offset++ + request.respond( + new Response( + JSON.stringify([ + ...startupRows(history.next, 2).map((row) => ({ + key: row.id, + value: { id: row.id, value: String(row.value) }, + headers: { operation: `insert` }, + })), + { + headers: { + control: `up-to-date`, + global_last_seen_lsn: String(offset), + }, + }, + ]), + { + headers: { + 'electric-handle': `startup-${offset}`, + 'electric-offset': `${offset}_0`, + 'electric-cursor': String(offset), + 'electric-up-to-date': `true`, + 'electric-schema': JSON.stringify({ + id: { type: `text`, pk_index: 0 }, + value: { type: `int4` }, + }), + }, + }, + ), + ) + available = true + } + return { + collection, + snapshot: () => ({ rows: sortedStartupRows(collection.values()) }), + deliver, + responseAvailable: () => available, + refresh: async () => { + const reloaded = collection.reload() + await deliver() + await reloaded + }, + dispose: () => collection.cleanup(), + } + }, + } diff --git a/packages/powersync-db-collection/package.json b/packages/powersync-db-collection/package.json index 2a5935e517..55fb36f925 100644 --- a/packages/powersync-db-collection/package.json +++ b/packages/powersync-db-collection/package.json @@ -64,6 +64,7 @@ "devDependencies": { "@powersync/common": "2.5.0", "@powersync/node": "0.21.0", + "@powersync/web": "2.6.0", "@types/debug": "^4.1.13", "@vitest/coverage-istanbul": "^5.1.0", "better-sqlite3": "^12.11.1" diff --git a/packages/powersync-db-collection/src/powersync.ts b/packages/powersync-db-collection/src/powersync.ts index dfb52c15f4..4975ec304c 100644 --- a/packages/powersync-db-collection/src/powersync.ts +++ b/packages/powersync-db-collection/src/powersync.ts @@ -625,9 +625,11 @@ function createPowerSyncCollectionConfig< const appliedReceipts: Array = [] await establishTracking( { - // When the Collection holds rows, such as a reload's kept rows, - // the load of every row replaces them. - snapshot: !collection._state.acceptedSyncedEntries().next().done, + // Replace retained rows, including persisted rows whose restore + // has not completed when this load starts. + snapshot: + !!params.metadata?.persistence || + !collection._state.acceptedSyncedEntries().next().done, when: { [DiffTriggerOperation.INSERT]: `TRUE`, [DiffTriggerOperation.UPDATE]: `TRUE`, diff --git a/packages/powersync-db-collection/tests/persisted-startup-browser.ts b/packages/powersync-db-collection/tests/persisted-startup-browser.ts new file mode 100644 index 0000000000..89ac420fcb --- /dev/null +++ b/packages/powersync-db-collection/tests/persisted-startup-browser.ts @@ -0,0 +1,23 @@ +// Browser PowerSync SDK receiving path. Its SQLite database is separate from +// the wrapper's worker/OPFS database; both execute their real storage engines. +import { PowerSyncDatabase } from '@powersync/web' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { powerSyncStartupAdapterFor } from './persisted-startup-common' + +let sequence = 0 +export const powerSyncBrowserStartupAdapter = powerSyncStartupAdapterFor( + async (schema) => { + const database = new PowerSyncDatabase({ + schema, + database: { dbFilename: `powersync-startup-${sequence++}.sqlite` }, + }) + return { + database, + dispose: () => + withOracleCleanup( + () => database.disconnectAndClear(), + [() => database.close()], + ), + } + }, +) diff --git a/packages/powersync-db-collection/tests/persisted-startup-common.ts b/packages/powersync-db-collection/tests/persisted-startup-common.ts new file mode 100644 index 0000000000..b21b44554d --- /dev/null +++ b/packages/powersync-db-collection/tests/persisted-startup-common.ts @@ -0,0 +1,85 @@ +/** + * A real local PowerSync SQLite table supplies the entire eager source result. + * The public onLoad hook holds that read while the oracle orders it against + * persisted restore. The shared model checks the second persistence store; + * source SQLite is not a reason to waive replacement in a wrapped Collection. + * Reload reads the unchanged table again. Remote PowerSync replication is + * outside this local-table witness. + */ +import { Schema, Table, column } from '@powersync/common' +import { createCollection } from '@tanstack/db' +import { z } from 'zod' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { + sortedStartupRows, + startupRows, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { powerSyncCollectionOptions } from '../src' +import type { CommonPowerSyncDatabase } from '@powersync/common' +import type { PersistedStartupHistory } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' + +const schema = new Schema({ rows: new Table({ value: column.integer }) }) +const rowSchema = z.object({ id: z.string(), value: z.number() }) +export function powerSyncStartupAdapterFor( + open: (schema: Schema) => Promise<{ + database: CommonPowerSyncDatabase + dispose: () => Promise + }>, +): PersistedStartupAdapter { + return { + scope: `collection`, + async create(persistence, history, id) { + const { database, dispose } = await open(schema) + await database.writeTransaction(async (tx) => { + for (const row of startupRows(history.next, 2)) + await tx.execute(`INSERT INTO rows (id, value) VALUES (?, ?)`, [ + row.id, + row.value, + ]) + }) + let pending: (() => void) | undefined + let available = false + const collection = createCollection( + persistedCollectionOptions({ + ...powerSyncCollectionOptions({ + id, + database, + table: schema.props.rows, + schema: rowSchema, + onDeserializationError: () => {}, + onLoad: () => + new Promise((resolve) => { + pending = resolve + }), + }), + persistence, + }), + ) + const deliver = async () => { + await untilStartup(() => pending !== undefined) + const release = pending! + pending = undefined + available = true + release() + } + return { + collection, + snapshot: () => ({ rows: sortedStartupRows(collection.values()) }), + deliver, + responseAvailable: () => available, + refresh: async () => { + const reloaded = collection.reload() + await deliver() + await reloaded + }, + dispose: async () => { + pending?.() + await withOracleCleanup(() => collection.cleanup(), [dispose]) + }, + } + }, + } +} diff --git a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts new file mode 100644 index 0000000000..45e4e5de01 --- /dev/null +++ b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -0,0 +1,23 @@ +/** PowerSync's complete eager initial result receives the shared persistence + * replacement law. The shared suite crosses old/new membership, empty results, + * changed overlap values and both restore orders on each storage host. + */ +import { describe } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { + persistedStartupHistories, + persistedStartupHistoryArb, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' +import { powerSyncStartupAdapter } from './persisted-startup-driver' + +const historyArb = persistedStartupHistoryArb(`collection`) +declareOracle({ grammar: [historyArb] }) +describe(`persisted startup conformance: PowerSync`, () => { + registerPersistedStartupConformance({ + property: `adapter.powersync.persisted-startup`, + adapter: powerSyncStartupAdapter, + histories: persistedStartupHistories, + arbitrary: historyArb, + }) +}) diff --git a/packages/powersync-db-collection/tests/persisted-startup-driver.ts b/packages/powersync-db-collection/tests/persisted-startup-driver.ts new file mode 100644 index 0000000000..ea749f439d --- /dev/null +++ b/packages/powersync-db-collection/tests/persisted-startup-driver.ts @@ -0,0 +1,36 @@ +// Node PowerSync SDK receiving path for the shared startup composition. +import { randomUUID } from 'node:crypto' +import { rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { PowerSyncDatabase } from '@powersync/node' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { powerSyncStartupAdapterFor } from './persisted-startup-common' +import { TEST_DATABASE_IMPLEMENTATION } from './test-db-implementation' + +export const powerSyncStartupAdapter = powerSyncStartupAdapterFor( + async (schema) => { + if (!TEST_DATABASE_IMPLEMENTATION) + throw new Error(`No PowerSync SQLite implementation available`) + const file = `startup-${randomUUID()}.sqlite` + const database = new PowerSyncDatabase({ + database: { + dbFilename: file, + dbLocation: tmpdir(), + implementation: TEST_DATABASE_IMPLEMENTATION, + }, + schema, + }) + return { + database, + dispose: () => + withOracleCleanup( + () => database.disconnectAndClear(), + [ + () => database.close(), + () => rm(join(tmpdir(), file), { force: true }), + ], + ), + } + }, +) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index e9d36b1fe1..071a5979dc 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -137,11 +137,13 @@ export function rxdbCollectionOptions( */ let cursor: RxDocumentData | undefined = undefined const syncBatchSize = config.syncBatchSize ? config.syncBatchSize : 1000 - // The initial fetch reads every document. When the Collection holds - // rows, such as a reload's kept rows, it is a snapshot that replaces - // them, so a row RxDB no longer has goes. + // Replace retained rows, including a persisted baseline whose restore + // has not completed. An empty, unwrapped Collection keeps ordinary + // commit ordering with pending mutations. begin({ - snapshot: !collection._state.acceptedSyncedEntries().next().done, + snapshot: + !!params.metadata?.persistence || + !collection._state.acceptedSyncedEntries().next().done, }) while (!initialFetchComplete) { diff --git a/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts new file mode 100644 index 0000000000..73dff454a6 --- /dev/null +++ b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -0,0 +1,23 @@ +/** RxDB's complete eager initial result receives the shared persistence + * replacement law. The shared suite crosses old/new membership, empty results, + * changed overlap values and both restore orders on each storage host. + */ +import { describe } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { + persistedStartupHistories, + persistedStartupHistoryArb, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' +import { rxdbStartupAdapter } from './persisted-startup-driver' + +const historyArb = persistedStartupHistoryArb(`collection`) +declareOracle({ grammar: [historyArb] }) +describe(`persisted startup conformance: RxDB`, () => { + registerPersistedStartupConformance({ + property: `adapter.rxdb.persisted-startup`, + adapter: rxdbStartupAdapter, + histories: persistedStartupHistories, + arbitrary: historyArb, + }) +}) diff --git a/packages/rxdb-db-collection/tests/persisted-startup-driver.ts b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts new file mode 100644 index 0000000000..f145f06a45 --- /dev/null +++ b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts @@ -0,0 +1,98 @@ +/** + * Real RxDB memory storage supplies the complete initial query, including its + * paging and last-write-time cursor. Only the first query response is held to + * cross persisted restore. The shared model judges rows at all startup cuts; + * public reload re-reads the same RxCollection. This is a local-storage SDK + * witness, not evidence about RxDB's separate server replication. + */ +import { createCollection } from '@tanstack/db' +import { createRxDatabase } from 'rxdb/plugins/core' +import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { + sortedStartupRows, + startupRows, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { rxdbCollectionOptions } from '../src/rxdb' +import type { RxCollection } from 'rxdb/plugins/core' +import type { + PersistedStartupHistory, + StartupRow, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' + +let sequence = 0 +export const rxdbStartupAdapter: PersistedStartupAdapter = + { + scope: `collection`, + async create(persistence, history, id) { + const database = await createRxDatabase<{ + rows: RxCollection + }>({ + name: `persistedstartup${sequence++}`, + storage: getRxStorageMemory(), + multiInstance: false, + }) + const { rows } = await database.addCollections({ + rows: { + schema: { + version: 0, + type: `object`, + primaryKey: `id`, + properties: { + id: { type: `string`, maxLength: 10 }, + value: { type: `number` }, + }, + required: [`id`, `value`], + }, + }, + }) + const inserted = await rows.bulkInsert(startupRows(history.next, 2)) + if (inserted.error.length) + throw new Error(`RxDB startup fixture failed to store source rows`) + let release!: () => void + let available = false + const response = new Promise((resolve) => { + release = resolve + }) + const query = rows.storageInstance.query.bind(rows.storageInstance) + let first = true + rows.storageInstance.query = async (prepared) => { + if (first) { + first = false + await response + } + return query(prepared) + } + const collection = createCollection( + persistedCollectionOptions({ + ...rxdbCollectionOptions({ + id, + rxCollection: rows, + syncBatchSize: 2, + syncMode: `eager`, + }), + persistence, + }), + ) + return { + collection, + snapshot: () => ({ rows: sortedStartupRows(collection.values()) }), + deliver: () => { + available = true + release() + return Promise.resolve() + }, + responseAvailable: () => available, + refresh: () => collection.reload(), + dispose: async () => { + release() + await withOracleCleanup( + () => collection.cleanup(), + [() => database.remove()], + ) + }, + } + }, + } diff --git a/packages/trailbase-db-collection/package.json b/packages/trailbase-db-collection/package.json index 201dba6c5d..e582016fb1 100644 --- a/packages/trailbase-db-collection/package.json +++ b/packages/trailbase-db-collection/package.json @@ -21,7 +21,7 @@ "dev": "vite build --watch", "lint": "eslint . --fix", "test": "vitest --run", - "test:oracles": "vitest run tests/lifecycle-oracle.property.test.ts", + "test:oracles": "vitest run tests/lifecycle-oracle.property.test.ts tests/persisted-startup-conformance-oracle.property.test.ts", "test:e2e": "vitest --run --config vitest.e2e.config.ts" }, "type": "module", diff --git a/packages/trailbase-db-collection/src/trailbase.ts b/packages/trailbase-db-collection/src/trailbase.ts index c4956328fe..48456d8919 100644 --- a/packages/trailbase-db-collection/src/trailbase.ts +++ b/packages/trailbase-db-collection/src/trailbase.ts @@ -2,6 +2,7 @@ import { Store } from '@tanstack/store' import { LoadSubsetOperationAbortedError, + validateSyncPersistenceCapability, withCollectionConfigFactory, } from '@tanstack/db' import { @@ -186,6 +187,9 @@ export function trailBaseCollectionOptions< const sync = { sync: (params: SyncParams) => { const { begin, write, commit, markReady, fail, collection } = params + const persistence = validateSyncPersistenceCapability( + params.metadata?.persistence, + ) let cancelled = false let periodicCleanupTask: ReturnType | undefined const cleanup = () => { @@ -355,6 +359,12 @@ export function trailBaseCollectionOptions< // list and that no change event touched is gone. // Only the gone keys are collected, since a write during the walk // would change the entries it reads. + if (everyRow && persistence) { + // The complete list must also remove omitted rows whose persisted + // restore was still pending while its pages arrived. + await persistence.hydrateBaseline() + if (cancelled) return + } const gone: Array = [] if (everyRow) for (const [key] of collection._state.acceptedSyncedEntries()) diff --git a/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts new file mode 100644 index 0000000000..27f8622524 --- /dev/null +++ b/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -0,0 +1,23 @@ +/** TrailBase's complete eager initial result receives the shared persistence + * replacement law. The shared suite crosses old/new membership, empty results, + * changed overlap values and both restore orders on each storage host. + */ +import { describe } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { + persistedStartupHistories, + persistedStartupHistoryArb, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' +import { trailBaseStartupAdapter } from './persisted-startup-driver' + +const historyArb = persistedStartupHistoryArb(`collection`) +declareOracle({ grammar: [historyArb] }) +describe(`persisted startup conformance: TrailBase`, () => { + registerPersistedStartupConformance({ + property: `adapter.trailbase.persisted-startup`, + adapter: trailBaseStartupAdapter, + histories: persistedStartupHistories, + arbitrary: historyArb, + }) +}) diff --git a/packages/trailbase-db-collection/tests/persisted-startup-driver.ts b/packages/trailbase-db-collection/tests/persisted-startup-driver.ts new file mode 100644 index 0000000000..b3166d56ea --- /dev/null +++ b/packages/trailbase-db-collection/tests/persisted-startup-driver.ts @@ -0,0 +1,72 @@ +/** + * TrailBase's eager list supplies every row; the shared startup oracle judges + * its replacement of persisted rows. The controlled RecordApi holds the list + * response and uses a native stream, as the lifecycle oracle does. No change + * events occur during this initial replacement; their page fence has its own + * oracle. Refresh is the public reload, which lists the same source again. + */ +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { + sortedStartupRows, + startupRows, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { trailBaseCollectionOptions } from '../src/trailbase' +import type { Event, ListResponse, RecordApi } from 'trailbase' +import type { + PersistedStartupHistory, + StartupRow, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' + +export const trailBaseStartupAdapter: PersistedStartupAdapter = + { + scope: `collection`, + create(persistence, history, id) { + let pending: ((result: ListResponse) => void) | undefined + let available = false + const api = { + subscribe: () => Promise.resolve(new ReadableStream()), + list: () => + new Promise>((resolve) => { + pending = resolve + }), + } as unknown as RecordApi + const collection = createCollection( + persistedCollectionOptions({ + ...trailBaseCollectionOptions({ + id, + recordApi: api, + getKey: (row) => row.id, + syncMode: `eager`, + parse: {}, + serialize: {}, + }), + persistence, + }), + ) + const deliver = async () => { + await untilStartup(() => pending !== undefined) + const respond = pending! + pending = undefined + respond({ records: startupRows(history.next, 2) }) + available = true + } + return { + collection, + snapshot: () => ({ rows: sortedStartupRows(collection.values()) }), + deliver, + responseAvailable: () => available, + refresh: async () => { + const reloaded = collection.reload() + await deliver() + await reloaded + }, + dispose: async () => { + pending?.({ records: [] }) + await collection.cleanup() + }, + } + }, + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2008d5527..fb4baa233c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1584,6 +1584,9 @@ importers: '@powersync/node': specifier: 0.21.0 version: 0.21.0(@powersync/common@2.5.0)(better-sqlite3@12.11.1) + '@powersync/web': + specifier: 2.6.0 + version: 2.6.0(@powersync/common@2.5.0)(@powersync/shared-internals@1.5.0(@powersync/common@2.5.0)) '@types/debug': specifier: ^4.1.13 version: 4.1.13 @@ -4692,6 +4695,9 @@ packages: '@journeyapps/wa-sqlite@1.7.2': resolution: {integrity: sha512-EVRMzqHtHgJBFcTGOgQ5/YlxrYFSXLFFztC5yo6+jQx2mqFR+a1kkLV4IirquiBzIonjmYXfBtvZ26fmI8r9Nw==} + '@journeyapps/wa-sqlite@2.0.10': + resolution: {integrity: sha512-6Wo5ZijOc4/6M9u+MDsJdW8zDCdrct/p+7LcG7S7CT5YKu42j6lshMfUvgtGYMaZeuH7rpQHULZupPTQFqbS0w==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -5378,6 +5384,18 @@ packages: peerDependencies: '@powersync/common': 2.0.0 + '@powersync/shared-internals@1.5.0': + resolution: {integrity: sha512-dIaMenW6gJNyKzD93d4X4CiBtJxyJ63pvK2CYGjqBtVqubGBt9d+jg6V6JTKw4VLPjFCZh7/zrXMV34wl7kzGw==} + peerDependencies: + '@powersync/common': ^2.5.0 + + '@powersync/web@2.6.0': + resolution: {integrity: sha512-/B8u/sPHyNiPH1bj+vOfN/9sHlbp9lwQGtS6BHJJjqWlhBaUxzh4xQMydyQcS6puHaDXzddpOlkZIvTLOTLMnw==} + hasBin: true + peerDependencies: + '@powersync/common': ^2.5.0 + '@powersync/shared-internals': ^1.5.0 + '@publint/pack@0.1.7': resolution: {integrity: sha512-4EDEmvxWtgsCnnVeBvtFIFZtUhPPt1+bA9JrSwU4Sa//6oKtzCSlGGXYJr44OD9aGISymbieJ4mCKHUygUDU+g==} engines: {node: '>=18'} @@ -16892,6 +16910,8 @@ snapshots: '@journeyapps/wa-sqlite@1.7.2': {} + '@journeyapps/wa-sqlite@2.0.10': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -17470,6 +17490,18 @@ snapshots: dependencies: '@powersync/common': 2.5.0 + '@powersync/shared-internals@1.5.0(@powersync/common@2.5.0)': + dependencies: + '@powersync/common': 2.5.0 + + '@powersync/web@2.6.0(@powersync/common@2.5.0)(@powersync/shared-internals@1.5.0(@powersync/common@2.5.0))': + dependencies: + '@journeyapps/wa-sqlite': 2.0.10 + '@powersync/common': 2.5.0 + '@powersync/shared-internals': 1.5.0(@powersync/common@2.5.0) + comlink: 4.4.2 + commander: 12.1.0 + '@publint/pack@0.1.7': dependencies: tinyexec: 1.3.1 From 9c5d7b02a2055d45ede611e23498f3db96146726 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 07:45:19 -0700 Subject: [PATCH 08/24] Record all-adapter startup validation reference --- .../2026-11-10-persisted-startup-adapters.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md index f034129737..2dfe7c3d51 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md +++ b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md @@ -197,3 +197,11 @@ all four surrounding package commands pass 2,026 checks. Explicit ESLint has zero errors and two require-await warnings in the Promise-returning PowerSync host factories. Formatting, `git diff --check` and staged `pnpm lint:changed` pass. No lowered or removed oracle run budget is reported. + +The validated all-adapter implementation is commit +`a0d7d49e54d820ee342443fdb10f74dab23a55db`. This reference update changes prose +only. A later commit-hook automatic install also hit HTTP 403 for +`@tailwindcss/postcss` 4.3.5. An isolated install of the nearest earlier version, +4.3.4, succeeds. Its local override also remains uncommitted. The commit runs +the normal lint-staged hook with pnpm's automatic pre-run installation disabled; +no hook or check is skipped. From 3fe76d1484ae17e0fd28f2a35055b1b339cfbe95 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 08:28:38 -0700 Subject: [PATCH 09/24] Cover network-first startup durability across eager adapters Law: network-first fallback preserves saved rows; the first complete source result then replaces and persists them under the existing scoped law. Reach: all six adapters on controlled and Node SQLite hosts, 80 histories per fixed/random campaign (1,920 generated); both enabled-option states reached per adapter/host/campaign. OPFS receives all 80 fixed histories. RED: no new defect on unchanged production 9c5d7b02; dropped-write mutants fail startupDurable for all twelve adapter/Node-host combinations. GREEN: 208 shared checks, 404 readiness checks, 80 OPFS histories, seven package typechecks, direct replay, formatting and changed-file lint pass. Mutant: source commits lose durable writes after fallback; the durable startup comparison rejects saved rows before refresh. Lost fallback rows also fail. Limits: existing eager/nonempty baseline domain. Exact timeout, failed/empty restore and remote failure remain with the readiness owner. No production changes, new on-demand claim, concurrent writer or intermediate publication. --- docs/contributing/oracle-coverage.md | 27 +++++- .../tests/persisted-startup-driver.ts | 33 ++++--- .../src/persisted-startup-oracle.ts | 89 +++++++++++++++++-- .../tests/persisted-startup-driver.ts | 84 ++++++++++++++++- .../tests/persisted-startup-oracle-suite.ts | 82 ++++++++++++++++- .../tests/persisted-startup-driver.ts | 29 +++--- .../tests/persisted-startup-common.ts | 33 ++++--- ...rsisted-startup-conformance-oracle.test.ts | 13 ++- .../tests/persisted-startup-driver.ts | 54 +++++++---- .../tests/persisted-startup-driver.ts | 21 +++-- .../tests/persisted-startup-driver.ts | 31 +++++-- 11 files changed, 412 insertions(+), 84 deletions(-) diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index affbab3a38..b1506dd76f 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -5104,6 +5104,13 @@ native e2e needs `cargo`. React Suspense reveals a covered failure about 300 ms late (its fallback throttle), so the contract's before-wait checkpoint sits at 450 ms. +The readiness fixture does not retain writes. The [shared adapter startup owner](../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts) +receives the network-first contract on all six eager adapters. It compares +saved rows and durable state while the source is held past the deadline, then +the first complete source result and its durability before refresh, plus reopen. +It also generates enabled-option responses before the deadline. Exact deadline, +empty/failed restore, and remote-failure timing remain with this readiness owner. + ## Sync metadata session [Issue #2220](https://github.com/TanStack/db/issues/2220) reports that a @@ -7747,24 +7754,36 @@ judgment, controlled storage, and real Node SQLite hosts. Per host, fixed and random campaigns each run 80 generated histories with separate enforced reach. Histories cross nonempty prior subsets of three keys, any next subset weighted toward empty, changed overlap values, and response before/after persisted -restore. Query additionally crosses peer ownership and five response origins: +restore. A generated initial-render axis enables network-first and crosses +source responses before the deadline with a source held past it. Every adapter, +host and generated campaign enforces separate reach for both enabled paths. +Query additionally crosses peer ownership and five response origins: fetch, initialData/cache with fetching disabled or held pending. Already available results precede restore; fetched and push results exercise both orders. -The shared comparisons check public and durable rows immediately after preload, -before refresh can repair storage, then public rows inside refresh fulfillment, +With network-first fallback, the shared comparisons check saved public and +durable rows after preload while the source is still held. After delivery, +the source's `markReady()` and applied receipts identify its first complete +result. Other histories retain the immediate post-preload comparison. +Public and durable rows are checked before refresh can repair storage, +then public rows inside refresh fulfillment, durable rows afterward, and rows after a local-only reopen. Scoped Query snapshots also check exact ownership at all four snapshot cuts. Startup checks the reload owner's one-full-row-read law before its own durable observation. Query's receiving driver preserves exact fetch-count and pending-fetch premises. The [Chromium OPFS receiver](../../packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts) -runs eight Query histories and four complete-result histories for each of the +runs twenty Query histories and twelve complete-result histories for each of the other five adapters through the same composition and check on real worker/OPFS. The original Query omission fails startup on both Node hosts and OPFS. Convex without its first-result truncate, a survivor in the bare eager-source grammar, now fails the same comparison on all three hosts. Current Convex passes; this migration confirms no new Convex defect. Electric also passes unchanged. +The expanded network-first grammar passes on unchanged production across both +Node hosts and OPFS. Per-adapter/host mutants that drop source persistence after +fallback fail at `startupDurable`, before a refresh. Fallback checker controls +also reject lost saved rows. The one-full-row-read law excludes only the +observer's own durable reads. The new receiving campaigns expose startup violations in RxDB, PowerSync and TrailBase before production edits, on both Node hosts and real OPFS. RxDB and PowerSync select snapshots for pending persisted baselines; TrailBase hydrates diff --git a/packages/convex-db-collection/tests/persisted-startup-driver.ts b/packages/convex-db-collection/tests/persisted-startup-driver.ts index c90b22119c..9ebce428e9 100644 --- a/packages/convex-db-collection/tests/persisted-startup-driver.ts +++ b/packages/convex-db-collection/tests/persisted-startup-driver.ts @@ -5,6 +5,10 @@ * exist here. Both delivery/restore orders use the same shared comparison. */ import { createCollection } from '@tanstack/db' +import { + startupInitialRender, + trackStartupSync, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' import { convexCollectionOptions } from '../src' import { @@ -21,20 +25,24 @@ export const convexStartupAdapter: PersistedStartupAdapter + result.map((row) => ({ + id: String.fromCharCode(97 + row.id), + value: row.value as number, + })), + getKey: (row) => row.id, + }) + const startup = trackStartupSync(options.sync) const collection = createCollection( persistedCollectionOptions({ - ...convexCollectionOptions({ - id, - client: provider.client, - query: rowsQuery, - queryArgs: {}, - select: (result) => - result.map((row) => ({ - id: String.fromCharCode(97 + row.id), - value: row.value as number, - })), - getKey: (row) => row.id, - }), + ...options, + sync: startup.sync, + ...startupInitialRender(h), persistence, }), ) @@ -53,6 +61,7 @@ export const convexStartupAdapter: PersistedStartupAdapter ({ rows: sortedStartupRows(collection.values()) }), deliver, responseAvailable: () => available, diff --git a/packages/db-adapter-conformance/src/persisted-startup-oracle.ts b/packages/db-adapter-conformance/src/persisted-startup-oracle.ts index 8226c98e9c..00558cec7a 100644 --- a/packages/db-adapter-conformance/src/persisted-startup-oracle.ts +++ b/packages/db-adapter-conformance/src/persisted-startup-oracle.ts @@ -1,6 +1,7 @@ /** - * Persisted restore cannot weaken a complete source snapshot. At startup and - * refresh fulfillment, public and durable rows must reflect replacement, even + * Persisted restore cannot weaken a complete source snapshot. At the first + * source result and refresh fulfillment, public and durable rows must reflect + * replacement, even * when the result arrived before restore. A scoped source removes only its own * ownership; a peer's omitted row survives. A collection-wide source replaces * every row. Authority: the shared eager-source READY-3/PUB-7 replacement law, @@ -8,6 +9,12 @@ * Data"), and persistence's durable acceptance contract. The maintainer chose * shared persistence conformance as this crossing's owner on 2026-11-09. * + * Network-first initial rendering changes when saved rows may be shown, not + * the later complete result or its durability. At deadline fallback, before any + * source response, saved rows and ownership stay intact. Authority: the glossary's + * network-first initial-render contract and persisted-readiness oracle. That + * owner retains empty/failed restore, remote-failure, and exact deadline laws. + * * The model abstracts source scopes to `source` and `peer`. These are ownership * edges, not subscriptions or provider identities. Whole-Collection adapters * expose rows only; scoped adapters also expose their ownership projection. @@ -26,7 +33,8 @@ export type PersistedStartupHistory = { previous: number next: number peer: boolean - response: `before-restore` | `after-restore` + response: `before-restore` | `after-restore` | `after-deadline` + initialRender?: `network-first` } export type StartupRow = { id: string; value: number } export type StartupScope = `collection` | `scoped` @@ -37,6 +45,8 @@ export type StartupSnapshot = { export type PersistedStartupObservation = { responseBeforeRestore: boolean startupRowReads: number + fallback?: StartupSnapshot + fallbackDurable?: StartupSnapshot startup: StartupSnapshot startupDurable: StartupSnapshot refresh: StartupSnapshot @@ -97,6 +107,22 @@ export const persistedStartupMembershipArb = (scope: StartupScope) => next: fc.oneof(fc.constant(0), fc.integer({ min: 0, max: 7 })), peer: scope === `scoped` ? fc.boolean() : fc.constant(false), }) +// A deadline fallback is legal only with network-first enabled and a held +// source result. Ordinary responses retain both restore orders. The deadline's +// exact threshold belongs to the persisted-readiness oracle; here zero expires +// the wait while the source is held, and 60 seconds keeps it pending otherwise. +export const persistedStartupRenderArb = fc.boolean() +export function withStartupRender( + h: H, + networkFirst: boolean, + fallback: boolean, +): H { + return { + ...h, + ...(networkFirst ? { initialRender: `network-first` as const } : {}), + response: networkFirst && fallback ? `after-deadline` : h.response, + } +} export const persistedStartupHistoryArb = ( scope: StartupScope, ): fc.Arbitrary => @@ -105,17 +131,56 @@ export const persistedStartupHistoryArb = ( persistedStartupMembershipArb(scope), fc.constantFrom(`before-restore` as const, `after-restore` as const), ) - .map(([membership, response]) => ({ ...membership, response })) + .chain(([membership, response]) => + fc + .tuple(persistedStartupRenderArb, fc.boolean()) + .map(([network, fallback]) => + withStartupRender({ ...membership, response }, network, fallback), + ), + ) // These are inputs to the same judgment as generated histories. Receivers may // add adapter-specific origins, but never supply hand-written expected answers. -export const persistedStartupHistories: Array = [ +const baseStartupHistories: Array = [ { previous: 1, next: 2, peer: false, response: `before-restore` }, { previous: 1, next: 0, peer: false, response: `before-restore` }, { previous: 3, next: 6, peer: false, response: `before-restore` }, { previous: 3, next: 6, peer: false, response: `after-restore` }, ] +export const persistedStartupHistories = baseStartupHistories.flatMap((h) => [ + h, + withStartupRender(h, true, false), + withStartupRender(h, true, true), +]) + +// Before any source result, timeout changes readiness only. Saved rows and +// ownership must survive intact, both publicly and durably. Reusing replacement +// with the old membership/value derives this cut without a second state model. +export function expectedStartupFallback( + h: PersistedStartupHistory, + scope: StartupScope, +): StartupSnapshot { + const saved = expectedPersistedStartup({ ...h, next: h.previous }, scope) + return { ...saved, rows: startupRows(h.previous, 1) } +} +export function persistedStartupFallbackViolations( + h: PersistedStartupHistory, + scope: StartupScope, + cut: string, + actual: StartupSnapshot | undefined, +): Array { + const expected = + h.response === `after-deadline` + ? expectedStartupFallback(h, scope) + : undefined + return JSON.stringify(actual) === JSON.stringify(expected) + ? [] + : [ + `${cut}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + ] +} + export class PersistedStartupViolation extends Error { constructor(readonly violations: Array) { super(violations.join(`\n`)) @@ -162,6 +227,10 @@ export function persistedStartupViolations( observation: PersistedStartupObservation, ): Array { const violations = persistedStartupPremiseViolations(h, observation) + for (const cut of [`fallback`, `fallbackDurable`] as const) + violations.push( + ...persistedStartupFallbackViolations(h, scope, cut, observation[cut]), + ) for (const cut of [ `startup`, `startupDurable`, @@ -180,9 +249,15 @@ export function persistedStartupViolations( } declareOracle({ - model: [expectedPersistedStartup], - grammar: [persistedStartupMembershipArb, persistedStartupHistoryArb], + model: [expectedPersistedStartup, expectedStartupFallback], + grammar: [ + persistedStartupMembershipArb, + persistedStartupRenderArb, + withStartupRender, + persistedStartupHistoryArb, + ], check: [ + persistedStartupFallbackViolations, persistedStartupCutViolations, persistedStartupPremiseViolations, persistedStartupViolations, diff --git a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts index 0541d4fb7b..c1789b9be9 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts @@ -2,7 +2,9 @@ * Shared production composition for persisted-startup-oracle.ts. The adapter * supplies a real Collection, delivery control, and (for scoped sources) its * metadata projection. The host supplies storage and holds the first row read. - * Seed through public persisted sync, then compare before any refresh, inside + * Seed through public persisted sync. Network-first fallback compares saved rows + * before delivery; the source result compares at readiness and applied receipts. + * Then compare before any refresh, inside * refresh fulfillment, after its durable read, and after a local-only reopen. */ import { createCollection } from '@tanstack/db' @@ -11,11 +13,13 @@ import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import { PersistedStartupViolation, persistedStartupCutViolations, + persistedStartupFallbackViolations, persistedStartupPremiseViolations, persistedStartupViolations, sortedStartupRows, startupRows, } from '../src/persisted-startup-oracle' +import type { SyncConfig } from '@tanstack/db' import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' import type { HeldCall } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' import type { @@ -40,6 +44,7 @@ export interface PersistedStartupSource { readonly size: number values: () => IterableIterator } + initialResult: Promise snapshot: () => StartupSnapshot deliver: () => Promise responseAvailable: () => boolean @@ -57,6 +62,50 @@ export interface PersistedStartupAdapter { id: string, ) => PersistedStartupSource | Promise } +// Observe the adapter's public sync boundary before persistence wraps it. +// markReady identifies its initial complete answer; applied receipts identify +// when that answer is visible (and durably accepted). Fallback preload alone +// cannot supply this checkpoint. This recorder supplies no writes or readiness. +export function trackStartupSync( + sync: SyncConfig, +) { + let resolve!: () => void + let reject!: (error: unknown) => void + const initialResult = new Promise((yes, no) => { + resolve = yes + reject = no + }) + void initialResult.catch(() => {}) + const tracked: SyncConfig = { + ...sync, + sync: (params) => { + const receipts: Array> = [] + return sync.sync({ + ...params, + commit: (signal) => { + const receipt = params.commit(signal) + receipts.push(receipt) + return receipt + }, + markReady: () => { + params.markReady() + void Promise.all(receipts).then(() => resolve(), reject) + }, + }) + }, + } + return { sync: tracked, initialResult } +} +export function startupInitialRender(h: PersistedStartupHistory) { + return h.initialRender + ? { + initialRender: { + strategy: `network-first` as const, + networkTimeoutMs: h.response === `after-deadline` ? 0 : 60_000, + }, + } + : {} +} export const startupTurn = () => new Promise((resolve) => setTimeout(resolve, 0)) export async function untilStartup(premise: () => boolean): Promise { @@ -134,24 +183,54 @@ export async function observePersistedStartup< : {}), } } + let observationReads = 0 const ready = current.collection.preload() await untilStartup(hold.isHeld) if (h.response === `before-restore`) await current.deliver() await startupTurn() const responseBeforeRestore = current.responseAvailable() && hold.isHeld() hold.release() + let fallback: StartupSnapshot | undefined + let fallbackDurable: StartupSnapshot | undefined + if (h.response === `after-deadline`) { + await ready + fallback = current.snapshot() + const violations = persistedStartupFallbackViolations( + h, + adapter.scope, + `fallback`, + fallback, + ) + if (current.responseAvailable()) + violations.push(`fallback did not precede the source response`) + if (violations.length) throw new PersistedStartupViolation(violations) + // Exclude the observer's own read from startup's one-row-scan budget. + const beforeRead = readRowCount() + fallbackDurable = await durableSnapshot() + observationReads += readRowCount() - beforeRead + const durableViolations = persistedStartupFallbackViolations( + h, + adapter.scope, + `fallbackDurable`, + fallbackDurable, + ) + if (durableViolations.length) + throw new PersistedStartupViolation(durableViolations) + await current.deliver() + } if (h.response === `after-restore`) { await untilStartup(() => current.collection.size > 0) await current.deliver() } await ready + if (h.response === `after-deadline`) await current.initialResult const startup = current.snapshot() if (responseBeforeRestore !== (h.response === `before-restore`)) throw new Error(`Startup response/restore premise was not reached`) onStartup() assertCut(`startup`, startup) // Work is captured before the observation's own durable read. - const startupRowReads = readRowCount() - initialReadCount + const startupRowReads = readRowCount() - initialReadCount - observationReads const premiseViolations = [ ...persistedStartupPremiseViolations(h, { responseBeforeRestore, @@ -183,6 +262,7 @@ export async function observePersistedStartup< const observation = { responseBeforeRestore, startupRowReads, + ...(fallback ? { fallback, fallbackDurable } : {}), startup, startupDurable, refresh, diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts index 271e0c0c4c..ad72bdb1af 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts @@ -1,6 +1,7 @@ /** * Receiving registration for persisted-startup-oracle.ts. Every adapter runs - * its legal grammar through both storage hosts, at the same four cuts and + * its legal grammar through both storage hosts, at fallback (when enabled), + * the first source result, durability, refresh and * reopen. Fixed histories and generated histories share driver and judgment. * Each host/campaign enforces its own reach; one cannot cover another's gap. */ @@ -18,7 +19,9 @@ import { } from '../../db/tests/oracle-config' import { expectedPersistedStartup, + expectedStartupFallback, persistedStartupCutViolations, + persistedStartupFallbackViolations, persistedStartupViolations, } from '../src/persisted-startup-oracle' import { observePersistedStartup } from './persisted-startup-driver' @@ -28,7 +31,11 @@ import type { PersistedStartupHistory } from '../src/persisted-startup-oracle' import type { PersistedStartupAdapter } from './persisted-startup-driver' declareOracle({ - check: [persistedStartupCutViolations, persistedStartupViolations], + check: [ + persistedStartupCutViolations, + persistedStartupFallbackViolations, + persistedStartupViolations, + ], }) export function registerPersistedStartupConformance< @@ -67,9 +74,28 @@ export function registerPersistedStartupConformance< ).not.toEqual([]) }, ) + oracleReplayTest(it)(`rejects a lost persisted fallback`, () => { + const h = { + ...histories[0]!, + initialRender: `network-first` as const, + response: `after-deadline` as const, + } + const saved = expectedStartupFallback(h, adapter.scope) + expect( + persistedStartupFallbackViolations(h, adapter.scope, `fallback`, { + ...saved, + rows: [], + }), + ).not.toEqual([]) + expect( + persistedStartupCutViolations(h, adapter.scope, `startupDurable`, saved), + ).not.toEqual([]) + }) const states = [ `response-before-restore`, `restore-before-response`, + `network-first-source-result`, + `network-first-fallback`, `empty-result`, `overlapping-update`, `omitted-row`, @@ -103,6 +129,12 @@ export function registerPersistedStartupConformance< ? `response-before-restore` : `restore-before-response`, ) + if (history.initialRender) + historyReach.reach( + history.response === `after-deadline` + ? `network-first-fallback` + : `network-first-source-result`, + ) if (history.next === 0) historyReach.reach(`empty-result`) if (history.next & history.previous) historyReach.reach(`overlapping-update`) @@ -120,6 +152,52 @@ export function registerPersistedStartupConformance< storage.close() } } + // A source that publishes after fallback but silently loses its durable + // writes must fail before refresh can repair it. Seed and restore use the + // real host; only the receiving source's persistence commits are dropped. + oracleReplayTest(it)( + `rejects lost source persistence after fallback`, + async () => { + const history = histories.find( + (h) => h.response === `after-deadline`, + )! + const storage = createStartupHost(host) + try { + await expect( + observePersistedStartup( + { + ...adapter, + create: (persistence, h, id) => + adapter.create( + { + ...persistence, + adapter: new Proxy(persistence.adapter, { + get(target, key) { + if (key === `applyCommittedTx`) + return async () => {} + const value: unknown = Reflect.get(target, key) + return typeof value === `function` + ? value.bind(target) + : value + }, + }), + }, + h, + id, + ), + }, + storage.persistence, + storage.holdBaseline, + history, + `lost-persistence-${host}`, + storage.readRowCount, + ), + ).rejects.toThrow(/startupDurable:/) + } finally { + storage.close() + } + }, + ) oracleReplayTest(it).each(histories)( `reconciles persisted startup: %j`, async (history) => { diff --git a/packages/electric-db-collection/tests/persisted-startup-driver.ts b/packages/electric-db-collection/tests/persisted-startup-driver.ts index 45a3a68948..ad80852a20 100644 --- a/packages/electric-db-collection/tests/persisted-startup-driver.ts +++ b/packages/electric-db-collection/tests/persisted-startup-driver.ts @@ -12,7 +12,11 @@ import { sortedStartupRows, startupRows, } from '../../db-adapter-conformance/src/persisted-startup-oracle' -import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { + startupInitialRender, + trackStartupSync, + untilStartup, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' import { electricCollectionOptions } from '../src/electric' import type { PersistedStartupHistory, @@ -50,17 +54,21 @@ export const electricStartupAdapter: PersistedStartupAdapter({ + id, + shapeOptions: { + url: `http://startup/shape-${id}`, + params: { table: `rows` }, + fetchClient, + }, + getKey: (row) => row.id, + }) + const startup = trackStartupSync(options.sync) const collection = createCollection( persistedCollectionOptions({ - ...electricCollectionOptions({ - id, - shapeOptions: { - url: `http://startup/shape-${id}`, - params: { table: `rows` }, - fetchClient, - }, - getKey: (row) => row.id, - }), + ...options, + sync: startup.sync, + ...startupInitialRender(history), persistence, }), ) @@ -116,6 +124,7 @@ export const electricStartupAdapter: PersistedStartupAdapter ({ rows: sortedStartupRows(collection.values()) }), deliver, responseAvailable: () => available, diff --git a/packages/powersync-db-collection/tests/persisted-startup-common.ts b/packages/powersync-db-collection/tests/persisted-startup-common.ts index b21b44554d..49880278ba 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-common.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-common.ts @@ -15,7 +15,11 @@ import { sortedStartupRows, startupRows, } from '../../db-adapter-conformance/src/persisted-startup-oracle' -import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { + startupInitialRender, + trackStartupSync, + untilStartup, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' import { powerSyncCollectionOptions } from '../src' import type { CommonPowerSyncDatabase } from '@powersync/common' import type { PersistedStartupHistory } from '../../db-adapter-conformance/src/persisted-startup-oracle' @@ -42,19 +46,23 @@ export function powerSyncStartupAdapterFor( }) let pending: (() => void) | undefined let available = false + const options = powerSyncCollectionOptions({ + id, + database, + table: schema.props.rows, + schema: rowSchema, + onDeserializationError: () => {}, + onLoad: () => + new Promise((resolve) => { + pending = resolve + }), + }) + const startup = trackStartupSync(options.sync) const collection = createCollection( persistedCollectionOptions({ - ...powerSyncCollectionOptions({ - id, - database, - table: schema.props.rows, - schema: rowSchema, - onDeserializationError: () => {}, - onLoad: () => - new Promise((resolve) => { - pending = resolve - }), - }), + ...options, + sync: startup.sync, + ...startupInitialRender(history), persistence, }), ) @@ -67,6 +75,7 @@ export function powerSyncStartupAdapterFor( } return { collection, + initialResult: startup.initialResult, snapshot: () => ({ rows: sortedStartupRows(collection.values()) }), deliver, responseAvailable: () => available, diff --git a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts index ab7e2840df..95f30715df 100644 --- a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts +++ b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts @@ -6,7 +6,11 @@ import { describe } from 'vitest' import fc from 'fast-check' import { declareOracle } from '../../db/tests/oracle-declaration' -import { persistedStartupMembershipArb } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { + persistedStartupMembershipArb, + persistedStartupRenderArb, + withStartupRender, +} from '../../db-adapter-conformance/src/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { queryStartupAdapter, @@ -43,6 +47,13 @@ const queryStartupHistoryArb: fc.Arbitrary = fc }, ) .map(({ membership, ...schedule }) => ({ ...membership, ...schedule })) + .chain((h) => + fc + .tuple(persistedStartupRenderArb, fc.boolean()) + .map(([network, fallback]) => + withStartupRender(h, network, fallback && h.origin === `fetch`), + ), + ) declareOracle({ grammar: [queryStartupHistoryArb] }) // Keep registration visible to Vitest's static typecheck collector as well diff --git a/packages/query-db-collection/tests/persisted-startup-driver.ts b/packages/query-db-collection/tests/persisted-startup-driver.ts index 65e39c61ed..d28d6632f7 100644 --- a/packages/query-db-collection/tests/persisted-startup-driver.ts +++ b/packages/query-db-collection/tests/persisted-startup-driver.ts @@ -10,8 +10,13 @@ import { queryCollectionOptions } from '../src/query' import { sortedStartupRows, startupRows, + withStartupRender, } from '../../db-adapter-conformance/src/persisted-startup-oracle' -import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { + startupInitialRender, + trackStartupSync, + untilStartup, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import type { PersistedStartupHistory, @@ -28,7 +33,7 @@ export type QueryStartupHistory = PersistedStartupHistory & { | `cached-disabled` | `cached-pending` } -export const queryStartupHistories: Array = [ +const baseQueryStartupHistories: Array = [ { origin: `fetch`, previous: 1, @@ -87,6 +92,12 @@ export const queryStartupHistories: Array = [ }, ] +export const queryStartupHistories = baseQueryStartupHistories.flatMap((h) => [ + h, + withStartupRender(h, true, false), + ...(h.origin === `fetch` ? [withStartupRender(h, true, true)] : []), +]) + type QueryMetadata = { queryCollection?: { owners?: Record } } | undefined const owners = (metadata: unknown, id: string) => @@ -134,24 +145,28 @@ export const queryStartupAdapter: PersistedStartupAdapter = if (event.type === `updated` && event.action.type === `success`) fetch.completed = true }) + const options = queryCollectionOptions({ + id, + queryKey: [id], + queryClient: client, + getKey: (row) => row.id, + enabled: !disabled, + staleTime: pending ? 0 : Infinity, + ...(h.origin.startsWith(`initial`) + ? { initialData: startupRows(h.next, 2) } + : {}), + queryFn: async () => { + fetch.calls++ + if (pending || h.response !== `before-restore`) await response + return startupRows(h.next, 2) + }, + }) + const startup = trackStartupSync(options.sync) const collection = createCollection( persistedCollectionOptions({ - ...queryCollectionOptions({ - id, - queryKey: [id], - queryClient: client, - getKey: (row) => row.id, - enabled: !disabled, - staleTime: pending ? 0 : Infinity, - ...(h.origin.startsWith(`initial`) - ? { initialData: startupRows(h.next, 2) } - : {}), - queryFn: async () => { - fetch.calls++ - if (pending || h.response === `after-restore`) await response - return startupRows(h.next, 2) - }, - }), + ...options, + sync: startup.sync, + ...startupInitialRender(h), persistence, }), ) @@ -166,10 +181,11 @@ export const queryStartupAdapter: PersistedStartupAdapter = }) return { collection, + initialResult: startup.initialResult, snapshot, deliver: async () => { if (h.origin === `fetch`) { - if (h.response === `after-restore`) releaseResponse() + if (h.response !== `before-restore`) releaseResponse() await untilStartup(() => fetch.completed) } }, diff --git a/packages/rxdb-db-collection/tests/persisted-startup-driver.ts b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts index f145f06a45..6007d6078d 100644 --- a/packages/rxdb-db-collection/tests/persisted-startup-driver.ts +++ b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts @@ -8,6 +8,10 @@ import { createCollection } from '@tanstack/db' import { createRxDatabase } from 'rxdb/plugins/core' import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' +import { + startupInitialRender, + trackStartupSync, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' import { @@ -65,19 +69,24 @@ export const rxdbStartupAdapter: PersistedStartupAdapter ({ rows: sortedStartupRows(collection.values()) }), deliver: () => { available = true diff --git a/packages/trailbase-db-collection/tests/persisted-startup-driver.ts b/packages/trailbase-db-collection/tests/persisted-startup-driver.ts index b3166d56ea..5f140f0cca 100644 --- a/packages/trailbase-db-collection/tests/persisted-startup-driver.ts +++ b/packages/trailbase-db-collection/tests/persisted-startup-driver.ts @@ -11,7 +11,11 @@ import { sortedStartupRows, startupRows, } from '../../db-adapter-conformance/src/persisted-startup-oracle' -import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { + startupInitialRender, + trackStartupSync, + untilStartup, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' import { trailBaseCollectionOptions } from '../src/trailbase' import type { Event, ListResponse, RecordApi } from 'trailbase' import type { @@ -33,16 +37,24 @@ export const trailBaseStartupAdapter: PersistedStartupAdapter + const options = trailBaseCollectionOptions< + StartupRow, + StartupRow, + string + >({ + id, + recordApi: api, + getKey: (row) => row.id, + syncMode: `eager`, + parse: {}, + serialize: {}, + }) + const startup = trackStartupSync(options.sync) const collection = createCollection( persistedCollectionOptions({ - ...trailBaseCollectionOptions({ - id, - recordApi: api, - getKey: (row) => row.id, - syncMode: `eager`, - parse: {}, - serialize: {}, - }), + ...options, + sync: startup.sync, + ...startupInitialRender(history), persistence, }), ) @@ -55,6 +67,7 @@ export const trailBaseStartupAdapter: PersistedStartupAdapter ({ rows: sortedStartupRows(collection.values()) }), deliver, responseAvailable: () => available, From 66cbad438b7982cd64e83bba560292b62cd34a80 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 08:31:13 -0700 Subject: [PATCH 10/24] Record network-first startup conformance evidence --- docs/contributing/oracle-coverage.md | 3 +- .../2026-11-10-network-first-startup.md | 159 ++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 docs/contributing/oracle-reviews/2026-11-10-network-first-startup.md diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index b1506dd76f..f6fca22407 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -7803,7 +7803,8 @@ premise. TrailBase has no deployed-server or SDK-wire claim, and the local RxDB and PowerSync sources supply no remote-replication claim. See [migration status](../../packages/db-adapter-conformance/MIGRATION.md), [original evidence](oracle-reviews/2026-11-09-issue-2367.md) and the -[all-adapter amendment](oracle-reviews/2026-11-10-persisted-startup-adapters.md). +[all-adapter amendment](oracle-reviews/2026-11-10-persisted-startup-adapters.md) and +[network-first evidence](oracle-reviews/2026-11-10-network-first-startup.md). ## Adapter conformance: eager live sources (#2118) diff --git a/docs/contributing/oracle-reviews/2026-11-10-network-first-startup.md b/docs/contributing/oracle-reviews/2026-11-10-network-first-startup.md new file mode 100644 index 0000000000..8dea41aa35 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-network-first-startup.md @@ -0,0 +1,159 @@ +# Network-first startup durability across eager adapters + +Reviewed oracle head: `3fe76d1484ae17e0fd28f2a35055b1b339cfbe95`. Production is unchanged from +`9c5d7b02a2055d45ede611e23498f3db96146726` throughout this extension. The +following evidence concerns new history and observation coverage, not another +production repair. Review method: self-review against the oracle rubric. + +## Law and ownership + +The SQLite guide's [Render after SQLite restore](../../guides/sqlite-persistence.md#render-after-sqlite-restore) +promises that network-first may release readiness from nonempty saved rows, +while network sync continues and its later changes apply. The glossary and +persisted-readiness oracle supply the same law. The shared eager-source +READY-3/PUB-7 law still requires complete-result replacement and persistence's +durable acceptance contract still applies. Enabling initial rendering changes +readiness, not the source's ownership or durable result. + +The readiness oracle already covers restore/deadline/remote-call ordering, but +its controlled persistence adapter discards writes. The shared startup owner +previously checked durable results on six adapters without enabling this option. +This extension closes that crossing in the existing owner. It introduces no +second lifecycle model and changes no production code. + +## Grammar, checkpoints, and independent judgment + +The existing three-key set model remains. Histories add network-first enabled +or omitted, and a held response after deadline. Enabled ordinary responses use +`networkTimeoutMs: 60_000`; fallback uses the documented immediate-render value +`0`, while source delivery remains explicitly held. Exact deadline threshold, +empty/failed restore, and remote-failure readiness remain with the readiness +owner. A fallback without the option is excluded by construction. Query's +initial/cache results remain before restore and cannot generate a late fallback. + +All previous fixed histories remain. Query now has 20; each other adapter has +12. All use the same model, driver and comparison on both Node hosts and OPFS. +The fixed and random campaigns retain 80 histories each per adapter/host, +1,920 generated histories total, with separate enforced reach in every campaign. + +Before source delivery in a fallback history, public and durable rows must equal +the original saved rows, including Query ownership. A source result that omits a +row or changes an overlap distinguishes saved state from source replacement. +An empty source result must remove old rows except for surviving peer support. +The new option and timing axes distinguish immediate fallback from waiting for +the source; removing either loses this integration case. The original membership, +response/restore, peer, and cache-origin axes retain their prior roles and bounds. + +After fallback, `preload()` cannot identify source completion. A recorder wraps +the adapter's public sync callbacks before persistence. It forwards all arguments +and returns each original commit receipt. The source's first `markReady()` plus +its applied receipts identify the first complete result. The recorder supplies +no writes and never calls readiness independently. Other histories keep the +original immediate post-preload comparison, so this does not defer that cut. + +The first source result is checked publicly and durably before refresh. Refresh +fulfillment, later durable state, and a local-only reopen retain their comparisons. +The startup work check still permits one full row read. It subtracts only the +observer's own explicit durable reads, including the new fallback read. + +## Measured generated reach + +Minimum required count is one per declared state per adapter, host and campaign. +These counts come from the final normal runs, not the fixed histories or mutants. +The existing response order, empty-result, overlap, omission, peer and origin +states also pass their enforced reach checks. + +| Adapter | Host | Campaign | Enabled source before deadline | Enabled fallback before source | +| --- | --- | --- | ---: | ---: | +| query | controlled | fixed | 24 | 7 | +| query | controlled | random | 35 | 8 | +| query | node-sqlite | fixed | 24 | 7 | +| query | node-sqlite | random | 24 | 11 | +| convex | controlled | fixed | 23 | 21 | +| convex | controlled | random | 25 | 27 | +| convex | node-sqlite | fixed | 23 | 21 | +| convex | node-sqlite | random | 23 | 16 | +| electric | controlled | fixed | 23 | 21 | +| electric | controlled | random | 28 | 17 | +| electric | node-sqlite | fixed | 23 | 21 | +| electric | node-sqlite | random | 17 | 22 | +| trailbase | controlled | fixed | 23 | 21 | +| trailbase | controlled | random | 20 | 15 | +| trailbase | node-sqlite | fixed | 23 | 21 | +| trailbase | node-sqlite | random | 18 | 22 | +| rxdb | controlled | fixed | 23 | 21 | +| rxdb | controlled | random | 13 | 24 | +| rxdb | node-sqlite | fixed | 23 | 21 | +| rxdb | node-sqlite | random | 20 | 23 | +| powersync | controlled | fixed | 23 | 21 | +| powersync | controlled | random | 25 | 15 | +| powersync | node-sqlite | fixed | 23 | 21 | +| powersync | node-sqlite | random | 21 | 28 | + +## RED, GREEN, and mutant evidence + +- **RED on unchanged production:** no new production mismatch. The expanded + Node and OPFS comparisons pass without any production edit. The earlier + production repair's RED results remain in the all-adapter evidence record. +- **GREEN:** 208 shared checks: 160 fixed histories, 24 generated campaign + checks, and 24 calibrations. Real Chromium worker/OPFS receives all 80 fixed + histories. All six adapters run on controlled and real Node SQLite hosts. +- **Neighboring readiness:** 404 runtime checks pass, including the unchanged + 390-history readiness oracle, with no type errors or unhandled errors. +- **Mutant:** for each adapter and Node host, a receiving-source wrapper drops + `applyCommittedTx` after real seed/restore, while source rows still publish. + All twelve executions fail the shared `startupDurable` comparison before a + refresh. These are assertion failures, not timeouts or setup failures. The + calibration requires that named checkpoint in the rejected error. Six new + checker controls also reject lost fallback rows and stale source durability. +- **Replay:** Query seed `2367`, path `0` runs one generated comparison per host, + with the 46 other registered checks skipped. The new grammar uses the same + direct replay interface. This replay reaches an initial-disabled source, + not fallback; normal campaigns supply fallback reach. +- **Static checks:** seven package typechecks, explicit ESLint, Prettier, + `git diff --check`, and `pnpm lint:changed` pass. No run budget decreases. + +The first Node-SQLite mutant attempt spread a class instance and omitted its +prototype methods. It failed adapter validation before comparison. Replacing +that fixture with a forwarding Proxy preserves bound methods, and the final +runs fail only at the intended durable comparison. This setup failure supplies +no mutant evidence. A missing ignored `jose@5.10.0` link in Convex's existing +auth-app fixture was restored locally before its successful typecheck. + +## Rubric audit and limits + +| Requirement | Outcome | +| --- | --- | +| ORC-001/002 | Public SQLite readiness and durable replacement laws. The model uses set replacement and unchanged saved state, with no production classifiers. | +| ORC-003/005/009 | Literate shared owner, named fallback versus source-result cuts, actual six-adapter receiving paths, exact rows/owners and durable storage. Applied receipts disambiguate source completion from earlier fallback readiness. | +| ORC-004/008 | Prior witnesses reconstruct. New render/timing axes have distinguishing outcomes. Saved-state and complete-result checkpoints need different predictions, without another state machine. Bounds and illegal fallback/cache schedules are explicit. | +| ORC-006/013 | Lost saved rows fail fallback. Dropped durable writes fail startupDurable after a valid fallback. Existing immediate post-preload cuts remain. Exact numeric threshold belongs to readiness, not this receiving witness. | +| ORC-007/010 | Same 80-history fixed/random campaigns and checked direct replay. Existing shared cleanup preserves primary comparison failures and secondary teardown diagnostics. | +| ORC-011 | No new shared-semantic fault requiring an independent second formulation was identified. This is not an exhaustive independence proof. | +| ORC-012/014/015/016 | This record ties evidence to the reviewed head. Both Node hosts and real OPFS receive the same held-source premise and judgment. Every generated campaign enforces both new reaches. Fixed histories and calibrations use shared models/checks, with no separate hand-written expected result. | + +The extension retains eager startup, nonempty saved baselines, one persistence +instance, three string keys/numeric values, and no initial-fetch mutation. +It does not establish failed I/O, concurrent writers, every intermediate +publication, on-demand windows, arbitrary metadata, or real remote replication +for the local RxDB and PowerSync receiving paths. Exact deadline and failure +readiness remain under their existing owners. No additional production defect +was found, and no production work, setup, batching, format, or bundle size changes +were needed for this extension. + +## Commands + +All commands run at the worktree root. Local dependency recovery overrides remain +uncommitted as documented in the original all-adapter record. + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/query-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false tests/persisted-startup-conformance-oracle.test.ts +# Repeat for convex, electric, trailbase, rxdb and powersync with their +# persisted-startup-conformance-oracle.property.test.ts receiving file. +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/browser-db-sqlite-persistence exec playwright test --config playwright.opfs.config.ts persisted-startup-conformance-oracle.opfs.spec.ts +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/db-sqlite-persistence-core exec vitest run --maxWorkers=2 --coverage.enabled=false tests/persisted-readiness-oracle.test.ts tests/persisted-readiness.test.ts +env TANSTACK_DB_ORACLE_PROPERTY=query.startup-ownership TANSTACK_DB_ORACLE_SEED=2367 TANSTACK_DB_ORACLE_PATH=0 pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/query-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false tests/persisted-startup-conformance-oracle.test.ts +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/db-adapter-conformance exec tsc --noEmit +# Also tsc --noEmit in each of the six receiving adapter packages. +pnpm --config.verifyDepsBeforeRun=false lint:changed +``` From 348ddac2ceeb486cf6cf5995363a50e4698a0220 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 09:07:33 -0700 Subject: [PATCH 11/24] Register startup oracle ownership and preserve gap verdicts --- ...tartup-conformance-oracle.property.test.ts | 7 +++++- .../tests/persisted-startup-driver.ts | 20 +++++++++------ .../tests/persisted-startup-oracle-suite.ts | 25 +++++++++++++++++++ ...tartup-conformance-oracle.property.test.ts | 7 +++++- ...tartup-conformance-oracle.property.test.ts | 7 +++++- ...rsisted-startup-conformance-oracle.test.ts | 7 +++++- ...tartup-conformance-oracle.property.test.ts | 7 +++++- ...tartup-conformance-oracle.property.test.ts | 7 +++++- 8 files changed, 74 insertions(+), 13 deletions(-) diff --git a/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index a0ab070588..f997c6e9e2 100644 --- a/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -12,7 +12,12 @@ import { registerPersistedStartupConformance } from '../../db-adapter-conformanc import { convexStartupAdapter } from './persisted-startup-driver' const historyArb = persistedStartupHistoryArb(`collection`) -declareOracle({ grammar: [historyArb] }) +// The receiving path judges persisted startup through this adapter entry. +// The shared model owns replacement; this list lets the gap CLI credit it. +declareOracle({ + owns: [`src/options.ts#convexCollectionOptions`], + grammar: [historyArb], +}) // Keep registration visible to Vitest's static typecheck collector as well // as its runtime collector; the shared suite registers the actual law checks. describe(`persisted startup conformance: Convex`, () => { diff --git a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts index c1789b9be9..05254a309f 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts @@ -126,6 +126,7 @@ export async function observePersistedStartup< id: string, readRowCount: () => number, onStartup: () => void = () => {}, + onViolation: (violations: Array) => void = () => {}, ): Promise { const seed = createCollection( persistedCollectionOptions({ @@ -153,6 +154,13 @@ export async function observePersistedStartup< return withOracleCleanup(async () => { source = await adapter.create(persistence, h, id) const current = source + // The Node runner asserts here so the gap CLI records a behavioral verdict + // at the original cut. Browser receivers keep the same structured error. + const rejectViolations = (violations: Array) => { + if (!violations.length) return + onViolation(violations) + throw new PersistedStartupViolation(violations) + } const assertCut = (cut: string, actual: StartupSnapshot) => { const violations = persistedStartupCutViolations( h, @@ -160,7 +168,7 @@ export async function observePersistedStartup< cut, actual, ) - if (violations.length) throw new PersistedStartupViolation(violations) + rejectViolations(violations) } const durableSnapshot = async (): Promise => { const stored = await persistence.adapter.loadResumeSnapshot( @@ -203,7 +211,7 @@ export async function observePersistedStartup< ) if (current.responseAvailable()) violations.push(`fallback did not precede the source response`) - if (violations.length) throw new PersistedStartupViolation(violations) + rejectViolations(violations) // Exclude the observer's own read from startup's one-row-scan budget. const beforeRead = readRowCount() fallbackDurable = await durableSnapshot() @@ -214,8 +222,7 @@ export async function observePersistedStartup< `fallbackDurable`, fallbackDurable, ) - if (durableViolations.length) - throw new PersistedStartupViolation(durableViolations) + rejectViolations(durableViolations) await current.deliver() } if (h.response === `after-restore`) { @@ -238,8 +245,7 @@ export async function observePersistedStartup< }), ...(current.checkStartup?.() ?? []), ] - if (premiseViolations.length) - throw new PersistedStartupViolation(premiseViolations) + rejectViolations(premiseViolations) const startupDurable = await durableSnapshot() assertCut(`startupDurable`, startupDurable) const refresh = await current.refresh().then(() => { @@ -274,7 +280,7 @@ export async function observePersistedStartup< adapter.scope, observation, ) - if (violations.length) throw new PersistedStartupViolation(violations) + rejectViolations(violations) return observation }, [() => reopened.cleanup()]) }, [() => hold.uninstall(), () => source?.dispose()]) diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts index ad72bdb1af..e672a9661b 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts @@ -18,6 +18,7 @@ import { oracleRuns, } from '../../db/tests/oracle-config' import { + PersistedStartupViolation, expectedPersistedStartup, expectedStartupFallback, persistedStartupCutViolations, @@ -59,6 +60,7 @@ export function registerPersistedStartupConformance< oracleReplayTest(it)( `rejects stale durability before a later refresh`, () => { + oracleRoute(`calibration`) const history = histories[0]! const stale = expectedPersistedStartup( { ...history, next: history.previous }, @@ -75,6 +77,7 @@ export function registerPersistedStartupConformance< }, ) oracleReplayTest(it)(`rejects a lost persisted fallback`, () => { + oracleRoute(`calibration`) const h = { ...histories[0]!, initialRender: `network-first` as const, @@ -144,6 +147,7 @@ export function registerPersistedStartupConformance< const responseOrigin = origin(history) if (responseOrigin) historyReach.reach(responseOrigin) }, + (violations) => expect(violations).toEqual([]), ) expect( persistedStartupViolations(history, adapter.scope, observation), @@ -158,9 +162,30 @@ export function registerPersistedStartupConformance< oracleReplayTest(it)( `rejects lost source persistence after fallback`, async () => { + oracleRoute(`calibration`) const history = histories.find( (h) => h.response === `after-deadline`, )! + // Controlled storage models one Collection; use a fresh host for + // each candidate so the clean run cannot seed the corrupted run. + const clean = createStartupHost(host) + try { + await observePersistedStartup( + adapter, + clean.persistence, + clean.holdBaseline, + history, + `clean-persistence-${host}`, + clean.readRowCount, + ) + } catch (error) { + // A broken clean trace is not a calibration candidate. The normal + // histories judge it; a second fault cannot calibrate that trace. + if (error instanceof PersistedStartupViolation) return + throw error + } finally { + clean.close() + } const storage = createStartupHost(host) try { await expect( diff --git a/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index aad9476ff3..146f4f0c15 100644 --- a/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -12,7 +12,12 @@ import { registerPersistedStartupConformance } from '../../db-adapter-conformanc import { electricStartupAdapter } from './persisted-startup-driver' const historyArb = persistedStartupHistoryArb(`collection`) -declareOracle({ grammar: [historyArb] }) +// The receiving path judges persisted startup through this adapter entry. +// The shared model owns replacement; this list lets the gap CLI credit it. +declareOracle({ + owns: [`src/electric.ts#electricCollectionOptions`], + grammar: [historyArb], +}) describe(`persisted startup conformance: Electric`, () => { registerPersistedStartupConformance({ property: `adapter.electric.persisted-startup`, diff --git a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index 45e4e5de01..a9b0fc1f71 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -12,7 +12,12 @@ import { registerPersistedStartupConformance } from '../../db-adapter-conformanc import { powerSyncStartupAdapter } from './persisted-startup-driver' const historyArb = persistedStartupHistoryArb(`collection`) -declareOracle({ grammar: [historyArb] }) +// The receiving path judges persisted startup through this adapter entry. +// The shared model owns replacement; this list lets the gap CLI credit it. +declareOracle({ + owns: [`src/powersync.ts#powerSyncCollectionOptions`], + grammar: [historyArb], +}) describe(`persisted startup conformance: PowerSync`, () => { registerPersistedStartupConformance({ property: `adapter.powersync.persisted-startup`, diff --git a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts index 95f30715df..0c3b5c5c1e 100644 --- a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts +++ b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts @@ -55,7 +55,12 @@ const queryStartupHistoryArb: fc.Arbitrary = fc ), ) -declareOracle({ grammar: [queryStartupHistoryArb] }) +// The receiving path judges persisted startup through this adapter entry. +// The shared model owns replacement; this list lets the gap CLI credit it. +declareOracle({ + owns: [`src/query.ts#queryCollectionOptions`], + grammar: [queryStartupHistoryArb], +}) // Keep registration visible to Vitest's static typecheck collector as well // as its runtime collector; the shared suite registers the actual law checks. describe(`persisted startup conformance: Query`, () => { diff --git a/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index 73dff454a6..0a8ee0ef9e 100644 --- a/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -12,7 +12,12 @@ import { registerPersistedStartupConformance } from '../../db-adapter-conformanc import { rxdbStartupAdapter } from './persisted-startup-driver' const historyArb = persistedStartupHistoryArb(`collection`) -declareOracle({ grammar: [historyArb] }) +// The receiving path judges persisted startup through this adapter entry. +// The shared model owns replacement; this list lets the gap CLI credit it. +declareOracle({ + owns: [`src/rxdb.ts#rxdbCollectionOptions`], + grammar: [historyArb], +}) describe(`persisted startup conformance: RxDB`, () => { registerPersistedStartupConformance({ property: `adapter.rxdb.persisted-startup`, diff --git a/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index 27f8622524..32c94f809d 100644 --- a/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -12,7 +12,12 @@ import { registerPersistedStartupConformance } from '../../db-adapter-conformanc import { trailBaseStartupAdapter } from './persisted-startup-driver' const historyArb = persistedStartupHistoryArb(`collection`) -declareOracle({ grammar: [historyArb] }) +// The receiving path judges persisted startup through this adapter entry. +// The shared model owns replacement; this list lets the gap CLI credit it. +declareOracle({ + owns: [`src/trailbase.ts#trailBaseCollectionOptions`], + grammar: [historyArb], +}) describe(`persisted startup conformance: TrailBase`, () => { registerPersistedStartupConformance({ property: `adapter.trailbase.persisted-startup`, From deae33c1b7afa554863ced20cbdab63ed9312fd3 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 09:18:28 -0700 Subject: [PATCH 12/24] Record startup gap CLI results and unresolved distinctions --- docs/contributing/oracle-coverage.md | 14 ++ .../2026-11-10-startup-gap-cli.md | 183 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 docs/contributing/oracle-reviews/2026-11-10-startup-gap-cli.md diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index f6fca22407..15d67c5072 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -7806,6 +7806,20 @@ and PowerSync sources supply no remote-replication claim. See [all-adapter amendment](oracle-reviews/2026-11-10-persisted-startup-adapters.md) and [network-first evidence](oracle-reviews/2026-11-10-network-first-startup.md). +The [diff-focused gap CLI audit](oracle-reviews/2026-11-10-startup-gap-cli.md) +records mutation evidence separately from startup conformance. Open receiving +distinctions are unwrapped retained-row reload and empty-load mutation ordering +for RxDB/PowerSync (`M9`/`M164`, core subscription-lifecycle owner with adapter +receivers), cleanup during TrailBase's held persisted hydration (`M150`, its +lifecycle owner plus this persistence composition), and Query's redundant +hydration/ownership classification (`M1466`/`M1458`, ownership-lifecycle and +reload-work owners), older fetch generations crossing newer direct writes +(`M1778`/`M1779`, direct-write-window owner), and derived-select results without +an inverse cache mapping (`M1733`, ownership-lifecycle/direct-write-window). +The audit names each required history and observation. These survivors are +unresolved sensitivity signals, not approved equivalences or known product +counterexamples. They do not narrow the startup comparisons above. + ## Adapter conformance: eager live sources (#2118) `packages/db-adapter-conformance/src/eager-live-source-oracle.ts` owns the diff --git a/docs/contributing/oracle-reviews/2026-11-10-startup-gap-cli.md b/docs/contributing/oracle-reviews/2026-11-10-startup-gap-cli.md new file mode 100644 index 0000000000..6d8805b1e9 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-startup-gap-cli.md @@ -0,0 +1,183 @@ +# Persisted-startup gap CLI audit + +Reviewed oracle integration: `348ddac2ceeb486cf6cf5995363a50e4698a0220`. +Production is unchanged from `66cbad438b7982cd64e83bba560292b62cd34a80`. +Diff base: `ea3c920c1540486d0fb7410e089baa3a1583299c` (`origin/main`). +The oracle author performed this audit. It is not an independent review. + +## Law and scope + +The shared persisted-startup owner judges complete replacement of saved rows, +with scoped ownership for Query, before refresh can repair the result. Its +network-first histories also preserve saved public and durable rows during +fallback and replace them after the first complete source result. The authority, +generated reach, original RED evidence and real-host limits remain in +[network-first startup](2026-11-10-network-first-startup.md) and +[adapter startup](2026-11-10-persisted-startup-adapters.md). + +This audit selects every operator mutant on production lines changed by this +PR, plus five authored semantic controls. It selects 24 of 2,651 Query mutants, +5 of 131 RxDB mutants, 5 of 528 PowerSync mutants, and 7 of 368 TrailBase +mutants. This is a diff-focused audit, not a whole-file audit. Convex and +Electric production files do not change in this PR. Their startup receiving +comparisons still run in the normal six-adapter validation. + +The CLI is the locked `@tanstack/oracles` package at `fe532f4`. Each package +runs both unmutated baselines with seed 2367 before its selected mutations. +Ordinary tests are not credited as oracle kills. Authored controls are +attributed to `/root (oracle author; sensitivity control, not independent +review)`. + +```sh +pnpm gap-report --package packages/rxdb-db-collection \ + --source src/rxdb.ts --diff-base origin/main --seed 2367 --contexts 3 \ + --only rxdb.ts:M5,rxdb.ts:M6,rxdb.ts:M7,rxdb.ts:M8,rxdb.ts:M9,authored:pending-baseline-is-not-snapshot \ + --mutants --out +``` + +The other packages use their corresponding changed-line IDs below. Scratch +artifacts retain the exact selection JSON, authored transformations, logs, +partial captures and completed `results.json` reports. The local dependency +recovery overrides remain uncommitted. Commands add +`--config.verifyDepsBeforeRun=false` for that local installation only. + +## Test integration repairs + +Each of the six startup registration files now declares ownership of its +adapter entry point. The shared model and checks remain shared. The Node +production driver reports its existing violation list through Vitest's assertion +at the original comparison cut, so the CLI can distinguish a law rejection +from an uncaught runtime failure. Browser receivers retain structured errors. +No expected value, history, observation cut, or run budget changed. + +Calibration tests declare the calibration route. The dropped-write calibration +first runs the clean candidate and skips a candidate already rejected by the +normal law. Clean and corrupted runs use distinct fresh storage hosts, since +the controlled host models one Collection rather than multiple IDs. + +Initial attempts exposed missing ownership and failure classification. Their +setup/runtime errors do not count as behavioral kills. Stopped full-file scans +are not completed audits. The normal test run also found ignored temporary CLI +copies left by stopped scans. Those copies were removed, and validation was +repeated with the temporary-copy pattern excluded. No product test was excluded. + +## Results + +| Receiver | Selected | Classified | Law rejections | Survivors | No final classification | +| --- | ---: | ---: | ---: | ---: | ---: | +| Query | 26 | 21 | 16 | 5 | 5 | +| RxDB | 6 | 6 | 5 | 1 | 0 | +| PowerSync | 6 | 6 | 5 | 1 | 0 | +| TrailBase | 8 | 8 | 7 | 1 | 0 | +| Total | 46 | 41 | 33 | 8 | 5 | + +There are no reported production crashes among the classified mutants. +Completed report IDs are `debe30ba9ef2` (RxDB), `9c853138a811` (PowerSync), +`2938619712c4` (TrailBase), and `4e559092f456` (Query authored controls). +Query's operator evidence combines the preserved partial result streams from +`gaps-query-diff` and `gaps-query-remaining`, without counting duplicate results. + +All five authored controls fail the shared startup comparison on a reproducible +fixed-history route. The CLI stops at the first rejecting receiver: these +kills credit the controlled host, not an additional Node SQLite or OPFS run. +The earlier real-host RED evidence remains separate. + +| Authored control | Transformation | Rejecting history | +| --- | --- | --- | +| Query `skip-baseline-await` | Replace the hydrated-baseline condition with `false`. | fetched response before restore, previous mask 1, next mask 2 | +| Query `cached-results-ignore-baseline` | Keep only retained-query revalidation in `needsPersistedBaseline`. | fetched response before restore, previous mask 1, next mask 2 | +| RxDB `pending-baseline-is-not-snapshot` | Remove the persistence-capability operand from snapshot selection. | response before restore, previous mask 1, next mask 2 | +| PowerSync `pending-baseline-is-not-snapshot` | Remove the persistence-capability operand from snapshot selection. | response before restore, previous mask 1, next mask 2 | +| TrailBase `sweep-before-baseline` | Replace baseline hydration with `await Promise.resolve()`. | response before restore, previous mask 1, next mask 2 | + +Query operator mutants `M1451` through `M1455` have no final classification. +The CLI repeatedly aborts their group with `has no unit and no earlier kill` +for `includes-work-counter-oracle.test.ts` or `ssr-dehydration-oracle.test.ts`. +A one-context retry also fails. A reduced group fails at `M1455`. The dependency +was not patched to manufacture a result. These five are neither kills nor +survivors. The Query authored-only run completes independently. + +Selected operator IDs are Query `M1451–M1458`, `M1462–M1470`, `M1733`, +`M1775–M1780`; RxDB `M5–M9`; PowerSync `M160–M164`; and TrailBase `M36`, +`M145–M150`. The remaining CLI work is to repair or upgrade the CLI's unit +accounting and rerun the five unclassified Query operators. This audit remains +incomplete rather than reporting a fabricated whole-PR mutation score. + +## Surviving distinctions + +A surviving mutant is a coverage signal, not proof of a product defect or +permission to remove a guard. None below is approved as globally equivalent. +The coverage map retains these exact open distinctions. + +- **RxDB `M9` and PowerSync `M164`:** remove the negation of + `acceptedSyncedEntries().next().done`. Persistence makes the left operand + true, so these mutations cannot alter snapshot selection on the persisted + receiving path. The bare eager-source grammar reaches the right operand but + starts empty after cleanup and does not reload retained rows. The missing + receiving witness is an unwrapped retained-row reload that changes or omits + a key, plus a fresh empty load while an optimistic transaction is persisting. + Authority: the glossary's reload/snapshot contract and accepted sync ordering. + The shared eager-source owner explicitly excludes reload. The Collection + subscription-lifecycle grammar owns the core boundary and needs adapter + receivers for this distinction. This audit does not establish the broader + unwrapped reload claim. +- **TrailBase `M150`:** replace `if (cancelled) return` after + `hydrateBaseline()` with `if (false) return`. Classified `generator-gap`: + no startup history cleans up during that held await. The needed witness holds + persisted restore, completes the list, cleans up, optionally starts a new + sync run, then releases the old restore. Compare old-work rejection, current + public/durable rows and resource release. Its lifecycle owner is + `tests/lifecycle-oracle.property.test.ts` under `tests/ORACLE.md`; the startup + composition supplies the real persistence receiving path. Core fencing may + make the extra work publicly inert, but that hypothesis is not an executed + equivalence result. +- **Query `M1466`:** always await optional baseline hydration, including + branches that retain the scoped baseline path. No observed difference in + the reached histories. A distinguishing case must show an owed work/order + difference for an on-demand or retained-query result. Owner: + `tests/ownership-lifecycle.oracle.test.ts`, with the persistence reload work + owner for scan counts. No new performance promise follows from this survivor. +- **Query `M1458`:** replace `queryToRows.has(queryHash)` with `undefined`, so + every eager persisted result takes the reconciliation route. The normal + startup law cannot distinguish redundant selection after its first result. + The next witness needs a later result with established ownership and an + observable work or ordering difference. Owner: Query ownership lifecycle + and the persistence reload work oracle. This is not evidence of repeated + physical row reads. +- **Query `M1778` and `M1779`:** change or omit the recorded fetch-start + generation fallback. The missing distinction is an older fetch/application + crossing a newer direct write while persisted reconciliation is held. Owner: + `tests/persisted-direct-write-window.oracle.test.ts` and its shared reload + receiving driver. The physical-fetch clock can supply redundant evidence, + so source inspection alone cannot prove a public violation. +- **Query `M1733`:** omit the baseline classifier in the derived-select branch + whose cache envelope has no inverse. The next witness crosses that branch + with a retained persisted baseline and a newer direct write, then compares + rows and durability at application settlement. Owner: Query ownership + lifecycle and direct-write window. Ordinary startup histories do not supply + this combination. + +These records do not retire any law, approve an exclusion, or classify an +unreproduced product violation as fixed. The initial complete-result replacement +claim keeps its original scope. A broader claim that all changed decisions are +fully judged remains unproven. + +## GREEN and limits + +After the integration repairs, all 208 normal shared startup checks pass: +160 fixed histories, 24 generated campaigns (80 histories each), and 24 +calibrations. Every adapter/host/campaign keeps its own enforced reach. +All 80 fixed Chromium worker/OPFS histories pass the same comparisons. +The first local OPFS attempt timed out before page completion. Its rerun +completed in 21.4 seconds. That timeout supplies no behavioral RED evidence. +The shared package typecheck, ESLint, Prettier and `pnpm lint:changed` pass. + +No production repair follows from this audit. The integration repair preserves +model independence, finite history domains, original checkpoint enforcement, +first-source readiness/applied receipts, and error-preserving cleanup. There is +no new grammar or model rule, so ORC-004/008/009 controls remain those recorded +for the original owner. ORC-001/002/003/005/007/010/013/014/015/016 remain at the +same boundaries. ORC-006 gains the CLI evidence, with internal CLI failures +kept separate from law rejections. ORC-011 has no newly identified shared +semantic fault. This record supplies the ORC-012 integration audit and its +explicit unresolved mutation distinctions. From d94d73f34bff17b87e01231bfc415f37541800c4 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 10:18:02 -0700 Subject: [PATCH 13/24] Fix persisted startup ownership behind optimistic transactions Read accepted restored Query ownership and permit an absent optional TrailBase metadata API. Extend the shared startup grammar across optimistic holds and empty saved baselines, prove real source delivery on RxDB/PowerSync, move the oracle outside src, and repair RxDB test module resolution. Preserve all ten review dispositions and measured snapshot costs in the evidence record. Law: complete eager results replace their persisted baseline within their scope, including accepted rows whose application an optimistic transaction holds. Authority: READY-3/PUB-7, scope-local omission, durable acceptance and the glossary. SyncParams.metadata remains optional. Reach: 24 generated campaigns x 80 histories, independently enforced on six adapters and two Node hosts; 392 fixed histories; 196 real Chromium OPFS histories. Each path reaches optimistic holds and empty saved baselines. RED: extended Query oracle before production edits rejects 32 Node checks and 14 OPFS histories. TrailBase absent-metadata histories reject four checks. GREEN: 440 shared startup checks, 196 OPFS histories, 439 surrounding checks, and all 89 normal RxDB package tests pass. Seven package typechecks, conformance build, changed-file lint and ESLint pass. Mutant: original Query accepted-to-applied ownership and RxDB/PowerSync snapshot decisions reject 112 Node checks and the OPFS comparison. Removing earlier-commit waiting rejects six direct-write comparisons. Limits: finite three-key histories, one disjoint persisting optimistic insert, local RxDB/PowerSync storage and controlled provider boundaries. No arbitrary overlapping mutations, failed I/O, remote replication, every intermediate publication, or elapsed-time startup guarantee. No run budget is reduced. --- .changeset/fix-persisted-query-startup.md | 2 +- docs/collections/query-collection.md | 10 +- docs/contributing/oracle-coverage.md | 18 +- .../oracle-reviews/2026-11-09-issue-2367.md | 4 +- .../2026-11-10-persisted-startup-adapters.md | 2 +- .../2026-11-10-pr-2394-external-review.md | 361 ++++++++++++++++++ ...ed-startup-conformance-oracle.opfs.spec.ts | 3 +- .../e2e/persisted-startup-conformance.opfs.ts | 26 +- ...tartup-conformance-oracle.property.test.ts | 2 +- .../tests/persisted-startup-driver.ts | 6 +- packages/db-adapter-conformance/README.md | 14 +- .../tests/persisted-startup-driver.ts | 96 ++++- .../tests/persisted-startup-oracle-suite.ts | 10 +- .../persisted-startup-oracle.ts | 67 +++- ...tartup-conformance-oracle.property.test.ts | 2 +- .../tests/persisted-startup-driver.ts | 6 +- .../tests/persisted-startup-common.ts | 15 +- ...tartup-conformance-oracle.property.test.ts | 2 +- packages/query-db-collection/src/query.ts | 7 +- ...rsisted-direct-write-window.oracle.test.ts | 5 +- ...rsisted-startup-conformance-oracle.test.ts | 12 +- .../tests/persisted-startup-driver.ts | 26 +- ...tartup-conformance-oracle.property.test.ts | 2 +- .../tests/persisted-startup-driver.ts | 14 +- packages/rxdb-db-collection/vite.config.ts | 10 + .../trailbase-db-collection/src/trailbase.ts | 7 +- .../trailbase-db-collection/tests/ORACLE.md | 3 + .../tests/lifecycle-oracle.property.test.ts | 87 ++++- ...tartup-conformance-oracle.property.test.ts | 2 +- .../tests/persisted-startup-driver.ts | 6 +- 30 files changed, 719 insertions(+), 108 deletions(-) create mode 100644 docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md rename packages/db-adapter-conformance/{src => tests}/persisted-startup-oracle.ts (83%) diff --git a/.changeset/fix-persisted-query-startup.md b/.changeset/fix-persisted-query-startup.md index f6614bf3ee..dd4774f04b 100644 --- a/.changeset/fix-persisted-query-startup.md +++ b/.changeset/fix-persisted-query-startup.md @@ -5,4 +5,4 @@ '@tanstack/trailbase-db-collection': patch --- -Remove obsolete persisted rows when an eager source's first complete result overlaps SQLite restore. Query Collection also handles initial data and cached results while preserving rows owned by other Queries. +Remove obsolete persisted rows when an eager source's first complete result overlaps SQLite restore. Query Collection also handles initial data and cached results while preserving rows owned by other Queries, including when a persisting optimistic transaction holds restored rows before publication. TrailBase also accepts a sync boundary without the optional metadata API. diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index 658fb6622b..04a8b3ca09 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -303,9 +303,13 @@ If `queryFn` is missing at runtime, `queryCollectionOptions` throws `QueryFnRequ Eager Query Collections support TanStack Query's `initialData` and `initialDataUpdatedAt` options. Initial data has the original Query response -shape, is stored in the Query cache, and is immediately materialized as -normalized collection rows. TanStack Query uses `initialDataUpdatedAt` together -with `staleTime` to decide whether to fetch. +shape and is stored in the Query cache. Without persistence, it is immediately +materialized as normalized collection rows. A persisted Collection first loads +its durable baseline, then reconciles the initial data and its query ownership. +With the default initial-render strategy, await `preload()` before reading the +reconciled result. Network-first rendering can expose saved rows at its deadline +while source reconciliation continues. TanStack Query uses +`initialDataUpdatedAt` together with `staleTime` to decide whether to fetch. ```typescript const serverRenderedAt = Date.now() diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 15d67c5072..1c3f3be863 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -5104,7 +5104,7 @@ native e2e needs `cargo`. React Suspense reveals a covered failure about 300 ms late (its fallback throttle), so the contract's before-wait checkpoint sits at 450 ms. -The readiness fixture does not retain writes. The [shared adapter startup owner](../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts) +The readiness fixture does not retain writes. The [shared adapter startup owner](../../packages/db-adapter-conformance/tests/persisted-startup-oracle.ts) receives the network-first contract on all six eager adapters. It compares saved rows and durable state while the source is held past the deadline, then the first complete source result and its durability before refresh, plus reopen. @@ -7730,7 +7730,7 @@ See the [versioned amendment](oracle-reviews/2026-10-30-release-replay-retention ## Adapter conformance: persisted startup (#2367) -The [shared persistence owner](../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts) +The [shared persistence owner](../../packages/db-adapter-conformance/tests/persisted-startup-oracle.ts) judges complete source replacement across persisted restore. A collection-wide source replaces every row. A scoped source removes only its old ownership; an omitted row with a surviving peer stays. Authority: READY-3/PUB-7, the Query @@ -7752,7 +7752,7 @@ Each uses the same [composition driver](../../packages/db-adapter-conformance/tests/persisted-startup-driver.ts), judgment, controlled storage, and real Node SQLite hosts. Per host, fixed and random campaigns each run 80 generated histories with separate enforced reach. -Histories cross nonempty prior subsets of three keys, any next subset weighted +Histories cross empty and nonempty prior subsets of three keys, any next subset weighted toward empty, changed overlap values, and response before/after persisted restore. A generated initial-render axis enables network-first and crosses source responses before the deadline with a source held past it. Every adapter, @@ -7772,7 +7772,7 @@ snapshots also check exact ownership at all four snapshot cuts. Startup checks the reload owner's one-full-row-read law before its own durable observation. Query's receiving driver preserves exact fetch-count and pending-fetch premises. The [Chromium OPFS receiver](../../packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts) -runs twenty Query histories and twelve complete-result histories for each of the +runs forty-six Query histories and thirty complete-result histories for each of the other five adapters through the same composition and check on real worker/OPFS. The original Query omission fails startup on both Node hosts and OPFS. Convex @@ -7794,8 +7794,11 @@ and OPFS files are retired after receiving parity; Query-specific ownership laws and the unrelated direct-write/failure-fidelity repairs remain. Limits: eager startup, three string keys and numeric values, one persistence -instance, one source scope plus an optional persisted peer, and no mutation -during the initial fetch. No on-demand window, +instance, one source scope plus an optional persisted peer, and a disjoint optimistic +insert held across the first source commit. Its settlement precedes the first +complete-result comparison. Source commits and persisted restore can both be +accepted while that transaction holds their application. Arbitrary mutation +contents and overlapping optimistic transactions remain outside this grammar. No on-demand window, concurrent tab writer, arbitrary metadata namespace, failed-I/O, or every intermediate-publication claim. Electric's resumed delta-only streams belong to its recovery owner; a fresh full shape supplies this family's replacement @@ -7804,7 +7807,8 @@ and PowerSync sources supply no remote-replication claim. See [migration status](../../packages/db-adapter-conformance/MIGRATION.md), [original evidence](oracle-reviews/2026-11-09-issue-2367.md) and the [all-adapter amendment](oracle-reviews/2026-11-10-persisted-startup-adapters.md) and -[network-first evidence](oracle-reviews/2026-11-10-network-first-startup.md). +[network-first evidence](oracle-reviews/2026-11-10-network-first-startup.md), and +[external-review amendment](oracle-reviews/2026-11-10-pr-2394-external-review.md). The [diff-focused gap CLI audit](oracle-reviews/2026-11-10-startup-gap-cli.md) records mutation evidence separately from startup conformance. Open receiving diff --git a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md index 643722d0ad..3cb58992da 100644 --- a/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md +++ b/docs/contributing/oracle-reviews/2026-11-09-issue-2367.md @@ -34,7 +34,7 @@ it does not judge a Query response's replacement of persisted ownership. The failure was a missing history and receiving-path crossing, not evidence that those narrower laws were wrong. -The new [shared dimension](../../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts) +The new [shared dimension](../../../packages/db-adapter-conformance/tests/persisted-startup-oracle.ts) uses independent set replacement. It removes the old Query edges, adds the response's edges, and retains rows with a peer owner. It copies no production query cache, reconciliation branch, or startup state machine. @@ -476,7 +476,7 @@ Convex production is unchanged. Review started at `3044e20470b226ec642bf232222f09dee8e84fd4` with the migration diff. The new primary owner is -[`persisted-startup-oracle.ts`](../../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts). +[`persisted-startup-oracle.ts`](../../../packages/db-adapter-conformance/tests/persisted-startup-oracle.ts). Its shared composition, host fixtures, and receiving suite live under `packages/db-adapter-conformance/tests/persisted-startup-*`. Query and Convex supply their real Collections and delivery controls. Query diff --git a/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md index 2dfe7c3d51..0e1b9568f9 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md +++ b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-adapters.md @@ -2,7 +2,7 @@ Review base: `736b167ec196f878288f631aff4e82180d9827a5`. This amendment extends the [prior Query/Convex evidence](2026-11-09-issue-2367.md#shared-persistence-conformance-migration-2026-11-09). -The shared [persisted-startup oracle](../../../packages/db-adapter-conformance/src/persisted-startup-oracle.ts) +The shared [persisted-startup oracle](../../../packages/db-adapter-conformance/tests/persisted-startup-oracle.ts) keeps its law, independent set model, history grammar, comparisons and 80-history run budget. Four receiving adapters now join Query and Convex. The earlier statement that they were optional remaining work was incomplete: the public diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md new file mode 100644 index 0000000000..ed5860cec4 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md @@ -0,0 +1,361 @@ +# PR 2394: external review of persisted startup + +Reviewed head: `deae33c1b7afa554863ced20cbdab63ed9312fd3`. +The external reviewer supplied ten findings and ran no tests. This amendment +records self-review and executable verification. Earlier versioned records +retain their original, narrower campaigns; they do not establish this extension. + +## Law, experiment, and inference + +A complete source result replaces its persisted baseline in its declared scope. +A scoped Query result removes only its own ownership. A persisting optimistic +transaction can hold application after restore acceptance; that delay does not +remove accepted rows or ownership from the next source result's baseline. +Authority: the glossary's accepted sync transaction and optimistic state laws, +Query's scope-local omission contract, READY-3/PUB-7, and durable acceptance. + +The shared [oracle](../../../packages/db-adapter-conformance/tests/persisted-startup-oracle.ts) +keeps the independent set judgment. Its new grammar crosses a disjoint optimistic +insert with restore and the first source result, then settles it before checking +the complete result. The unconfirmed insert drops at settlement. No production +queue or ownership algorithm computes the expected result. Empty saved baselines +also enter the grammar. An empty restore cannot supply deadline fallback, so the +grammar excludes that schedule; the readiness owner retains its loading law. + +The production driver waits for a source commit invocation, or source markReady +when a complete empty answer needs no commit, before settling the optimistic +transaction. It waits for applied receipts before comparing that result. The +first public and durable cuts still precede refresh. Refresh fulfillment, +post-refresh storage, local-only reopen, and one-full-row-read comparisons remain. +A restore-first schedule uses the advertised baseline acceptance boundary, +including when restore contains no row. Row nonemptiness is not its witness. + +RxDB now proves source commit invocation before releasing restore. PowerSync +uses one-row batches and proves that commits contain every row the fixture's +complete table supplies before releasing restore. Gate release alone establishes +neither premise. Each Node campaign and OPFS adapter has separate enforced reach. + +**RED:** Before production edits, the optimistic extension fails on both +controlled storage and real Node SQLite: 32 Query checks fail, 56 pass. The +smallest generated trace is a saved `a`, an empty initial result, and a persisting +optimistic insert while restore is held. After settlement `a` remains. A peer +history instead retains the obsolete source ownership on an omitted peer row. +The real Chromium worker/OPFS run reproduces fourteen Query startup mismatches +before the repair; all six receiving adapters ran. Setup/type errors and an +initial browser timeout are excluded from this evidence. + +**Repair:** Query enumerates accepted synced entries when recovering baseline +ownership, matching the metadata API's accepted-transaction view. It no longer +misses a restored row whose application is held. No extra persistence scan, +queue, or lifecycle state is introduced. + +**GREEN:** The shared Node campaign passes 440 checks: 392 fixed histories, +24 generated registrations (80 histories each, 1,920 generated histories), and +24 calibrations. The OPFS receiver passes 196 histories: 46 Query and 30 per other +adapter. Surrounding Query ownership/direct-write and TrailBase lifecycle suites +pass another 439 checks. All Node campaigns separately enforce response ordering, +network-first result/fallback, empty result/baseline, overlap, omission, and the +persisting mutation; Query additionally enforces peers and each cache origin. + +**Mutants:** Reintroducing applied-row Query ownership and the original +RxDB/PowerSync snapshot selection rejects startup comparisons on both Node hosts +and OPFS. Removing the direct-write wait for earlier commits produces six +comparison failures. These are assertion failures, not setup failures or timeouts. +The prior applied-row Query implementation is the original-defect control for the +new optimistic histories. TrailBase's original validator call rejects both +optional-metadata fixed histories and both generated campaigns before its repair. + +**Limits:** Three string keys, numeric values, one disjoint optimistic insert, +one eager source scope and optional Query peer, and local-only reopen. This does +not establish arbitrary overlapping optimistic transactions, on-demand windows, +concurrent tab writers, failed I/O, every intermediate publication, remote RxDB +or PowerSync replication, or deployed TrailBase wire behavior. Existing owners +retain those neighboring contracts. No elapsed-time startup benchmark is claimed. + +## Direct-write contract and test integrity (R1, R2) + +The documented boundary is earlier source commits, not Query cache success. +`writeUpdate` and `writeDelete` reject before their row is loaded. The same-path +matrix confirms zero source commits and row-not-found rejection while persisted +restore holds the first result. Insert/upsert succeed. Already committed holders +still require positive commit reach and match the unwrapped reference. + +A control that lets the first source result commit before restore instead admits +update/delete. The timing change is real, but the contract permits both event +orders. The oracle supplies the recorded commit event order to the unwrapped +reference; it never supplies production's rows or write verdict as the answer. +An unconditional positive-commit assertion on startup would invent a timing +promise. The six failures from removing the earlier-commit wait show that the +comparison still rejects a wrong result under the committed premise. + +R1 is accepted under the documented admission contract; its timing consequence +is retained here. R2's model-hacking/quiet-exclusion accusation is not established. +The test's causal-input explanation is clearer, and the mutation evidence is +retained. No contract comparison or budget was weakened for this evaluation. + +## Startup work under the snapshot contract (R4, R5) + +The same shared comparison and real Node SQLite receiving path record startup +persistence transactions for empty and three-row saved baselines, with a complete +three-row source answer. Original selection sends three source row mutations +without truncate. Current selection sends three source row mutations with +truncate. Seed writes are counted separately. Thus this PR adds a storage-wide +clear and metadata reset; it does not introduce the full N source row writes. +SQLite's truncate branch clears the row and tombstone tables and expected-key +set. Snapshot replacement is allowed to remove old row metadata. Neither RxDB +nor PowerSync consumes a provider resume cursor from this metadata. + +PowerSync's snapshot path additionally retains its source batches until commit. +RxDB already begins one transaction before paging and commits after the full +read; attributing that buffering to this PR would be inaccurate. The existing +one-full-wrapper-row-read guarantee remains checked. No throughput or elapsed +startup regression magnitude has been measured. + +The glossary's snapshot-transaction contract explicitly permits replacement of +all synced rows. These adapters do not promise bounded startup source batches or +preservation of per-row metadata across that replacement. The previously reviewed +startup budgets still pass: one full wrapper row read, skipped complete setup, +and hydration fairness. The added clear and PowerSync batch retention are real +costs of choosing that existing snapshot path. They remain documented costs, +not evidence that startup latency is unchanged. + +No new publication contract is needed to complete this repair. Progressive +publication would be a separate design change, and this amendment does not make +it. R4 and R5 are accepted under the existing snapshot contract, with the work +measurements and unmeasured latency limit preserved. This classification does +not assert that a new user decision approved the costs. + +## Repair location (R9) + +The wrapper receives explicit snapshot, delta, and scoped transactions. A first +eager commit is not evidence of a complete Collection-wide replacement. Query +has scoped ownership; TrailBase can commit pages before exhaustion; Electric can +resume from delta-only progress. Automatically treating a first commit as a +snapshot would discard legitimate peer or unmentioned rows. The existing +snapshot contract intentionally puts replacement authority at the source. + +The accepted-row ownership repair belongs to Query's ownership boundary. A +central helper could reduce repeated adapter code, but the proposed wrapper-only +repair lacks scope/exhaustion information and is not a substitute for these laws. + +## Other findings and loss audit + +| ID | Technical verdict and action | Durable value | +| --- | --- | --- | +| R1 | Accepted design: real timing change, no cache-success admission promise. | Preserve pre-commit rejection and committed-source admission witnesses. | +| R2 | Refuted as test-integrity defect; event order is a causal input. | Clarified prose; earlier-commit-wait mutant fails six checks. | +| R3 | Fixed now: accepted restored ownership was absent from the applied-row walk. | Shared optimistic-history dimension on every receiver. | +| R4 | Accepted design under the snapshot contract. Added clear and batch retention are real. | Preserve measured original/current work and the unmeasured latency limit. | +| R5 | Accepted design under the snapshot contract. Added clear is real. Full-read transaction predates PR. | Separate existing N writes from added truncate work. No provider resume cursor is consumed here. | +| R6 | Fixed now: absent optional metadata threw. | Lifecycle grammar crosses metadata presence and absence in both modes. | +| R7 | Fixed now: gate release was weaker than the declared premise. | Semantic commit evidence, multi-batch PowerSync, original-defect controls. | +| R8 | Fixed now as cleanup; external consumer break overstated. | Oracle moved to tests; package is private and root export omits this module. | +| R9 | Refuted as blanket wrapper repair. | Recorded information boundary and scoped/delta counterexamples. | +| R10 | Fixed now: both documentation statements were stale. | README covers six receivers; initialData docs distinguish persisted reconciliation. | + +R6 uses the existing TrailBase lifecycle model and comparisons. Its generated +metadata dimension is an omission of the entire optional API, not an incomplete +advertised capability. The validator still rejects a supplied API with a missing +persistence field. Presence/absence reach is separately enforced in fixed and +random 50-history campaigns. The unchanged production RED has four failures and +95 passes; repaired production passes all 99 checks. No test budget was lowered. + +R8's actual build emits startup declarations but no references to nonexistent +test imports in those declarations. The private package exports only its root; +the oracle is not a root export. Moving it beside its production driver and +suite prevents test dependencies under src and removes the unused declarations. +The build succeeds after relocation. Current imports, documentation links and +receiving paths follow the new location. + +Ten raw items are accounted for: five fixed now, two refuted, and three accepted +designs under existing contracts. No item is silently deferred or dropped. No +additional production defect was established while testing neighboring laws; +the empty-restore scheduling mistakes were repaired in the test driver/grammar. + +Reviewer assessment: **hire with reservations**. The accepted/applied ownership +crossing and weak delivery premises were strong findings, and the missing-metadata +case was correct. The review also found real costs and stale documentation. +Its admission-contract, package-consumer, and wrapper-centralization conclusions +were overstated, and running no tests left important distinctions unverified. + +The review accounting and authorized repairs are complete. Whole-PR readiness +is not assessed here. The prior gap CLI audit remains incomplete, and the final +amendment still needs CI verification. No elapsed-time performance claim follows +from the work counters. + +## Oracle-rubric audit + +ORC-001/002: authorities above; unchanged independent set model and differential +reference. ORC-003: contract, grammar, production driver, and cuts remain explained +beside their code. ORC-004: removing mutation loses queued-restore ownership; +removing timing loses the restore race; zero/nonzero baselines distinguish empty +fallback legality; scope peers forbid global replacement. The three-key range +includes empty, overlap, omission and complete membership. Duplicate source keys, +Collection-wide peers and empty deadline fallback are excluded. +ORC-005: all six actual adapter entry points and controlled/Node/OPFS storage +receivers run; first-result durable observations still precede refresh. +ORC-006: original-defect and earlier-commit-wait controls fail comparisons. +ORC-007: existing fixed/random campaigns and 80-history budgets remain; no replay +parser changes. ORC-008: no reference-model state was added. A scheduling axis was added. The optimistic insert drops +before the compared source result, so no model queue is needed. +ORC-009: accepted/visible boundaries follow the glossary; firstSubmission combines +a source commit invocation and an empty source's markReady solely for scheduling. +ORC-010: shared withOracleCleanup retains primary errors and cleanup diagnostics. +ORC-011: no new shared-semantic-fault hypothesis warrants another set model; the +existing unwrapped direct-write differential remains separate. +ORC-012: this record names the reviewed head, limits, ten dispositions and +measured startup costs. ORC-013: empty/nonempty, before/after restore, peers, and +optimistic/no-optimistic histories distinguish the promised conditional laws. +ORC-014: real QueryClient, SDK/provider boundaries, Node SQLite and worker/OPFS +receive the same judgment. ORC-015: each receiving campaign enforces separate +reach. The final measured counts appear below. +ORC-016: fixed histories use the shared model and comparison without their own +expected answers. The work-count experiment also reuses that composition and +judgment; it is diagnostic performance evidence, not a new product oracle law. + + +## Final receiving evidence + +The following counts come from the final repaired run. Each generated +registration executes 80 histories and enforces its own reach. Columns count +response-before-restore, restore-before-response, a persisting optimistic +transaction, an empty saved baseline, and network-first deadline fallback. +Counts overlap because one history can reach several states. + +| Adapter, host, campaign | Before | After | Mutation | Empty baseline | Fallback | +| --- | ---: | ---: | ---: | ---: | ---: | +| convex.controlled.fixed | 37 | 43 | 38 | 39 | 5 | +| convex.controlled.random | 37 | 43 | 40 | 36 | 7 | +| convex.node-sqlite.fixed | 37 | 43 | 38 | 39 | 5 | +| convex.node-sqlite.random | 33 | 47 | 42 | 40 | 8 | +| electric.controlled.fixed | 37 | 43 | 38 | 39 | 5 | +| electric.controlled.random | 29 | 51 | 41 | 40 | 15 | +| electric.node-sqlite.fixed | 37 | 43 | 38 | 39 | 5 | +| electric.node-sqlite.random | 33 | 47 | 38 | 40 | 10 | +| powersync.controlled.fixed | 37 | 43 | 38 | 39 | 5 | +| powersync.controlled.random | 36 | 44 | 43 | 42 | 10 | +| powersync.node-sqlite.fixed | 37 | 43 | 38 | 39 | 5 | +| powersync.node-sqlite.random | 35 | 45 | 37 | 41 | 12 | +| query.controlled.fixed | 54 | 26 | 38 | 41 | 10 | +| query.controlled.random | 63 | 17 | 41 | 43 | 5 | +| query.node-sqlite.fixed | 54 | 26 | 38 | 41 | 10 | +| query.node-sqlite.random | 56 | 24 | 41 | 36 | 7 | +| rxdb.controlled.fixed | 37 | 43 | 38 | 39 | 5 | +| rxdb.controlled.random | 37 | 43 | 44 | 40 | 10 | +| rxdb.node-sqlite.fixed | 37 | 43 | 38 | 39 | 5 | +| rxdb.node-sqlite.random | 26 | 54 | 33 | 43 | 9 | +| trailbase.controlled.fixed | 37 | 43 | 38 | 39 | 5 | +| trailbase.controlled.random | 37 | 43 | 45 | 37 | 12 | +| trailbase.node-sqlite.fixed | 37 | 43 | 38 | 39 | 5 | +| trailbase.node-sqlite.random | 26 | 54 | 38 | 40 | 11 | + +Chromium uses the same fixed histories, production driver and comparison. +Its browser-local counters enforce each receiving path independently, without +importing the Node runner's AsyncLocalStorage dependency. + +| OPFS receiver | Histories | Before | After | Mutation | Empty baseline | +| --- | ---: | ---: | ---: | ---: | ---: | +| Query | 46 | 34 | 12 | 23 | 6 | +| Convex | 30 | 18 | 12 | 15 | 6 | +| Electric | 30 | 18 | 12 | 15 | 6 | +| TrailBase | 30 | 18 | 12 | 15 | 6 | +| RxDB | 30 | 18 | 12 | 15 | 6 | +| PowerSync | 30 | 18 | 12 | 15 | 6 | + +Original-defect controls reject 112 of 236 Node checks across Query, RxDB and +PowerSync. Their Chromium run reaches every adapter's declared states and rejects +the same shared startup comparison. The repaired Node run passes all 440 checks. +The repaired Chromium run passes all 196 histories. + +The diagnostic work experiment uses `observePersistedStartup` with the Node +SQLite host, wraps `applyCommittedTx` to record `truncate` and mutation count, +and reports at the startup cut. It uses `next: 7`, `peer: false`, and +`response: 'before-restore'`, once with `previous: 0` and once with `previous: 7`. +Each receiver passes the same shared comparison under both original and current +snapshot selection. A nonempty baseline has a separate three-write seed +transaction. The source transaction counts are: + +| Receiver | Saved rows | Original row writes / truncate | Current row writes / truncate | +| --- | ---: | --- | --- | +| RxDB | 0 | 3 / false | 3 / true | +| RxDB | 3 | 3 / false | 3 / true | +| PowerSync | 0 | 3 / false | 3 / true | +| PowerSync | 3 | 3 / false | 3 / true | + +## Commands and validation limits + +The six-adapter Node run uses source aliases to avoid mixing built and source +copies of Collection transactions. This local test plumbing does not ship. +Save this config at the checkout root as `startup-review.config.ts`: + +```ts +import { resolve } from 'node:path' +import { defineConfig } from 'vitest/config' +const root = import.meta.dirname +export default defineConfig({ + root, + resolve: { + alias: { + '@tanstack/db': resolve(root, 'packages/db/src'), + '@tanstack/db-ivm': resolve(root, 'packages/db-ivm/src'), + '@tanstack/db-sqlite-persistence-core': resolve(root, 'packages/db-sqlite-persistence-core/src'), + }, + }, + test: { + environment: 'jsdom', + typecheck: { enabled: false }, + coverage: { enabled: false }, + testTimeout: 30000, + }, +}) +``` + +Run from the checkout root, then remove the temporary config: + +```sh +pnpm exec vitest run --config startup-review.config.ts --maxWorkers=2 \ + packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts \ + packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts \ + packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts \ + packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts \ + packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts \ + packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts + +pnpm --filter @tanstack/browser-db-sqlite-persistence exec playwright test \ + --config playwright.opfs.config.ts persisted-startup-conformance-oracle.opfs.spec.ts +``` + +For the Query RED/control, change the accepted ownership enumeration back to +`collection._state.syncedData.keys()` and destructure its key directly. For the +RxDB and PowerSync controls, remove `!!params.metadata?.persistence ||` from +snapshot selection. Run the same comparisons, then restore production and run +GREEN. For the TrailBase RED, remove the `params.metadata === undefined` guard. +For direct-write calibration, remove `ctx.earlierCommits?.()` in `manual-sync.ts`. +Its comparison rejects six histories instead of accepting collateral corruption. + +Surrounding validation runs Query's `ownership-lifecycle.oracle.test.ts`, +`persisted-direct-write-window.oracle.test.ts`, `persisted-direct-write.test.ts`, +and TrailBase's `lifecycle-oracle.property.test.ts` and `lifecycle.test.ts` through +the same source-aliased config: 439 checks pass. All seven affected package +`tsc --noEmit` commands pass. The conformance package build passes after the +move and no longer emits the unused startup declaration. + +Local registry recovery selects `@tanstack/query-core` 5.106.0 instead of +5.107.1, `fake-indexeddb` 6.2.7 instead of 6.2.8, and `@tailwindcss/postcss` +4.3.4 instead of 4.3.5 after HTTP 403 responses. Overrides and lockfile churn +remain uncommitted. The exact locked versions remain untested locally. + + +The ordinary RxDB package command exposed a separate receiving setup gap: +its Vite runner combined the built Collection with the shared driver's source +transaction stack. The first optimistic fixed histories timed out before their +checkpoint, and later fixture construction reported leaked RxDB instances. +These failures are not production RED evidence. Explicit test aliases in the +RxDB Vite config make DB, IVM and persistence core resolve to the same workspace +sources as the shared drivers. This changes test resolution only. The repaired normal package startup command +passes all 68 checks. The full normal RxDB suite passes all 89 checks across +three files. Its package typecheck also passes after the runner change. + + +Final staged validation: `pnpm lint:changed`, ESLint on every changed TypeScript +file, and `git diff --cached --check` pass. No run budget was lowered or removed. diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts index 587e3d8a91..dcb76c4703 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test' import { declareOracle } from '../../db/tests/oracle-declaration' import { queryStartupHistories } from '../../query-db-collection/tests/persisted-startup-driver' -import { persistedStartupHistories } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import { persistedStartupHistories } from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { StartupConformanceResult } from './persisted-startup-conformance.opfs' // The page compares each cut with the shared model. Count each adapter's @@ -27,6 +27,7 @@ declareOracle({ check: [checkStartupConformance] }) test(`complete source results replace persisted rows across OPFS startup`, async ({ page, }) => { + test.setTimeout(180_000) page.on(`console`, (message) => console.info(`browser ${message.type()}: ${message.text()}`), ) diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts index 16b3c9b8b4..a5dfc3359f 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts @@ -5,7 +5,7 @@ import { PersistedStartupViolation, persistedStartupHistories, persistedStartupViolations, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { isStartupRowRead, observePersistedStartup, @@ -24,7 +24,7 @@ import { createBrowserWASQLitePersistence, openBrowserWASQLiteOPFSDatabase, } from '../src' -import type { PersistedStartupHistory } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupHistory } from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' export type StartupConformanceResult = { @@ -48,6 +48,16 @@ async function run() { adapter: PersistedStartupAdapter, histories: Array, ) { + // This page has no Node oracle runner. Each completed history calls its + // startup observer once; fail locally if any required receiving state is absent. + const reach: Record = Object.fromEntries( + [ + `response-before-restore`, + `restore-before-response`, + `persisting-mutation`, + `empty-baseline`, + ].map((state) => [state, 0]), + ) for (const history of histories) { const index = sequence++ const database = await openBrowserWASQLiteOPFSDatabase({ @@ -68,6 +78,15 @@ async function run() { history, `startup-${index}`, () => rowReads, + () => { + reach[ + history.response === `before-restore` + ? `response-before-restore` + : `restore-before-response` + ]!++ + if (history.mutation) reach[`persisting-mutation`]!++ + if (history.previous === 0) reach[`empty-baseline`]!++ + }, ) results.push({ adapter: name, @@ -86,6 +105,9 @@ async function run() { await database.close?.() } } + for (const [state, count] of Object.entries(reach)) + if (!count) throw new Error(`${name}.opfs did not reach ${state}`) + console.info(`${name}.opfs ${JSON.stringify(reach)}`) } try { await receive(`query`, queryStartupAdapter, queryStartupHistories) diff --git a/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index f997c6e9e2..a33a306756 100644 --- a/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/convex-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -7,7 +7,7 @@ import { declareOracle } from '../../db/tests/oracle-declaration' import { persistedStartupHistories, persistedStartupHistoryArb, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { convexStartupAdapter } from './persisted-startup-driver' diff --git a/packages/convex-db-collection/tests/persisted-startup-driver.ts b/packages/convex-db-collection/tests/persisted-startup-driver.ts index 9ebce428e9..c1fd977d0c 100644 --- a/packages/convex-db-collection/tests/persisted-startup-driver.ts +++ b/packages/convex-db-collection/tests/persisted-startup-driver.ts @@ -14,10 +14,10 @@ import { convexCollectionOptions } from '../src' import { sortedStartupRows, startupRows, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import { ConvexProvider, rowsQuery } from './conformance-driver' -import type { PersistedStartupHistory } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupHistory } from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' export const convexStartupAdapter: PersistedStartupAdapter = @@ -62,6 +62,8 @@ export const convexStartupAdapter: PersistedStartupAdapter ({ rows: sortedStartupRows(collection.values()) }), deliver, responseAvailable: () => available, diff --git a/packages/db-adapter-conformance/README.md b/packages/db-adapter-conformance/README.md index cab57b5b64..cb8473e9b4 100644 --- a/packages/db-adapter-conformance/README.md +++ b/packages/db-adapter-conformance/README.md @@ -68,7 +68,7 @@ from `packages/db/tests/oracle-reach.ts`. ## Persisted startup conformance -`src/persisted-startup-oracle.ts` owns complete-result replacement across a +`tests/persisted-startup-oracle.ts` owns complete-result replacement across a persisted restore. It applies to both push adapters and client-cache adapters. The bare eager-source family does not generate persisted rows. Replacement uses the same READY-3/PUB-7 judgment, with scope-local ownership for sources @@ -83,10 +83,14 @@ public/durable rows before refresh, public rows inside refresh fulfillment, durable rows after refresh, and a local-only reopen. It also shares the reload owner's one-row-read law. A scoped adapter checks exact source/peer ownership. -Query receives the shared property with its five cache/fetch origins and peer -scope. Convex receives it over the real SDK and controlled socket. Both run -under their package's `test:oracles` command. Chromium OPFS receives eight Query -and four Convex histories through the same model, composition, and comparison: +Query receives the shared property with five cache/fetch origins and peer +ownership. Convex, Electric, TrailBase, RxDB and PowerSync also receive it through +their installed SDK or controlled provider boundary. Chromium worker/OPFS runs +46 Query histories and 30 histories for each other adapter. Every receiver +crosses ordinary and network-first rendering, empty and nonempty baselines, +and a persisting optimistic transaction. RxDB and PowerSync prove source +commit invocation before releasing a held restore; gate release alone does +not count as response delivery. ```sh pnpm --filter @tanstack/query-db-collection exec vitest run --coverage.enabled=false --typecheck.enabled=false tests/persisted-startup-conformance-oracle.test.ts diff --git a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts index 05254a309f..3949c6c8d3 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts @@ -7,7 +7,7 @@ * Then compare before any refresh, inside * refresh fulfillment, after its durable read, and after a local-only reopen. */ -import { createCollection } from '@tanstack/db' +import { createCollection, createTransaction } from '@tanstack/db' import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import { @@ -18,7 +18,7 @@ import { persistedStartupViolations, sortedStartupRows, startupRows, -} from '../src/persisted-startup-oracle' +} from './persisted-startup-oracle' import type { SyncConfig } from '@tanstack/db' import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' import type { HeldCall } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' @@ -28,7 +28,7 @@ import type { StartupRow, StartupScope, StartupSnapshot, -} from '../src/persisted-startup-oracle' +} from './persisted-startup-oracle' export function isStartupRowRead(sql: string): boolean { return sql @@ -41,10 +41,13 @@ export interface PersistedStartupSource { collection: { preload: () => Promise cleanup: () => Promise + insert: (row: StartupRow) => unknown readonly size: number values: () => IterableIterator } initialResult: Promise + firstSubmission: Promise + waitForRestore: () => Promise snapshot: () => StartupSnapshot deliver: () => Promise responseAvailable: () => boolean @@ -69,6 +72,17 @@ export interface PersistedStartupAdapter { export function trackStartupSync( sync: SyncConfig, ) { + let submit!: () => void + const firstSubmission = new Promise((resolve) => { + submit = resolve + }) + let hydrateBaseline: (() => Promise) | undefined + let committedRows = 0 + let committed = false + let resolveCommit!: () => void + const firstCommit = new Promise((resolve) => { + resolveCommit = resolve + }) let resolve!: () => void let reject!: (error: unknown) => void const initialResult = new Promise((yes, no) => { @@ -79,22 +93,46 @@ export function trackStartupSync( const tracked: SyncConfig = { ...sync, sync: (params) => { + hydrateBaseline = params.metadata?.persistence?.hydrateBaseline const receipts: Array> = [] + let writes = 0 return sync.sync({ ...params, + write: (message) => { + params.write(message) + writes++ + }, commit: (signal) => { const receipt = params.commit(signal) receipts.push(receipt) + committedRows += writes + writes = 0 + committed = true + resolveCommit() + submit() return receipt }, markReady: () => { params.markReady() + submit() void Promise.all(receipts).then(() => resolve(), reject) }, }) }, } - return { sync: tracked, initialResult } + return { + sync: tracked, + initialResult, + firstCommit, + firstSubmission, + hasCommitted: () => committed, + committedRowCount: () => committedRows, + waitForRestore: async () => { + if (!hydrateBaseline) + throw new Error(`Persisted receiver has no baseline capability`) + await hydrateBaseline() + }, + } } export function startupInitialRender(h: PersistedStartupHistory) { return h.initialRender @@ -151,6 +189,8 @@ export async function observePersistedStartup< const initialReadCount = readRowCount() const hold = holdBaseline(id) let source: PersistedStartupSource | undefined + let settleMutation: (() => void) | undefined + let mutationSettled: Promise | undefined return withOracleCleanup(async () => { source = await adapter.create(persistence, h, id) const current = source @@ -192,9 +232,34 @@ export async function observePersistedStartup< } } let observationReads = 0 + const holdMutation = async () => { + if (!h.mutation) return + let entered!: () => void + const started = new Promise((resolve) => { + entered = resolve + }) + const pending = new Promise((resolve) => { + settleMutation = resolve + }) + const tx = createTransaction({ + autoCommit: false, + mutationFn: () => { + entered() + return pending + }, + }) + tx.mutate(() => { + current.collection.insert({ id: `local`, value: 9 }) + }) + mutationSettled = tx.commit() + await started + } const ready = current.collection.preload() await untilStartup(hold.isHeld) - if (h.response === `before-restore`) await current.deliver() + if (h.response === `before-restore`) { + await holdMutation() + await current.deliver() + } await startupTurn() const responseBeforeRestore = current.responseAvailable() && hold.isHeld() hold.release() @@ -223,14 +288,25 @@ export async function observePersistedStartup< fallbackDurable, ) rejectViolations(durableViolations) + await holdMutation() await current.deliver() } if (h.response === `after-restore`) { - await untilStartup(() => current.collection.size > 0) + await current.waitForRestore() + await holdMutation() await current.deliver() } + if (h.mutation) { + // Commit invocation (or an empty source's markReady) precedes applied + // settlement. Waiting for its receipt + // here would wait for the very optimistic transaction this driver holds. + await current.firstSubmission + settleMutation!() + await mutationSettled + } await ready - if (h.response === `after-deadline`) await current.initialResult + if (h.response === `after-deadline` || h.mutation) + await current.initialResult const startup = current.snapshot() if (responseBeforeRestore !== (h.response === `before-restore`)) throw new Error(`Startup response/restore premise was not reached`) @@ -283,5 +359,9 @@ export async function observePersistedStartup< rejectViolations(violations) return observation }, [() => reopened.cleanup()]) - }, [() => hold.uninstall(), () => source?.dispose()]) + }, [ + () => settleMutation?.(), + () => hold.uninstall(), + () => source?.dispose(), + ]) } diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts index e672a9661b..83fccd0521 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts @@ -24,11 +24,11 @@ import { persistedStartupCutViolations, persistedStartupFallbackViolations, persistedStartupViolations, -} from '../src/persisted-startup-oracle' +} from './persisted-startup-oracle' import { observePersistedStartup } from './persisted-startup-driver' import { createStartupHost } from './persisted-startup-host' import type { Arbitrary } from 'fast-check' -import type { PersistedStartupHistory } from '../src/persisted-startup-oracle' +import type { PersistedStartupHistory } from './persisted-startup-oracle' import type { PersistedStartupAdapter } from './persisted-startup-driver' declareOracle({ @@ -79,7 +79,7 @@ export function registerPersistedStartupConformance< oracleReplayTest(it)(`rejects a lost persisted fallback`, () => { oracleRoute(`calibration`) const h = { - ...histories[0]!, + ...histories.find((history) => history.previous !== 0)!, initialRender: `network-first` as const, response: `after-deadline` as const, } @@ -100,8 +100,10 @@ export function registerPersistedStartupConformance< `network-first-source-result`, `network-first-fallback`, `empty-result`, + `empty-baseline`, `overlapping-update`, `omitted-row`, + `persisting-mutation`, ...(adapter.scope === `scoped` ? [`peer-owner`] : []), ...origins, ] @@ -138,6 +140,8 @@ export function registerPersistedStartupConformance< ? `network-first-fallback` : `network-first-source-result`, ) + if (history.mutation) historyReach.reach(`persisting-mutation`) + if (history.previous === 0) historyReach.reach(`empty-baseline`) if (history.next === 0) historyReach.reach(`empty-result`) if (history.next & history.previous) historyReach.reach(`overlapping-update`) diff --git a/packages/db-adapter-conformance/src/persisted-startup-oracle.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts similarity index 83% rename from packages/db-adapter-conformance/src/persisted-startup-oracle.ts rename to packages/db-adapter-conformance/tests/persisted-startup-oracle.ts index 00558cec7a..85364d85b8 100644 --- a/packages/db-adapter-conformance/src/persisted-startup-oracle.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts @@ -22,8 +22,11 @@ * * Limits: three string keys, numeric values, one eager persisted sync run and * local-only reopen. No on-demand windows, concurrent writers, failed I/O, or - * intermediate-publication claim. Drivers and host controls live in - * ../tests/persisted-startup-driver.ts and persisted-startup-host.ts. + * intermediate-publication claim. A disjoint optimistic insert can persist + * across restore and the first source commit. After its settlement, that + * unconfirmed insert drops (optimistic-history-oracle.ts); replacement has + * the same set answer. The mutation axis changes scheduling, not that answer. Drivers and host controls live in + * persisted-startup-driver.ts and persisted-startup-host.ts. */ import fc from 'fast-check' import { declareOracle } from '../../db/tests/oracle-declaration' @@ -34,6 +37,7 @@ export type PersistedStartupHistory = { next: number peer: boolean response: `before-restore` | `after-restore` | `after-deadline` + mutation?: `persisting` initialRender?: `network-first` } export type StartupRow = { id: string; value: number } @@ -96,18 +100,26 @@ export function expectedPersistedStartup( } } -// Nonempty durable baselines and any next subset include omissions, an empty +// Empty and nonempty durable baselines and any next subset include omissions, an empty // answer, equal membership, and changed values on overlap. Dropping the timing // axis loses the startup race; dropping peers admits a scoped global truncate. // A cached result is already available, so its receiving grammar restricts it // to before-restore. Duplicate keys and collection-wide peers are not legal. export const persistedStartupMembershipArb = (scope: StartupScope) => - fc.record({ - previous: fc.integer({ min: 1, max: 7 }), - next: fc.oneof(fc.constant(0), fc.integer({ min: 0, max: 7 })), - peer: scope === `scoped` ? fc.boolean() : fc.constant(false), - }) -// A deadline fallback is legal only with network-first enabled and a held + fc.oneof( + fc.record({ + previous: fc.constant(0), + next: fc.integer({ min: 0, max: 7 }), + peer: fc.constant(false), + }), + fc.record({ + previous: fc.integer({ min: 1, max: 7 }), + next: fc.oneof(fc.constant(0), fc.integer({ min: 0, max: 7 })), + peer: scope === `scoped` ? fc.boolean() : fc.constant(false), + }), + ) +// A deadline fallback requires a nonempty saved baseline (empty restore stays +// loading), network-first enabled, and a held // source result. Ordinary responses retain both restore orders. The deadline's // exact threshold belongs to the persisted-readiness oracle; here zero expires // the wait while the source is held, and 60 seconds keeps it pending otherwise. @@ -120,7 +132,10 @@ export function withStartupRender( return { ...h, ...(networkFirst ? { initialRender: `network-first` as const } : {}), - response: networkFirst && fallback ? `after-deadline` : h.response, + response: + networkFirst && fallback && h.previous !== 0 + ? `after-deadline` + : h.response, } } export const persistedStartupHistoryArb = ( @@ -129,30 +144,47 @@ export const persistedStartupHistoryArb = ( fc .tuple( persistedStartupMembershipArb(scope), + fc.boolean(), fc.constantFrom(`before-restore` as const, `after-restore` as const), ) - .chain(([membership, response]) => + .chain(([membership, mutation, response]) => fc .tuple(persistedStartupRenderArb, fc.boolean()) .map(([network, fallback]) => - withStartupRender({ ...membership, response }, network, fallback), + withStartupRender( + { + ...membership, + response, + ...(mutation ? { mutation: `persisting` as const } : {}), + }, + network, + fallback, + ), ), ) // These are inputs to the same judgment as generated histories. Receivers may // add adapter-specific origins, but never supply hand-written expected answers. const baseStartupHistories: Array = [ + { previous: 0, next: 7, peer: false, response: `before-restore` }, { previous: 1, next: 2, peer: false, response: `before-restore` }, { previous: 1, next: 0, peer: false, response: `before-restore` }, { previous: 3, next: 6, peer: false, response: `before-restore` }, { previous: 3, next: 6, peer: false, response: `after-restore` }, ] -export const persistedStartupHistories = baseStartupHistories.flatMap((h) => [ - h, - withStartupRender(h, true, false), - withStartupRender(h, true, true), -]) +export const withStartupMutations = ( + histories: Array, +): Array => + histories.flatMap((h) => [h, { ...h, mutation: `persisting` as const }]) + +export const persistedStartupHistories = withStartupMutations( + baseStartupHistories.flatMap((h) => [ + h, + withStartupRender(h, true, false), + withStartupRender(h, true, true), + ]), +) // Before any source result, timeout changes readiness only. Saved rows and // ownership must survive intact, both publicly and durably. Reusing replacement @@ -254,6 +286,7 @@ declareOracle({ persistedStartupMembershipArb, persistedStartupRenderArb, withStartupRender, + withStartupMutations, persistedStartupHistoryArb, ], check: [ diff --git a/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index 146f4f0c15..203a4b5f0a 100644 --- a/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/electric-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -7,7 +7,7 @@ import { declareOracle } from '../../db/tests/oracle-declaration' import { persistedStartupHistories, persistedStartupHistoryArb, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { electricStartupAdapter } from './persisted-startup-driver' diff --git a/packages/electric-db-collection/tests/persisted-startup-driver.ts b/packages/electric-db-collection/tests/persisted-startup-driver.ts index ad80852a20..3a8798530c 100644 --- a/packages/electric-db-collection/tests/persisted-startup-driver.ts +++ b/packages/electric-db-collection/tests/persisted-startup-driver.ts @@ -11,7 +11,7 @@ import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src import { sortedStartupRows, startupRows, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { startupInitialRender, trackStartupSync, @@ -21,7 +21,7 @@ import { electricCollectionOptions } from '../src/electric' import type { PersistedStartupHistory, StartupRow, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' type Request = { url: URL; respond: (response: Response) => void } @@ -125,6 +125,8 @@ export const electricStartupAdapter: PersistedStartupAdapter ({ rows: sortedStartupRows(collection.values()) }), deliver, responseAvailable: () => available, diff --git a/packages/powersync-db-collection/tests/persisted-startup-common.ts b/packages/powersync-db-collection/tests/persisted-startup-common.ts index 49880278ba..9b99499902 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-common.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-common.ts @@ -14,7 +14,7 @@ import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src import { sortedStartupRows, startupRows, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { startupInitialRender, trackStartupSync, @@ -22,7 +22,7 @@ import { } from '../../db-adapter-conformance/tests/persisted-startup-driver' import { powerSyncCollectionOptions } from '../src' import type { CommonPowerSyncDatabase } from '@powersync/common' -import type { PersistedStartupHistory } from '../../db-adapter-conformance/src/persisted-startup-oracle' +import type { PersistedStartupHistory } from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' const schema = new Schema({ rows: new Table({ value: column.integer }) }) @@ -45,11 +45,11 @@ export function powerSyncStartupAdapterFor( ]) }) let pending: (() => void) | undefined - let available = false const options = powerSyncCollectionOptions({ id, database, table: schema.props.rows, + syncBatchSize: 1, schema: rowSchema, onDeserializationError: () => {}, onLoad: () => @@ -66,19 +66,24 @@ export function powerSyncStartupAdapterFor( persistence, }), ) + const responseAvailable = () => + startup.hasCommitted() && + startup.committedRowCount() >= startupRows(history.next, 2).length const deliver = async () => { await untilStartup(() => pending !== undefined) const release = pending! pending = undefined - available = true release() + await untilStartup(responseAvailable) } return { collection, initialResult: startup.initialResult, + firstSubmission: startup.firstSubmission, + waitForRestore: startup.waitForRestore, snapshot: () => ({ rows: sortedStartupRows(collection.values()) }), deliver, - responseAvailable: () => available, + responseAvailable, refresh: async () => { const reloaded = collection.reload() await deliver() diff --git a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index a9b0fc1f71..c377adedd6 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -7,7 +7,7 @@ import { declareOracle } from '../../db/tests/oracle-declaration' import { persistedStartupHistories, persistedStartupHistoryArb, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { powerSyncStartupAdapter } from './persisted-startup-driver' diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 48b4624f5b..fb70042198 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1674,8 +1674,8 @@ export function queryCollectionOptions( const ownedRows = new Set() const hydratedOwners = Array.from( - collection._state.syncedData.keys(), - (rowKey) => [rowKey, getPersistedOwners(rowKey)] as const, + collection._state.acceptedSyncedEntries(), + ([rowKey]) => [rowKey, getPersistedOwners(rowKey)] as const, ) for (const [rowKey, owners] of hydratedOwners) { if (owners.size === 0) { @@ -2897,7 +2897,8 @@ export function queryCollectionOptions( selectedItems ?? validateSuccessfulResultItemsForApplication(queryKey, result) // Eager restore already owns the complete row read and its key evidence. - // Await that publication before diffing, rather than scanning it again. + // Await its acceptance before diffing, rather than scanning it again. + // A persisting optimistic transaction can still hold its application. const useHydratedBaseline = syncMode === `eager` && !retainedQueriesPendingRevalidation.has(hashedQueryKey) diff --git a/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts b/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts index 54628be5ad..ae3bdae2b9 100644 --- a/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts +++ b/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts @@ -65,7 +65,10 @@ import type { Op, Row, StorageKind } from './persisted-direct-write-support' * write is held, so a later direct write waits for that earlier commit. * * The production driver records source commits before the direct write. That - * causal order selects the reference schedule, never its expected row values. + * causal order selects the reference schedule, never its expected row values + * or write outcome. Commit invocation is an input event to this differential + * law. Cache success is not that event. Requiring a commit in `startup` would + * impose an ordering that the direct-write contract does not promise. * With no earlier commit, the reference holds its first result until after the * direct write. An early update/delete can then reject because no row is loaded * (query-collection.md, "Individual Write Operations"). Both committed holders diff --git a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts index 0c3b5c5c1e..e4841b4f9b 100644 --- a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts +++ b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts @@ -10,7 +10,7 @@ import { persistedStartupMembershipArb, persistedStartupRenderArb, withStartupRender, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { queryStartupAdapter, @@ -49,9 +49,13 @@ const queryStartupHistoryArb: fc.Arbitrary = fc .map(({ membership, ...schedule }) => ({ ...membership, ...schedule })) .chain((h) => fc - .tuple(persistedStartupRenderArb, fc.boolean()) - .map(([network, fallback]) => - withStartupRender(h, network, fallback && h.origin === `fetch`), + .tuple(persistedStartupRenderArb, fc.boolean(), fc.boolean()) + .map(([network, fallback, mutation]) => + withStartupRender( + { ...h, ...(mutation ? { mutation: `persisting` as const } : {}) }, + network, + fallback && h.origin === `fetch`, + ), ), ) diff --git a/packages/query-db-collection/tests/persisted-startup-driver.ts b/packages/query-db-collection/tests/persisted-startup-driver.ts index d28d6632f7..9388c5aa46 100644 --- a/packages/query-db-collection/tests/persisted-startup-driver.ts +++ b/packages/query-db-collection/tests/persisted-startup-driver.ts @@ -10,8 +10,9 @@ import { queryCollectionOptions } from '../src/query' import { sortedStartupRows, startupRows, + withStartupMutations, withStartupRender, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { startupInitialRender, trackStartupSync, @@ -22,7 +23,7 @@ import type { PersistedStartupHistory, StartupRow, StartupSnapshot, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' export type QueryStartupHistory = PersistedStartupHistory & { @@ -34,6 +35,13 @@ export type QueryStartupHistory = PersistedStartupHistory & { | `cached-pending` } const baseQueryStartupHistories: Array = [ + { + origin: `fetch`, + previous: 0, + next: 7, + peer: false, + response: `before-restore`, + }, { origin: `fetch`, previous: 1, @@ -92,11 +100,13 @@ const baseQueryStartupHistories: Array = [ }, ] -export const queryStartupHistories = baseQueryStartupHistories.flatMap((h) => [ - h, - withStartupRender(h, true, false), - ...(h.origin === `fetch` ? [withStartupRender(h, true, true)] : []), -]) +export const queryStartupHistories = withStartupMutations( + baseQueryStartupHistories.flatMap((h) => [ + h, + withStartupRender(h, true, false), + ...(h.origin === `fetch` ? [withStartupRender(h, true, true)] : []), + ]), +) type QueryMetadata = { queryCollection?: { owners?: Record } } | undefined @@ -182,6 +192,8 @@ export const queryStartupAdapter: PersistedStartupAdapter = return { collection, initialResult: startup.initialResult, + firstSubmission: startup.firstSubmission, + waitForRestore: startup.waitForRestore, snapshot, deliver: async () => { if (h.origin === `fetch`) { diff --git a/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index 0a8ee0ef9e..bc4a5790cd 100644 --- a/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -7,7 +7,7 @@ import { declareOracle } from '../../db/tests/oracle-declaration' import { persistedStartupHistories, persistedStartupHistoryArb, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { rxdbStartupAdapter } from './persisted-startup-driver' diff --git a/packages/rxdb-db-collection/tests/persisted-startup-driver.ts b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts index 6007d6078d..b160b60859 100644 --- a/packages/rxdb-db-collection/tests/persisted-startup-driver.ts +++ b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts @@ -17,13 +17,13 @@ import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src import { sortedStartupRows, startupRows, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { rxdbCollectionOptions } from '../src/rxdb' import type { RxCollection } from 'rxdb/plugins/core' import type { PersistedStartupHistory, StartupRow, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' let sequence = 0 @@ -56,7 +56,6 @@ export const rxdbStartupAdapter: PersistedStartupAdapter void - let available = false const response = new Promise((resolve) => { release = resolve }) @@ -87,13 +86,14 @@ export const rxdbStartupAdapter: PersistedStartupAdapter ({ rows: sortedStartupRows(collection.values()) }), - deliver: () => { - available = true + deliver: async () => { release() - return Promise.resolve() + await startup.firstCommit }, - responseAvailable: () => available, + responseAvailable: startup.hasCommitted, refresh: () => collection.reload(), dispose: async () => { release() diff --git a/packages/rxdb-db-collection/vite.config.ts b/packages/rxdb-db-collection/vite.config.ts index aa589f6669..cb355c6c75 100644 --- a/packages/rxdb-db-collection/vite.config.ts +++ b/packages/rxdb-db-collection/vite.config.ts @@ -1,9 +1,19 @@ +import { resolve } from 'node:path' import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' import packageJson from './package.json' const config = defineConfig({ test: { + // Shared persistence drivers and this adapter must use one transaction stack. + alias: { + '@tanstack/db': resolve(import.meta.dirname, `../db/src`), + '@tanstack/db-ivm': resolve(import.meta.dirname, `../db-ivm/src`), + '@tanstack/db-sqlite-persistence-core': resolve( + import.meta.dirname, + `../db-sqlite-persistence-core/src`, + ), + }, name: packageJson.name, dir: `./tests`, environment: `jsdom`, diff --git a/packages/trailbase-db-collection/src/trailbase.ts b/packages/trailbase-db-collection/src/trailbase.ts index 48456d8919..5bb5feab23 100644 --- a/packages/trailbase-db-collection/src/trailbase.ts +++ b/packages/trailbase-db-collection/src/trailbase.ts @@ -187,9 +187,10 @@ export function trailBaseCollectionOptions< const sync = { sync: (params: SyncParams) => { const { begin, write, commit, markReady, fail, collection } = params - const persistence = validateSyncPersistenceCapability( - params.metadata?.persistence, - ) + const persistence = + params.metadata === undefined + ? null + : validateSyncPersistenceCapability(params.metadata.persistence) let cancelled = false let periodicCleanupTask: ReturnType | undefined const cleanup = () => { diff --git a/packages/trailbase-db-collection/tests/ORACLE.md b/packages/trailbase-db-collection/tests/ORACLE.md index 19ef24acf9..dbab31084d 100644 --- a/packages/trailbase-db-collection/tests/ORACLE.md +++ b/packages/trailbase-db-collection/tests/ORACLE.md @@ -11,6 +11,9 @@ The same interpreter runs a fixed corpus and generated histories. ## Laws and scope - Published inserts, updates and deletes agree with the model after each event. +- The optional `SyncParams.metadata` API can be absent. Generated lifecycle + histories cross absence and presence without changing the row or lifecycle + law. An advertised API must still forward its `persistence` field intact. - Eager startup waits for its list; on-demand startup does not claim to load rows. - A list starts only after the change subscription is established, in both diff --git a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts index 083210aef7..0bfa7e4896 100644 --- a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts +++ b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts @@ -1,5 +1,5 @@ import { fc, test as fcTest } from '@fast-check/vitest' -import { expect, it, vi } from 'vitest' +import { afterAll, expect, it, vi } from 'vitest' import { createCollection, createLiveQueryCollection, eq } from '@tanstack/db' import { oracleFixedSeed, @@ -8,6 +8,7 @@ import { oracleRouteOf, oracleRuns, } from '../../db/tests/oracle-config' +import { createOracleReach } from '../../db/tests/oracle-reach' import { declareOracle } from '../../db/tests/oracle-declaration' import { trailBaseCollectionOptions } from '../src/trailbase' import { MockRecordApi } from './mock-record-api' @@ -66,6 +67,7 @@ type SyncRunPlan = } type Scenario = { mode: `eager` | `on-demand` + metadata?: `absent` syncRuns: Array } @@ -76,9 +78,11 @@ type Scenario = { * The model never reads adapter bookkeeping, pending transactions or caches. */ async function checkLifecycle( - { mode, syncRuns }: Scenario, + { mode, syncRuns, metadata }: Scenario, providerFault?: `list` | `set` | `buffered`, + metadataReach?: ReturnType, ) { + metadataReach?.beginHistory() const api = new MockRecordApi() let current = source() api.subscribe.mockImplementation(() => current.subscription.promise) @@ -102,20 +106,35 @@ async function checkLifecycle( const intervals = vi.spyOn(globalThis, `setInterval`) const cleared = vi.spyOn(globalThis, `clearInterval`) process.on(`unhandledRejection`, onUnhandled) - const collection = createCollection( - trailBaseCollectionOptions({ - recordApi: api, - getKey: (row: Row) => row.id, - syncMode: mode, - parse: { - value: (value) => { - if (value === invalidValue) throw failure - return value - }, + const options = trailBaseCollectionOptions({ + recordApi: api, + getKey: (row: Row) => row.id, + syncMode: mode, + parse: { + value: (value) => { + if (value === invalidValue) throw failure + return value }, - serialize: {}, - }), - ) + }, + serialize: {}, + }) + // SyncParams.metadata is optional. A receiving wrapper may omit the whole + // API; an advertised API must still forward its persistence field intact. + const collection = createCollection({ + ...options, + sync: { + ...options.sync, + sync: (params) => { + metadataReach?.reach( + metadata === `absent` ? `metadata-absent` : `metadata-present`, + ) + return options.sync.sync({ + ...params, + ...(metadata === `absent` ? { metadata: undefined } : {}), + }) + }, + }, + }) let expected = new Map() // Model entries come only from command scalars. Provider objects may be // changed before parse without changing any expected row. @@ -399,6 +418,7 @@ const syncRunArb: fc.Arbitrary = fc.oneof( }), ) const scenarioArb = fc.record({ + metadata: fc.option(fc.constant(`absent` as const), { nil: undefined }), mode: fc.constantFrom(`eager` as const, `on-demand` as const), syncRuns: fc.array(syncRunArb, { minLength: 1, @@ -441,6 +461,17 @@ const corpus: Array<{ syncRuns: [{ kind }, live(`close`)], })), ] +it.each([`eager`, `on-demand`] as const)( + `preserves lifecycle without a metadata API: %s`, + (mode) => { + oracleRoute(`fixed history`) + return checkLifecycle({ + mode, + metadata: `absent`, + syncRuns: [live(`close`)], + }) + }, +) it.each( corpus.flatMap((entry) => ([`eager`, `on-demand`] as const).map((mode) => ({ ...entry, mode })), @@ -477,12 +508,33 @@ it.each([`list`, `set`, `buffered`] as const)( }) }, ) +const metadataCampaigns = new Set() +const metadataReaches = { + fixed: createOracleReach(`trailbase.lifecycle.metadata.fixed`, [ + `metadata-absent`, + `metadata-present`, + ]), + random: createOracleReach(`trailbase.lifecycle.metadata.random`, [ + `metadata-absent`, + `metadata-present`, + ]), +} +afterAll(() => { + for (const [campaign, reach] of Object.entries(metadataReaches)) { + if (!metadataCampaigns.has(campaign)) continue + reach.assertReached() + process.stdout.write( + `trailbase.lifecycle.metadata.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + } +}) const scenarioFixedSeed = oracleFixedSeed(714_203, { numRuns: oracleRuns(50) }) fcTest.prop([scenarioArb], scenarioFixedSeed)( `matches generated lifecycle histories with a fixed seed`, (scenario) => { oracleRoute(oracleRouteOf(scenarioFixedSeed)) - return checkLifecycle(scenario) + metadataCampaigns.add(`fixed`) + return checkLifecycle(scenario, undefined, metadataReaches.fixed) }, ) const scenarioCampaign = oraclePropertyOptions(50, `trailbase.lifecycle`) @@ -490,7 +542,8 @@ fcTest.prop([scenarioArb], scenarioCampaign)( `matches generated lifecycle histories with a random or replayed seed`, (scenario) => { oracleRoute(oracleRouteOf(scenarioCampaign)) - return checkLifecycle(scenario) + metadataCampaigns.add(`random`) + return checkLifecycle(scenario, undefined, metadataReaches.random) }, ) diff --git a/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index 32c94f809d..8d57c632c4 100644 --- a/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/trailbase-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -7,7 +7,7 @@ import { declareOracle } from '../../db/tests/oracle-declaration' import { persistedStartupHistories, persistedStartupHistoryArb, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { trailBaseStartupAdapter } from './persisted-startup-driver' diff --git a/packages/trailbase-db-collection/tests/persisted-startup-driver.ts b/packages/trailbase-db-collection/tests/persisted-startup-driver.ts index 5f140f0cca..a1db38e179 100644 --- a/packages/trailbase-db-collection/tests/persisted-startup-driver.ts +++ b/packages/trailbase-db-collection/tests/persisted-startup-driver.ts @@ -10,7 +10,7 @@ import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src import { sortedStartupRows, startupRows, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { startupInitialRender, trackStartupSync, @@ -21,7 +21,7 @@ import type { Event, ListResponse, RecordApi } from 'trailbase' import type { PersistedStartupHistory, StartupRow, -} from '../../db-adapter-conformance/src/persisted-startup-oracle' +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' export const trailBaseStartupAdapter: PersistedStartupAdapter = @@ -68,6 +68,8 @@ export const trailBaseStartupAdapter: PersistedStartupAdapter ({ rows: sortedStartupRows(collection.values()) }), deliver, responseAvailable: () => available, From 1368594358c7eaf9ecf5dc903219986669608503 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 10:19:58 -0700 Subject: [PATCH 14/24] Clarify per-read batch size versus snapshot buffering --- .../oracle-reviews/2026-11-10-pr-2394-external-review.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md index ed5860cec4..3e9996e4d8 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md @@ -112,7 +112,8 @@ one-full-wrapper-row-read guarantee remains checked. No throughput or elapsed startup regression magnitude has been measured. The glossary's snapshot-transaction contract explicitly permits replacement of -all synced rows. These adapters do not promise bounded startup source batches or +all synced rows. The `syncBatchSize` option still bounds each source read. It does not bound +total buffering across a complete snapshot. These adapters do not promise preservation of per-row metadata across that replacement. The previously reviewed startup budgets still pass: one full wrapper row read, skipped complete setup, and hydration fairness. The added clear and PowerSync batch retention are real From 5feb26616a8357853454c2f9ff2ce0b0adb9c908 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 11:42:57 -0700 Subject: [PATCH 15/24] Fix persisted startup provenance and review instrumentation Reconcile omitted ownerless saved rows and values first returned for peer-only rows. Validate persistence capabilities at real RxDB/PowerSync sync admission. Make startup calibration nonvacuous, measure all OPFS reach states, deduplicate identical histories, and remove receiving fixture artifacts. Law: Complete eager results determine scoped rows and returned values; persistence preserves durable acceptance. Advertised persistence capabilities must satisfy the versioned SyncMetadataApi protocol. Reach: Six adapters on controlled/Node hosts, 1,920 generated and 492 fixed startup histories; Chromium repeats 246 startup + 44 protocol histories twice. RED: Before source edits, 72 Query and 76 protocol checks fail on Node; OPFS reports 34 startup mismatches and 38 protocol failures. Peer-only extension also fails on original Query source and rejects a narrow ownerless-only patch. GREEN: Combined 640 checks, normal Query 781, ordinary PowerSync 128/RxDB 19, and 580 Chromium comparisons pass. Eight typechecks, builds, and lint pass. Mutant: First-result-discard and actual scoped persistence-loss controls fail at the shared reference/durable comparison. Original source decisions give RED. Limits: Finite eager histories and local SDK sources; no arbitrary concurrent writers, failed I/O, all intermediate publications, or elapsed-time guarantee. Gap CLI audit remains incomplete. Whole-PR readiness is not established. Evidence: docs/contributing/oracle-reviews/2026-11-10-pr-2394-rereview.md --- .changeset/fix-persisted-query-startup.md | 2 +- docs/contributing/oracle-coverage.md | 24 +- .../2026-11-10-pr-2394-external-review.md | 7 + .../2026-11-10-pr-2394-rereview.md | 255 ++++++++++++++++++ ...ed-startup-conformance-oracle.opfs.spec.ts | 20 +- .../e2e/persisted-startup-conformance.opfs.ts | 81 ++++-- .../e2e/persisted-startup-opfs-cleanup.ts | 30 +++ packages/db-adapter-conformance/README.md | 13 +- ...sted-capability-conformance-oracle.test.ts | 46 ++++ .../tests/persisted-capability-oracle.ts | 121 +++++++++ .../tests/persisted-startup-driver.ts | 62 +++-- .../tests/persisted-startup-oracle-suite.ts | 197 +++++++------- .../tests/persisted-startup-oracle.ts | 79 +++++- .../tests/persisted-startup-driver.ts | 7 +- .../powersync-db-collection/src/powersync.ts | 13 +- .../tests/persisted-startup-common.ts | 5 +- ...tartup-conformance-oracle.property.test.ts | 1 + .../tests/persisted-startup-driver.ts | 10 +- packages/query-db-collection/src/query.ts | 20 +- ...rsisted-direct-write-window.oracle.test.ts | 43 ++- ...rsisted-startup-conformance-oracle.test.ts | 17 +- .../tests/persisted-startup-driver.ts | 36 ++- packages/rxdb-db-collection/src/rxdb.ts | 11 +- ...tartup-conformance-oracle.property.test.ts | 1 + .../tests/persisted-startup-driver.ts | 5 +- 25 files changed, 921 insertions(+), 185 deletions(-) create mode 100644 docs/contributing/oracle-reviews/2026-11-10-pr-2394-rereview.md create mode 100644 packages/browser-db-sqlite-persistence/e2e/persisted-startup-opfs-cleanup.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-capability-conformance-oracle.test.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-capability-oracle.ts diff --git a/.changeset/fix-persisted-query-startup.md b/.changeset/fix-persisted-query-startup.md index dd4774f04b..c39175f6e1 100644 --- a/.changeset/fix-persisted-query-startup.md +++ b/.changeset/fix-persisted-query-startup.md @@ -5,4 +5,4 @@ '@tanstack/trailbase-db-collection': patch --- -Remove obsolete persisted rows when an eager source's first complete result overlaps SQLite restore. Query Collection also handles initial data and cached results while preserving rows owned by other Queries, including when a persisting optimistic transaction holds restored rows before publication. TrailBase also accepts a sync boundary without the optional metadata API. +Remove obsolete persisted rows when an eager source's first complete result overlaps SQLite restore. Query Collection also reconciles ownerless saved rows and returned values from initial data and cached results while preserving peer ownership, including when an optimistic transaction holds publication. RxDB and PowerSync reject incomplete persistence capabilities. TrailBase accepts a sync boundary without the optional metadata API. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 1c3f3be863..e5d21c5327 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -7772,8 +7772,28 @@ snapshots also check exact ownership at all four snapshot cuts. Startup checks the reload owner's one-full-row-read law before its own durable observation. Query's receiving driver preserves exact fetch-count and pending-fetch premises. The [Chromium OPFS receiver](../../packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts) -runs forty-six Query histories and thirty complete-result histories for each of the -other five adapters through the same composition and check on real worker/OPFS. +runs 116 Query histories and 26 complete-result histories for each of the other +five adapters through the same composition and check on real worker/OPFS. It +repeats in the same browser profile, with file cleanup and separately enforced +reach for network-first result/fallback, empty result/baseline, overlap, omission, +mutation, and Query peers/ownerless baselines. Identical normalized histories are +deduplicated; empty-baseline mutations still hold publication and remain distinct. +Query crosses source-owned and ownerless saved provenance. Peer-only overlaps +exercise every initial/cache origin. At fallback, ownerless rows retain their +original absent source ownership. Complete results remove omitted ownerless rows +and apply returned values even when a peer previously supplied the row. + +The independent [capability-admission owner](../../packages/db-adapter-conformance/tests/persisted-capability-oracle.ts) +checks the cross-package SyncMetadataApi protocol. It enumerates nine required +members with missing and false-valued corruptions, a missing capability field, +and three legal forms (complete, explicit null, absent optional API). Real RxDB +and PowerSync receive all 22 histories on controlled, Node SQLite and Chromium +OPFS hosts. It checks invocation errors before source delivery and completes +source startup for legal forms. It does not claim optional extension faults, +on-demand acquisition results, or arbitrary malformed values. The shared startup +calibration now fails on a bad clean candidate, counts injected persistence +loss, and wraps hydration-scoped writes. RxDB/PowerSync must reach that scoped +fault before restore release on real SQLite. See the [re-review evidence](oracle-reviews/2026-11-10-pr-2394-rereview.md). The original Query omission fails startup on both Node hosts and OPFS. Convex without its first-result truncate, a survivor in the bare eager-source grammar, diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md index 3e9996e4d8..591cdbc63d 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md @@ -360,3 +360,10 @@ three files. Its package typecheck also passes after the runner change. Final staged validation: `pnpm lint:changed`, ESLint on every changed TypeScript file, and `git diff --cached --check` pass. No run budget was lowered or removed. + +## Subsequent re-review + +The [re-review at head 1368594](2026-11-10-pr-2394-rereview.md) supersedes the +current coverage claims with ownerless and peer-only provenance, measured browser +reach, capability admission, nonvacuous scoped persistence-loss calibration, and +resource cleanup. This record retains its historical test counts and verdicts. diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-rereview.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-rereview.md new file mode 100644 index 0000000000..54c88d00c0 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-rereview.md @@ -0,0 +1,255 @@ +# PR 2394 re-review: ownerless persisted startup + +Reviewed head: `1368594358c7eaf9ecf5dc903219986669608503` (55 changed files). +The reviewer supplied ten findings, ran no tests, verified no findings, needed +no install, and added no dependency overrides. This record is self-review with +executable verification. The earlier review's silence on a finding supplies +no verification; the affected neighboring laws are rerun here. + +## Laws and experiments + +A complete eager result determines the rows and returned values in its scope. +A later complete, unbounded response can remove an ownerless direct-write row +that it omits. A peer keeps an omitted row alive, but not its old value when the +new result returns that row. Authority: Query's “Direct Writes and Query Sync” +and “Initial Data” contracts, READY-3/PUB-7, and durable acceptance. + +The shared startup owner now crosses source-owned and ownerless persisted +provenance, including peer-only rows. Its independent set judgment is unchanged. +Fallback preserves original saved ownership, including its absence. Histories +cross both restore orders, five Query origins, network-first result/fallback, +and a persisting disjoint optimistic insert. Fixed regressions replay the same +model, production driver and comparison as generated histories. The first public +and durable cuts precede refresh; refresh fulfillment and local-only reopen +remain checked. Node and Chromium use the same state names and separately +require every receiving state's reach. + +On unchanged production, the first extension fails 72 Query checks on controlled +and real Node SQLite; Chromium reports 34 mismatching startup histories. The +existing driver always gave saved rows source ownership, excluding this law's +ownerless state. The extension also finds a sibling: a peer-only restored row +returned by initialData or cached success gains source ownership but keeps its +old value. Both cases violate the existing complete-result judgment. + +An advertised persistence capability must be complete and version-compatible. +An absent optional metadata API and explicit null persistence remain legal. +Authority: `SyncMetadataApi.persistence` and the cross-package protocol contract +in `sync-persistence.ts`. A separate finite oracle owns this structural admission +law; replacement's row-set model cannot judge malformed protocols. It enumerates +nine required members, omission and false-valued corruption, a missing capability, +and three legal forms. Real RxDB and PowerSync invoke sync through the persisted +wrapper on both Node hosts and worker/OPFS. Invalid inputs must report +`InvalidSyncPersistenceCapabilityError` at invocation; legal inputs also complete +a real source result. Unchanged production fails 76 Node admission checks and +38 Chromium checks. Both adapters now validate at sync entry. + +The persistence-loss calibration no longer reports success when its clean +candidate fails. It wraps outer and hydration-scoped persistence adapters, counts +actual injected drops, and checks first-result durability before refresh. Separate +before-restore controls require scoped fault reach for RxDB and PowerSync on +SQLite. Query's baseline wait moves its commit outside that scope, so that +counter is not a Query premise. The existing after-deadline controls remain. + +## Direct writes and startup work + +RR1 describes a real timing change: a successful Query cache entry can precede +its source commit while persisted restore is held. Update/delete then reject +because the row has not loaded. The contract validates against preceding accepted +sync commits, not cache availability. The same-path differential matrix tests +both committed and pre-commit orders. It supplies the causal schedule to the +unwrapped reference, never production's rows or write outcome. Eight new mutants +discard the first result's row writes, commit its metadata transaction, and report +readiness. Each requires positive discarded-row and readiness counters, then +fails that same reference comparison (4 operations through fake storage and real +SQLite). Suppressing the transaction start would test a metadata protocol error +instead, so it is not the final instrument. All 53 direct-write checks pass. +This refutes RR2's specific never-first-commit assertion; it does not turn cache +success into an admission promise. + +RR4's added truncate and PowerSync batch retention are real costs. The prior +same-path deterministic SQLite work experiment remains applicable: with empty +or three-row saved baselines and three source rows, both old and current paths +write three source rows. Current snapshot selection adds a clear/metadata reset. +PowerSync retains all snapshot batches; RxDB already used one transaction across +its full initial read. `syncBatchSize` bounds each read, not total snapshot memory. +The snapshot contract permits replacement, and neither adapter consumes persisted +provider resume progress here. Existing one-full-wrapper-row-read laws remain +enforced. No new elapsed-time or bounded-total-memory promise follows. Progressive +publication is a separate product change and is not introduced by this repair. + +## Lossless finding ledger + +| ID | Original claim and evidence | Technical verdict / PR action | Durable value | +| --- | --- | --- | --- | +| RR1 | `query.ts:2905`: update/delete reject during hydrate wait despite cached row; main succeeded. Same-path matrix reaches zero preceding commits and that rejection. | Accepted design under commit-based admission; timing consequence is real. | Preserve pre-commit and committed controls; cache is not a commit witness. | +| RR2 | `direct-write-window.oracle.test.ts:400–406`: reference schedule uses `commitsBeforeWrite`; startup skips positive commit assertion; cited docs specify no startup order; alleged never-commit false green and absent maintainer exclusion. | Refuted: event order is a differential input; first-result-discard mutants fail value comparisons on both hosts. No comparison or budget is relaxed. | Added eight reusable-comparison calibrations; no invented timing law. | +| RR3 | `query.ts:1681`: restored direct insert with no owners survives omission; all seeds were source-owned. | Fixed now: ownerless provenance reaches the original defect, including delayed optimistic publication. | Shared primary grammar and receiving witnesses; peer-only overlap sibling recorded separately. | +| RR4 | `powersync.ts:631`, `rxdb.ts:144`: clear/rewrite each persisted startup; PowerSync buffers all rows. Suggested nonempty-only snapshots or hydrate-and-diff. | Accepted design with measured added clear and buffering cost under the existing snapshot law. N source writes predate this selection. | Preserve deterministic work measurements and unmeasured latency/memory limits. A nonempty-only check before restore can miss the saved baseline. | +| RR5 | OPFS receiver counts only four states, omitting network-first, empty answer, omission, overlap and peers despite broader claims. | Fixed now: shared state classification and enforced browser-local counters for every promised state. Fallback comparisons already executed; missing counters did not establish missing comparisons. | Same names and distinct per-adapter reach across all hosts, including ownerless provenance. | +| RR6 | Calibration returns green if clean trace fails; fault proxy omits hydration-scoped adapter. | Fixed now: clean failure fails honestly, drop counters are mandatory, scoped adapter receives the fault, before-restore controls supplement fallback. | Nonvacuous first-result durable sensitivity and explicit receiving premises. | +| RR7 | RxDB/PowerSync truth-test advertised persistence, silently accepting a dropped field; stale rows may survive. | Fixed now: protocol admission oracle is RED on both Node hosts and Chromium; adapters use the shared validator. | Missing/malformed required members, optional API absence and explicit null are distinct legal/fault forms. | +| RR8 | Alleged previous=0 makes mutation flag ineffective, duplicates inflate 196/392 counts. | Fixed now for actual duplicated normalized inputs. The mutation claim is refuted: even with no saved row, a persisting optimistic insert holds publication. | Deduplicate identical histories, preserve the mutation axis and generated budgets; report revised counts. | +| RR9 | PowerSync leaves Node WAL/SHM; browser retains ps-N databases, allowing reused-profile contamination. | Fixed now: confirmed Node sidecars and 774 OPFS artifacts. Fixture-owned Node directories and browser SQLite sidecars are removed after close, including failing histories. | Reused-origin browser replay checks isolation; cleanup keeps primary and secondary diagnostics. | +| RR10 | `useHydratedBaseline` duplicates `needsPersistedBaseline`, risking drift. | Refuted as duplicate predicates: the outer test decides whether reconciliation is needed; the inner selects eager hydration versus retained-query recovery. They have different domains and consequences. No behavior edit. | Keep that distinction visible; typechecks and receiving histories protect both branches. | + +Sibling S1: peer-only restored overlap from initial/cache origins retained its +old value. This is a missing provenance/overlap combination, not a law change. +The generated grammar found it; fixed receiving histories preserve it alongside +the broader campaign. Its first-result public and durable cuts precede refresh. + +## Verification and limits + +### RED and checker calibration + +Before production edits, the combined Node campaign reports **148 failed / 236 +passed**: 72 Query failures and 76 protocol failures. Chromium reports **34 +startup mismatches / 214 histories**, **38 admission failures / 44 histories**, +and 774 retained fixture artifacts. Every required receiving state is reached. +These are comparison/admission failures, not a missing-host or timeout result. + +The peer-only overlap extension was also run against Query's exact reviewed +source. It reports **104 failed / 138 passed** on Node and **50 startup +mismatches / 246 histories** on Chromium. The source was restored from the +reviewed commit for this experiment. A narrow ownerless-only repair then fails +two fresh generated initialData histories with peer-only overlap. The final +repair handles the wider provenance/value law, rather than only the reported +omitted-row trace. + +RR6 has a separate bad-clean-candidate experiment. Wrong saved ownership makes +the original suite's two selected calibration tests pass. The repaired suite's +two selected calibration tests fail at the shared fallback ownership comparison. +Both filtered runs also fail their unrelated afterAll reach checks because the +normal histories were not selected. The original *tests* falsely pass; the +filtered original run as a whole does not pass. + +The original-defect controls are the exact Query baseline/value omissions and +RxDB/PowerSync capability truth tests. Narrow wrong designs (global truncation, +missing first-result rows, lost scoped durable writes) have separate controls. +Before-restore and after-deadline persistence-loss controls require an actual +dropped write. The before-restore RxDB/PowerSync SQLite controls additionally +require that drop inside the hydration-scoped adapter. + +### Repaired GREEN + +The combined source-alias Vitest run passes **640 checks in seven files**. +It comprises 492 fixed startup histories, 24 generated campaigns of 80 histories +(1,920 generated histories), 36 startup calibrations, and 88 protocol histories. +Each adapter/host/campaign enforces its own reach. Query has 116 unique fixed +histories per host; each other adapter has 26. The protocol grammar supplies +22 histories per adapter/host. A final Query rerun after tightening the exact +ownerless reach predicate passes **242 checks**. A single peer-only row no +longer counts as an ownerless row. + +Chromium passes **246 startup + 44 protocol histories per run**, then repeats all +290 histories with the same profile and file names: **580 receiving comparisons**. +The Playwright test passes in 4.1 minutes. Every adapter has positive counters +for both restore orders, network-first result/fallback, empty result/baseline, +changed overlap, omission, and persisting mutation. Query also measures peer +ownership, ownerless saved rows, and all five response origins. Its fixed reach +per run includes 16 fallback histories, 60 peer histories, and 56 ownerless +histories. Each run ends with zero fixture artifacts. + +Repeating Electric exposed a provider-fixture error: its SDK remembers expired +shape handles in localStorage. Each new source now supplies a fresh handle, +as the SDK protocol requires. The test still reuses the origin, URL, SQLite file +names, and browser profile. Electric's Node receiver also passes **62 checks** +after that fixture correction. PowerSync Node fixtures now remove their owned +temporary directory after close, including WAL/SHM. Historical files from older +fixtures were observed but were not deleted by this repair. + +Surrounding Query ownership/direct-write and TrailBase lifecycle owners pass +**431 checks**. A normal Query package run passes **781 checks in five files**, +including the ordinary Query and direct-write suites. The corrected direct-write +calibration run passes **53 checks**. PowerSync's ordinary suites pass **128 +checks in four files**, and RxDB's ordinary suite passes **19**. The capability +owner also passes **88 checks** through its normal package command. + +All eight affected package typechecks pass. The conformance declaration/build +check passes, as do the four changed production package builds. Lint and +`git diff --check` pass. Compared with merge-base `ea3c920c1`, the four bundles +grow by **208 gzip / 198 brotli bytes** combined; the ordinary app scenario grows +by **56 gzip bytes**. Test modules remain outside the shipped source tree. + +The normal package commands include: + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/query-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false tests/ownership-lifecycle.oracle.test.ts tests/persisted-startup-conformance-oracle.test.ts tests/persisted-direct-write-window.oracle.test.ts tests/persisted-direct-write.test.ts tests/query.test.ts +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/db-adapter-conformance exec vitest run --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false tests/persisted-capability-conformance-oracle.test.ts +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/browser-db-sqlite-persistence exec playwright test --config playwright.opfs.config.ts persisted-startup-conformance-oracle.opfs.spec.ts +pnpm --config.verifyDepsBeforeRun=false lint:changed +``` + +Each other adapter registers the shared Node campaign in +`tests/persisted-startup-conformance-oracle.property.test.ts`. The combined +source-alias harness uses `.review-2367/shared-startup.config.ts` to run all six +receivers plus the capability owner together, before and after the source edits. +Local logs retain RED, sibling RED, narrow-repair rejection, checker calibration, +GREEN, and host reach separately. The local harness and logs are uncommitted. + +The independent set/value model and durable comparisons remain unchanged. +The repair extends provenance and receiving paths, rather than deriving new +expected answers from production. The finite admission model separately checks +structural protocol validity. Source ownership, same-run direct-write fetch +precedence, delayed optimistic publication, fallback ownership, one-full-row-read +work, and post-refresh/reopen durability remain enforced by their applicable +owners. No comparison exclusion or generated budget is lowered. + +### Applicable rubric audit + +This is a self-review of the changed startup grammar, drivers, comparisons, and +finite protocol owner. Existing owners retain their broader contracts. + +| Requirement | Evidence or applicability | +| --- | --- | +| ORC-001 | The opening laws cite complete-result, durable-acceptance, snapshot, and capability contracts. The limits below bound each receiving claim. | +| ORC-002 | Replacement uses independent set/value recomputation. Admission uses input kinds and expected error identity, without calling the validator. The differential reference receives event order, not production's answer. | +| ORC-003 | Shared owner, suite, drivers, and capability owner explain contract, grammar, production boundary, checkpoint, and judgment beside executable code. | +| ORC-004 | Ownerless provenance reconstructs RR3; peer-only overlap reconstructs S1. Removing provenance loses both; removing overlap/origin loses S1; removing mutation loses delayed publication; removing fallback loses the saved-row checkpoint. Three keys include empty, singleton, disjoint, and overlapping sets. Empty restore cannot request deadline fallback. The protocol grammar separates absent optional API from missing advertised members. | +| ORC-005 | Actual source sync invocation, commit submission, applied receipts, public rows, SQLite rows/metadata, refresh fulfillment, and local-only reopen supply their named cuts. The first-result comparison precedes refresh. No concrete missing-input hypothesis remains for these cuts: complete rows are delivered and restore is released before their comparison. | +| ORC-006 | Original source RED, narrow-repair rejection, first-result-discard mutants, and actual scoped durable-write drops reject wrong observations at the intended comparison. The original bad-clean-candidate calibration is explicitly distinguished from a whole-run GREEN. | +| ORC-007 | Startup retains identical fixed-seed and fresh campaigns, 80 histories each, with the existing direct seed/path replay route. Finite protocol enumeration has no random-campaign requirement. This repair does not change replay parsing. | +| ORC-008 | No stateful reference machine is added or merged. Provenance is an input distinction: fallback ownership differs, while the later complete-result row judgment stays the same. | +| ORC-009 | Saved ownership maps to row metadata; source result maps to actual adapter delivery/commit. Accepted and applied rows remain distinct across an optimistic publication hold. The small bitsets represent keyed row sets, not production queues. | +| ORC-010 | Existing cleanup aggregation preserves primary mismatch and secondary errors. Browser cleanup now covers the whole body; dedicated Node directories include sidecars. Successful repeated-profile runs finish without fixture artifacts. | +| ORC-011 | RR2 supplies a shared-assumption concern; the unwrapped same-path differential and calibrated discarded-row subject test it. No new shared semantic fault is identified for the independent complete-set or structural-admission judgments. | +| ORC-012 | This versioned record ties the ten findings, sibling, RED/GREEN, calibration, finite boundary, and applicable requirements to reviewed head `1368594`. It separates repair completion from whole-PR readiness. | +| ORC-013 | Ownerless omission versus peer retention, returned peer overlap versus omission, and complete/null/absent API versus missing/malformed advertised members distinguish the laws from global replacement and permissive admission. | +| ORC-014 | Controlled, real Node SQLite, and Chromium OPFS use the same judgments. Real local RxDB/PowerSync supply their SDK boundaries. Controlled remote protocols remain limited as stated below. | +| ORC-015 | Required states are shared, but counts and minimum-one enforcement are separate per adapter/host/campaign. Chromium now requires every claimed state. Exact ownerless reach excludes a singleton with only a peer owner. | +| ORC-016 | Fixed histories replay the common judgment. Calibration supplies hostile observations to that judgment. The new finite grammar has no per-example expected row answers or standalone type assertions. | + +HTTP 403 install recovery used local-only overrides: `@tanstack/query-core` +5.107.1 → 5.106.0, `fake-indexeddb` 6.2.8 → 6.2.7, and +`@tailwindcss/postcss` 4.3.5 → 4.3.4. Those overrides and lockfile changes are +uncommitted. The exact unrecovered dependency versions lack a local witness. + +Limits: three string keys and numeric values, one eager run, one disjoint +optimistic insert, optional Query peer, five response origins, local SDK sources, +and local-only reopen. No arbitrary concurrent writers, remote RxDB/PowerSync +replication, deployed TrailBase wire behavior, failed storage, all intermediate +publications, optional protocol extension faults, elapsed startup benchmark, or +unbounded performance claim. The existing primary owners retain those contracts. + +Ten raw findings are accounted for: six fixed now (RR3/5/6/7/8/9), two refuted +(RR2/10), two accepted designs (RR1/4). RR8 includes its refuted subclaim without +losing the actual duplicate-input repair. The comparison prose is retained: +earlier accepted/applied ownership, absent TrailBase metadata, response-delivery +premises, per-adapter design, and docs were not re-raised; that silence proves +nothing. The oracle's move from src to tests survives. Affected earlier laws are +included in surrounding verification. + +Reviewer assessment: **hire with reservations**. Ownerless provenance, weak +calibration, missing protocol validation and resource leakage are valuable +findings. Prioritizing the unverified admission assertion above these defects +and treating mutation scheduling as duplicate input reduced precision. Several +claims would have been sharpened by executing the supplied tests. The review +found a useful bug class, but did not test nearby peer-only overlaps. + +Finding accounting and these bounded repairs are complete. Whole-PR readiness +remains unproven: the gap CLI audit is incomplete, and green CI for reviewed head +`1368594` does not validate the forthcoming repair commit. Check the pushed +repair head separately. + +The gap CLI's earlier internal accounting error remains separately recorded in +`2026-11-10-startup-gap-cli.md`; this evaluation does not claim a completed gap +audit or whole-PR readiness from green bounded campaigns. diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts index dcb76c4703..bd77f229cb 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts @@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test' import { declareOracle } from '../../db/tests/oracle-declaration' import { queryStartupHistories } from '../../query-db-collection/tests/persisted-startup-driver' import { persistedStartupHistories } from '../../db-adapter-conformance/tests/persisted-startup-oracle' +import { capabilityHistories } from '../../db-adapter-conformance/tests/persisted-capability-oracle' import type { StartupConformanceResult } from './persisted-startup-conformance.opfs' // The page compares each cut with the shared model. Count each adapter's @@ -21,13 +22,21 @@ function checkStartupConformance(result: StartupConformanceResult) { expect( result.results.filter((item) => item.adapter === adapter), ).toHaveLength(persistedStartupHistories.length) + for (const name of [`rxdb`, `powersync`]) + expect( + result.capabilities.filter((item) => item.adapter === name), + ).toHaveLength(capabilityHistories.length) + expect(result.capabilities.flatMap(({ violations }) => violations)).toEqual( + [], + ) + expect(result.artifacts).toEqual([]) expect(result.results.flatMap(({ violations }) => violations)).toEqual([]) } declareOracle({ check: [checkStartupConformance] }) test(`complete source results replace persisted rows across OPFS startup`, async ({ page, }) => { - test.setTimeout(180_000) + test.setTimeout(600_000) page.on(`console`, (message) => console.info(`browser ${message.type()}: ${message.text()}`), ) @@ -42,4 +51,13 @@ test(`complete source results replace persisted rows across OPFS startup`, async ) expect(errors).toEqual([]) checkStartupConformance(result) + // Reuse the same origin and file names. Successful cleanup must keep the + // next run independent, rather than relying on a fresh browser profile. + await page.reload() + await page.waitForFunction( + () => window.__persistedStartupConformance !== undefined, + ) + checkStartupConformance( + await page.evaluate(() => window.__persistedStartupConformance!), + ) }) diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts index a5dfc3359f..45c7182bab 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts @@ -5,6 +5,8 @@ import { PersistedStartupViolation, persistedStartupHistories, persistedStartupViolations, + startupReachedStates, + startupRequiredStates, } from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { isStartupRowRead, @@ -24,6 +26,16 @@ import { createBrowserWASQLitePersistence, openBrowserWASQLiteOPFSDatabase, } from '../src' +import { + capabilityHistories, + capabilityViolations, + observeCapability, +} from '../../db-adapter-conformance/tests/persisted-capability-oracle' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { + removeStartupOPFSArtifacts, + startupOPFSArtifacts, +} from './persisted-startup-opfs-cleanup' import type { PersistedStartupHistory } from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' @@ -33,6 +45,12 @@ export type StartupConformanceResult = { history: unknown violations: Array }> + capabilities: Array<{ + adapter: string + history: unknown + violations: Array + }> + artifacts: Array error?: string } declare global { @@ -42,6 +60,8 @@ declare global { } async function run() { const results: StartupConformanceResult[`results`] = [] + const capabilities: StartupConformanceResult[`capabilities`] = [] + let artifacts: Array = [] let sequence = 0 async function receive( name: string, @@ -51,19 +71,14 @@ async function run() { // This page has no Node oracle runner. Each completed history calls its // startup observer once; fail locally if any required receiving state is absent. const reach: Record = Object.fromEntries( - [ - `response-before-restore`, - `restore-before-response`, - `persisting-mutation`, - `empty-baseline`, - ].map((state) => [state, 0]), + startupRequiredStates(adapter.scope).map((state) => [state, 0]), ) for (const history of histories) { const index = sequence++ - const database = await openBrowserWASQLiteOPFSDatabase({ - databaseName: `ps-${index}.sqlite`, - }) - try { + const databaseName = `ps-${index}.sqlite` + await removeStartupOPFSArtifacts(databaseName) + const database = await openBrowserWASQLiteOPFSDatabase({ databaseName }) + await withOracleCleanup(async () => { let rowReads = 0 const execute = database.execute.bind(database) database.execute = (sql, params) => { @@ -79,13 +94,7 @@ async function run() { `startup-${index}`, () => rowReads, () => { - reach[ - history.response === `before-restore` - ? `response-before-restore` - : `restore-before-response` - ]!++ - if (history.mutation) reach[`persisting-mutation`]!++ - if (history.previous === 0) reach[`empty-baseline`]!++ + for (const state of startupReachedStates(history)) reach[state]!++ }, ) results.push({ @@ -101,9 +110,10 @@ async function run() { if (!(error instanceof PersistedStartupViolation)) throw error results.push({ adapter: name, history, violations: error.violations }) } - } finally { - await database.close?.() - } + }, [ + () => database.close?.(), + () => removeStartupOPFSArtifacts(databaseName), + ]) } for (const [state, count] of Object.entries(reach)) if (!count) throw new Error(`${name}.opfs did not reach ${state}`) @@ -124,10 +134,39 @@ async function run() { powerSyncBrowserStartupAdapter, persistedStartupHistories, ) - window.__persistedStartupConformance = { results } + for (const [name, adapter] of [ + [`rxdb`, rxdbStartupAdapter], + [`powersync`, powerSyncBrowserStartupAdapter], + ] as const) { + for (const history of capabilityHistories) { + const databaseName = `ps-${sequence++}.sqlite` + await removeStartupOPFSArtifacts(databaseName) + const database = await openBrowserWASQLiteOPFSDatabase({ databaseName }) + await withOracleCleanup(async () => { + const error = await observeCapability( + adapter, + createBrowserWASQLitePersistence({ database }), + history, + `capability-${sequence}`, + ) + capabilities.push({ + adapter: name, + history, + violations: capabilityViolations(history, error), + }) + }, [ + () => database.close?.(), + () => removeStartupOPFSArtifacts(databaseName), + ]) + } + } + artifacts = await startupOPFSArtifacts() + window.__persistedStartupConformance = { results, capabilities, artifacts } } catch (error) { window.__persistedStartupConformance = { results, + capabilities, + artifacts, error: error instanceof Error ? `${error.stack} ${JSON.stringify(error, Object.getOwnPropertyNames(error))}` diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-opfs-cleanup.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-opfs-cleanup.ts new file mode 100644 index 0000000000..c5d0acb872 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-opfs-cleanup.ts @@ -0,0 +1,30 @@ +// Fixture-owned files only. Close their worker before deleting SQLite sidecars. +// The Playwright context gives this page an isolated origin. +export async function removeStartupOPFSArtifacts(databaseName: string) { + const root = await navigator.storage.getDirectory() + for (const suffix of [``, `-journal`, `-wal`, `-shm`]) { + try { + await root.removeEntry(`${databaseName}${suffix}`) + } catch (error) { + if (!(error instanceof DOMException && error.name === `NotFoundError`)) + throw error + } + } + const iterable = root as FileSystemDirectoryHandle & { + entries: () => AsyncIterableIterator<[string, FileSystemHandle]> + } + for await (const [name, handle] of iterable.entries()) { + if (handle.kind === `directory` && name.startsWith(`.ahp-`)) + await root.removeEntry(name, { recursive: true }) + } +} +export async function startupOPFSArtifacts(): Promise> { + const root = + (await navigator.storage.getDirectory()) as FileSystemDirectoryHandle & { + keys: () => AsyncIterableIterator + } + const artifacts: Array = [] + for await (const name of root.keys()) + if (name.startsWith(`ps-`) || name.startsWith(`.ahp-`)) artifacts.push(name) + return artifacts.sort() +} diff --git a/packages/db-adapter-conformance/README.md b/packages/db-adapter-conformance/README.md index cb8473e9b4..bc8bbfdd2c 100644 --- a/packages/db-adapter-conformance/README.md +++ b/packages/db-adapter-conformance/README.md @@ -83,14 +83,19 @@ public/durable rows before refresh, public rows inside refresh fulfillment, durable rows after refresh, and a local-only reopen. It also shares the reload owner's one-row-read law. A scoped adapter checks exact source/peer ownership. -Query receives the shared property with five cache/fetch origins and peer -ownership. Convex, Electric, TrailBase, RxDB and PowerSync also receive it through +Query receives the shared property with five cache/fetch origins, ownerless +saved rows, peer ownership, and peer-only overlaps. Convex, Electric, TrailBase, RxDB and PowerSync also receive it through their installed SDK or controlled provider boundary. Chromium worker/OPFS runs -46 Query histories and 30 histories for each other adapter. Every receiver +116 Query histories and 26 histories for each other adapter. The browser repeats +these histories in one profile and removes fixture-owned OPFS files. It also +runs 22 protocol-admission histories for each of RxDB and PowerSync. Every receiver crosses ordinary and network-first rendering, empty and nonempty baselines, and a persisting optimistic transaction. RxDB and PowerSync prove source commit invocation before releasing a held restore; gate release alone does -not count as response delivery. +not count as response delivery. Both Node hosts also judge the independent +persistence capability protocol through the real RxDB/PowerSync sync boundary. +The persistence-loss calibration wraps hydration-scoped adapters, requires an +actual dropped write, and fails if its clean candidate fails. ```sh pnpm --filter @tanstack/query-db-collection exec vitest run --coverage.enabled=false --typecheck.enabled=false tests/persisted-startup-conformance-oracle.test.ts diff --git a/packages/db-adapter-conformance/tests/persisted-capability-conformance-oracle.test.ts b/packages/db-adapter-conformance/tests/persisted-capability-conformance-oracle.test.ts new file mode 100644 index 0000000000..87002e8677 --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-capability-conformance-oracle.test.ts @@ -0,0 +1,46 @@ +/** Real RxDB/PowerSync receiving paths for the finite capability grammar. */ +import { afterAll, describe, expect, it } from 'vitest' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { oracleRoute } from '../../db/tests/oracle-config' +import { rxdbStartupAdapter } from '../../rxdb-db-collection/tests/persisted-startup-driver' +import { powerSyncStartupAdapter } from '../../powersync-db-collection/tests/persisted-startup-driver' +import { createStartupHost } from './persisted-startup-host' +import { + capabilityHistories, + capabilityViolations, + observeCapability, +} from './persisted-capability-oracle' + +declareOracle({ check: [capabilityViolations], grammar: [capabilityHistories] }) +for (const [name, adapter] of [ + [`rxdb`, rxdbStartupAdapter], + [`powersync`, powerSyncStartupAdapter], +] as const) + for (const host of [`controlled`, `node-sqlite`] as const) + describe(`${name} capability through ${host}`, () => { + const reach = createOracleReach(`${name}.capability.${host}`, [ + ...new Set(capabilityHistories.map((h) => h.kind)), + ]) + afterAll(() => reach.assertReached()) + it.each(capabilityHistories)( + `validates persistence protocol: %j`, + async (h) => { + oracleRoute(`fixed history`) + reach.beginHistory() + const storage = createStartupHost(host) + try { + const error = await observeCapability( + adapter, + storage.persistence, + h, + `${name}-${host}-${JSON.stringify(h)}`, + ) + reach.reach(h.kind) + expect(capabilityViolations(h, error)).toEqual([]) + } finally { + storage.close() + } + }, + ) + }) diff --git a/packages/db-adapter-conformance/tests/persisted-capability-oracle.ts b/packages/db-adapter-conformance/tests/persisted-capability-oracle.ts new file mode 100644 index 0000000000..fbec6571b2 --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-capability-oracle.ts @@ -0,0 +1,121 @@ +/** + * An advertised persistence capability is a versioned structural protocol. + * Missing or malformed required members must fail at sync admission, before a + * source result can claim successful startup. An absent metadata API and an + * explicit null capability are legal. Authority: SyncMetadataApi.persistence + * and sync-persistence.ts's cross-package protocol contract. + * + * This independent judgment uses a finite field grammar, not the validator. + * Each receiver supplies a real SDK and one of the startup storage hosts. + * The observation is the error returned by actual sync invocation, before + * source delivery; accepted inputs additionally complete a real source result. + * This owner checks protocol admission, not row replacement or I/O failure. + */ +import { declareOracle } from '../../db/tests/oracle-declaration' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' +import type { PersistedStartupAdapter } from './persisted-startup-driver' +import type { PersistedStartupHistory } from './persisted-startup-oracle' + +const requiredMembers = [ + `protocol`, + `version`, + `hydrateBaseline`, + `reserveCommitTurn`, + `scanPersistedRows`, + `resumeSnapshot`, + `resumeSnapshot.certify`, + `resumeSnapshot.getKeySetEvidence`, + `resumeSnapshot.expectCurrentCommit`, +] as const +export type CapabilityHistory = + | { kind: `complete` | `null` | `absent-metadata` | `missing-capability` } + | { + kind: `missing-member` | `malformed-member` + member: (typeof requiredMembers)[number] + } +export const capabilityHistories: Array = [ + { kind: `complete` }, + { kind: `null` }, + { kind: `absent-metadata` }, + { kind: `missing-capability` }, + ...requiredMembers.flatMap((member): Array => [ + { kind: `missing-member`, member }, + { kind: `malformed-member`, member }, + ]), +] +export const capabilityMustAccept = (h: CapabilityHistory): boolean => + h.kind === `complete` || h.kind === `null` || h.kind === `absent-metadata` + +// Supply protocol faults at the wrapper-to-source boundary. Never call the +// production validator to decide the expected verdict. +function transformCapability(metadata: unknown, h: CapabilityHistory): unknown { + if (h.kind === `complete`) return metadata + if (h.kind === `absent-metadata`) return undefined + const api = metadata as Record + if (h.kind === `null`) return { ...api, persistence: null } + if (h.kind === `missing-capability`) { + const { persistence: _removed, ...rest } = api + return rest + } + if (!(`member` in h)) throw new Error(`Missing capability fault member`) + const capability = { ...(api.persistence as Record) } + const [parent, child] = h.member.split(`.`) + const object = child + ? { ...(capability[parent!] as Record) } + : capability + const key = child ?? parent! + if (h.kind === `missing-member`) delete object[key] + else object[key] = false + if (child) capability[parent!] = object + return { ...api, persistence: capability } +} +export function capabilityViolations( + h: CapabilityHistory, + error: unknown, +): Array { + const name = error instanceof Error ? error.name : undefined + if (capabilityMustAccept(h)) + return error === undefined + ? [] + : [`legal capability rejected: ${String(error)}`] + return name === `InvalidSyncPersistenceCapabilityError` + ? [] + : [`invalid capability accepted or wrong error: ${String(error)}`] +} +export async function observeCapability( + adapter: PersistedStartupAdapter, + persistence: PersistedCollectionPersistence, + h: CapabilityHistory, + id: string, +): Promise { + const source = await adapter.create( + persistence, + { + previous: 0, + next: 1, + peer: false, + response: `after-restore`, + }, + id, + (metadata) => transformCapability(metadata, h), + ) + return withOracleCleanup(async () => { + const loading = source.collection.preload() + void loading.catch(() => {}) + if (!source.syncOutcome) + throw new Error(`Receiver does not record sync admission`) + const { error } = await source.syncOutcome + if (error === undefined && capabilityMustAccept(h)) { + await source.deliver() + await source.initialResult + await loading + } + return error + }, [() => source.dispose()]) +} +declareOracle({ + model: [capabilityMustAccept], + grammar: [capabilityHistories], + check: [capabilityViolations], +}) diff --git a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts index 3949c6c8d3..e1f03ee651 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts @@ -54,6 +54,7 @@ export interface PersistedStartupSource { refresh: () => Promise dispose: () => unknown checkStartup?: () => Array + syncOutcome?: Promise<{ error?: unknown }> } export interface PersistedStartupAdapter { scope: StartupScope @@ -63,6 +64,7 @@ export interface PersistedStartupAdapter { persistence: PersistedCollectionPersistence, h: H, id: string, + metadataTransform?: (metadata: unknown) => unknown, ) => PersistedStartupSource | Promise } // Observe the adapter's public sync boundary before persistence wraps it. @@ -71,7 +73,12 @@ export interface PersistedStartupAdapter { // cannot supply this checkpoint. This recorder supplies no writes or readiness. export function trackStartupSync( sync: SyncConfig, + metadataTransform: (metadata: unknown) => unknown = (metadata) => metadata, ) { + let recordSync!: (outcome: { error?: unknown }) => void + const syncOutcome = new Promise<{ error?: unknown }>((resolve) => { + recordSync = resolve + }) let submit!: () => void const firstSubmission = new Promise((resolve) => { submit = resolve @@ -96,31 +103,42 @@ export function trackStartupSync( hydrateBaseline = params.metadata?.persistence?.hydrateBaseline const receipts: Array> = [] let writes = 0 - return sync.sync({ - ...params, - write: (message) => { - params.write(message) - writes++ - }, - commit: (signal) => { - const receipt = params.commit(signal) - receipts.push(receipt) - committedRows += writes - writes = 0 - committed = true - resolveCommit() - submit() - return receipt - }, - markReady: () => { - params.markReady() - submit() - void Promise.all(receipts).then(() => resolve(), reject) - }, - }) + try { + const result = sync.sync({ + ...params, + metadata: metadataTransform( + params.metadata, + ) as typeof params.metadata, + write: (message) => { + params.write(message) + writes++ + }, + commit: (signal) => { + const receipt = params.commit(signal) + receipts.push(receipt) + committedRows += writes + writes = 0 + committed = true + resolveCommit() + submit() + return receipt + }, + markReady: () => { + params.markReady() + submit() + void Promise.all(receipts).then(() => resolve(), reject) + }, + }) + recordSync({}) + return result + } catch (error) { + recordSync({ error }) + throw error + } }, } return { + syncOutcome, sync: tracked, initialResult, firstCommit, diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts index 83fccd0521..6cfe458c8b 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts @@ -18,16 +18,18 @@ import { oracleRuns, } from '../../db/tests/oracle-config' import { - PersistedStartupViolation, expectedPersistedStartup, expectedStartupFallback, persistedStartupCutViolations, persistedStartupFallbackViolations, persistedStartupViolations, + startupReachedStates, + startupRequiredStates, } from './persisted-startup-oracle' import { observePersistedStartup } from './persisted-startup-driver' import { createStartupHost } from './persisted-startup-host' import type { Arbitrary } from 'fast-check' +import type { PersistenceAdapter } from '../../db-sqlite-persistence-core/src' import type { PersistedStartupHistory } from './persisted-startup-oracle' import type { PersistedStartupAdapter } from './persisted-startup-driver' @@ -48,11 +50,13 @@ export function registerPersistedStartupConformance< arbitrary, origins = [], origin = () => undefined, + scopedStartupCommit = false, }: { property: string adapter: PersistedStartupAdapter histories: Array arbitrary: Arbitrary + scopedStartupCommit?: boolean origins?: Array origin?: (history: H) => string | undefined }) { @@ -94,19 +98,7 @@ export function registerPersistedStartupConformance< persistedStartupCutViolations(h, adapter.scope, `startupDurable`, saved), ).not.toEqual([]) }) - const states = [ - `response-before-restore`, - `restore-before-response`, - `network-first-source-result`, - `network-first-fallback`, - `empty-result`, - `empty-baseline`, - `overlapping-update`, - `omitted-row`, - `persisting-mutation`, - ...(adapter.scope === `scoped` ? [`peer-owner`] : []), - ...origins, - ] + const states = [...startupRequiredStates(adapter.scope), ...origins] for (const host of [`controlled`, `node-sqlite`] as const) { describe(`${property} through ${host}`, () => { const reach = createOracleReach(`${property}.${host}`, states) @@ -129,25 +121,8 @@ export function registerPersistedStartupConformance< `startup-${host}-${sequence++}`, storage.readRowCount, () => { - historyReach.reach( - history.response === `before-restore` - ? `response-before-restore` - : `restore-before-response`, - ) - if (history.initialRender) - historyReach.reach( - history.response === `after-deadline` - ? `network-first-fallback` - : `network-first-source-result`, - ) - if (history.mutation) historyReach.reach(`persisting-mutation`) - if (history.previous === 0) historyReach.reach(`empty-baseline`) - if (history.next === 0) historyReach.reach(`empty-result`) - if (history.next & history.previous) - historyReach.reach(`overlapping-update`) - if (history.previous & ~history.next) - historyReach.reach(`omitted-row`) - if (history.peer) historyReach.reach(`peer-owner`) + for (const state of startupReachedStates(history)) + historyReach.reach(state) const responseOrigin = origin(history) if (responseOrigin) historyReach.reach(responseOrigin) }, @@ -163,70 +138,100 @@ export function registerPersistedStartupConformance< // A source that publishes after fallback but silently loses its durable // writes must fail before refresh can repair it. Seed and restore use the // real host; only the receiving source's persistence commits are dropped. - oracleReplayTest(it)( - `rejects lost source persistence after fallback`, - async () => { - oracleRoute(`calibration`) - const history = histories.find( - (h) => h.response === `after-deadline`, - )! - // Controlled storage models one Collection; use a fresh host for - // each candidate so the clean run cannot seed the corrupted run. - const clean = createStartupHost(host) - try { - await observePersistedStartup( - adapter, - clean.persistence, - clean.holdBaseline, - history, - `clean-persistence-${host}`, - clean.readRowCount, - ) - } catch (error) { - // A broken clean trace is not a calibration candidate. The normal - // histories judge it; a second fault cannot calibrate that trace. - if (error instanceof PersistedStartupViolation) return - throw error - } finally { - clean.close() - } - const storage = createStartupHost(host) - try { - await expect( - observePersistedStartup( - { - ...adapter, - create: (persistence, h, id) => - adapter.create( - { - ...persistence, - adapter: new Proxy(persistence.adapter, { - get(target, key) { - if (key === `applyCommittedTx`) - return async () => {} - const value: unknown = Reflect.get(target, key) - return typeof value === `function` - ? value.bind(target) - : value - }, - }), - }, - h, - id, - ), - }, - storage.persistence, - storage.holdBaseline, + for (const response of [`before-restore`, `after-deadline`] as const) + oracleReplayTest(it)( + `rejects lost source persistence with ${response}`, + async () => { + oracleRoute(`calibration`) + const history = histories.find((h) => h.response === response)! + // Controlled storage models one Collection; use a fresh host for + // each candidate so the clean run cannot seed the corrupted run. + const clean = createStartupHost(host) + try { + await observePersistedStartup( + adapter, + clean.persistence, + clean.holdBaseline, history, - `lost-persistence-${host}`, - storage.readRowCount, - ), - ).rejects.toThrow(/startupDurable:/) - } finally { - storage.close() - } - }, - ) + `clean-persistence-${host}-${response}`, + clean.readRowCount, + ) + } finally { + clean.close() + } + const storage = createStartupHost(host) + let dropped = 0 + let scopedDrops = 0 + const corrupt = ( + subject: PersistenceAdapter, + inScope = false, + ): PersistenceAdapter => + new Proxy(subject, { + get(target, key) { + if (key === `applyCommittedTx`) + return () => { + dropped++ + if (inScope) scopedDrops++ + return Promise.resolve() + } + if ( + key === `runInHydrationScope` && + target.runInHydrationScope + ) + return ( + task: Parameters< + NonNullable + >[0], + ) => + target.runInHydrationScope!((scoped) => + task(corrupt(scoped, true)), + ) + const value: unknown = Reflect.get(target, key) + return typeof value === `function` + ? value.bind(target) + : value + }, + }) + try { + await expect( + observePersistedStartup( + { + ...adapter, + create: (persistence, h, id) => + adapter.create( + { + ...persistence, + adapter: corrupt(persistence.adapter), + }, + h, + id, + ), + }, + storage.persistence, + storage.holdBaseline, + history, + `lost-persistence-${host}-${response}`, + storage.readRowCount, + ), + ).rejects.toThrow(/startupDurable:/) + expect( + dropped, + `fault must reach a source persistence commit`, + ).toBeGreaterThan(0) + if ( + host === `node-sqlite` && + response === `before-restore` && + scopedStartupCommit + ) + expect( + scopedDrops, + `fault must reach the hydration-scoped adapter`, + ).toBeGreaterThan(0) + } finally { + storage.close() + } + }, + ) oracleReplayTest(it).each(histories)( `reconciles persisted startup: %j`, async (history) => { diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts index 85364d85b8..75d892dfa2 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts @@ -4,7 +4,9 @@ * replacement, even * when the result arrived before restore. A scoped source removes only its own * ownership; a peer's omitted row survives. A collection-wide source replaces - * every row. Authority: the shared eager-source READY-3/PUB-7 replacement law, + * every row. An ownerless direct-write row omitted by a later complete, + * unbounded response is removed too (Query "Direct Writes and Query Sync"). + * Authority: the shared eager-source READY-3/PUB-7 replacement law, * docs/collections/query-collection.md ("Direct Writes and Query Sync", "Initial * Data"), and persistence's durable acceptance contract. The maintainer chose * shared persistence conformance as this crossing's owner on 2026-11-09. @@ -36,6 +38,8 @@ export type PersistedStartupHistory = { previous: number next: number peer: boolean + // Omit the source ownership edge; the independent peer edge may remain. + previousOwner?: `none` response: `before-restore` | `after-restore` | `after-deadline` mutation?: `persisting` initialRender?: `network-first` @@ -178,12 +182,59 @@ export const withStartupMutations = ( ): Array => histories.flatMap((h) => [h, { ...h, mutation: `persisting` as const }]) -export const persistedStartupHistories = withStartupMutations( - baseStartupHistories.flatMap((h) => [ - h, - withStartupRender(h, true, false), - withStartupRender(h, true, true), - ]), +// Normalize only identical inputs. A persisting mutation remains a distinct +// action even with no saved rows: it holds source publication until settlement. +export const uniqueStartupHistories = ( + histories: Array, +): Array => [ + ...new Map(histories.map((h) => [JSON.stringify(h), h])).values(), +] + +export const startupReachedStates = ( + h: PersistedStartupHistory, +): Array => [ + h.response === `before-restore` + ? `response-before-restore` + : `restore-before-response`, + ...(h.initialRender + ? [ + h.response === `after-deadline` + ? `network-first-fallback` + : `network-first-source-result`, + ] + : []), + ...(h.mutation ? [`persisting-mutation`] : []), + ...(h.previous === 0 ? [`empty-baseline`] : []), + ...(h.next === 0 ? [`empty-result`] : []), + ...(h.next & h.previous ? [`overlapping-update`] : []), + ...(h.previous & ~h.next ? [`omitted-row`] : []), + ...(h.peer ? [`peer-owner`] : []), + ...(h.previousOwner === `none` && + (h.peer ? (h.previous & (h.previous - 1)) !== 0 : h.previous !== 0) + ? [`ownerless-baseline`] + : []), +] +export const startupRequiredStates = (scope: StartupScope): Array => [ + `response-before-restore`, + `restore-before-response`, + `network-first-source-result`, + `network-first-fallback`, + `empty-result`, + `empty-baseline`, + `overlapping-update`, + `omitted-row`, + `persisting-mutation`, + ...(scope === `scoped` ? [`peer-owner`, `ownerless-baseline`] : []), +] + +export const persistedStartupHistories = uniqueStartupHistories( + withStartupMutations( + baseStartupHistories.flatMap((h) => [ + h, + withStartupRender(h, true, false), + withStartupRender(h, true, true), + ]), + ), ) // Before any source result, timeout changes readiness only. Saved rows and @@ -194,7 +245,18 @@ export function expectedStartupFallback( scope: StartupScope, ): StartupSnapshot { const saved = expectedPersistedStartup({ ...h, next: h.previous }, scope) - return { ...saved, rows: startupRows(h.previous, 1) } + return { + ...saved, + rows: startupRows(h.previous, 1), + ...(saved.owners && h.previousOwner === `none` + ? { + owners: saved.owners.map(([key, owners]): [string, Array] => [ + key, + owners.filter((owner) => owner !== `source`), + ]), + } + : {}), + } } export function persistedStartupFallbackViolations( h: PersistedStartupHistory, @@ -287,6 +349,7 @@ declareOracle({ persistedStartupRenderArb, withStartupRender, withStartupMutations, + uniqueStartupHistories, persistedStartupHistoryArb, ], check: [ diff --git a/packages/electric-db-collection/tests/persisted-startup-driver.ts b/packages/electric-db-collection/tests/persisted-startup-driver.ts index 3a8798530c..184747548e 100644 --- a/packages/electric-db-collection/tests/persisted-startup-driver.ts +++ b/packages/electric-db-collection/tests/persisted-startup-driver.ts @@ -32,6 +32,9 @@ export const electricStartupAdapter: PersistedStartupAdapter = [] let available = false let offset = 0 + // Shape handles must never be reused. The SDK keeps expired handles in + // localStorage, including across a browser reload with the same URL. + const shape = `startup-${crypto.randomUUID()}` const fetchClient: typeof fetch = (input, init) => new Promise((resolve, reject) => { const signal = init?.signal @@ -84,7 +87,7 @@ export const electricStartupAdapter: PersistedStartupAdapter = { sync: (params) => { + const persistence = + params.metadata === undefined + ? null + : validateSyncPersistenceCapability(params.metadata.persistence) const { begin, write, @@ -628,7 +637,7 @@ function createPowerSyncCollectionConfig< // Replace retained rows, including persisted rows whose restore // has not completed when this load starts. snapshot: - !!params.metadata?.persistence || + persistence !== null || !collection._state.acceptedSyncedEntries().next().done, when: { [DiffTriggerOperation.INSERT]: `TRUE`, diff --git a/packages/powersync-db-collection/tests/persisted-startup-common.ts b/packages/powersync-db-collection/tests/persisted-startup-common.ts index 9b99499902..7a4571591c 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-common.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-common.ts @@ -35,7 +35,7 @@ export function powerSyncStartupAdapterFor( ): PersistedStartupAdapter { return { scope: `collection`, - async create(persistence, history, id) { + async create(persistence, history, id, metadataTransform) { const { database, dispose } = await open(schema) await database.writeTransaction(async (tx) => { for (const row of startupRows(history.next, 2)) @@ -57,7 +57,7 @@ export function powerSyncStartupAdapterFor( pending = resolve }), }) - const startup = trackStartupSync(options.sync) + const startup = trackStartupSync(options.sync, metadataTransform) const collection = createCollection( persistedCollectionOptions({ ...options, @@ -78,6 +78,7 @@ export function powerSyncStartupAdapterFor( } return { collection, + syncOutcome: startup.syncOutcome, initialResult: startup.initialResult, firstSubmission: startup.firstSubmission, waitForRestore: startup.waitForRestore, diff --git a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index c377adedd6..7d5b31af3f 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -24,5 +24,6 @@ describe(`persisted startup conformance: PowerSync`, () => { adapter: powerSyncStartupAdapter, histories: persistedStartupHistories, arbitrary: historyArb, + scopedStartupCommit: true, }) }) diff --git a/packages/powersync-db-collection/tests/persisted-startup-driver.ts b/packages/powersync-db-collection/tests/persisted-startup-driver.ts index ea749f439d..c53cba72fa 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-driver.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-driver.ts @@ -1,6 +1,5 @@ // Node PowerSync SDK receiving path for the shared startup composition. -import { randomUUID } from 'node:crypto' -import { rm } from 'node:fs/promises' +import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { PowerSyncDatabase } from '@powersync/node' @@ -12,11 +11,12 @@ export const powerSyncStartupAdapter = powerSyncStartupAdapterFor( async (schema) => { if (!TEST_DATABASE_IMPLEMENTATION) throw new Error(`No PowerSync SQLite implementation available`) - const file = `startup-${randomUUID()}.sqlite` + const directory = await mkdtemp(join(tmpdir(), `powersync-startup-`)) + const file = `source.sqlite` const database = new PowerSyncDatabase({ database: { dbFilename: file, - dbLocation: tmpdir(), + dbLocation: directory, implementation: TEST_DATABASE_IMPLEMENTATION, }, schema, @@ -28,7 +28,7 @@ export const powerSyncStartupAdapter = powerSyncStartupAdapterFor( () => database.disconnectAndClear(), [ () => database.close(), - () => rm(join(tmpdir(), file), { force: true }), + () => rm(directory, { force: true, recursive: true }), ], ), } diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index fb70042198..04544c93bb 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1679,6 +1679,15 @@ export function queryCollectionOptions( ) for (const [rowKey, owners] of hydratedOwners) { if (owners.size === 0) { + // An eager result covers every row. A restored direct-write row has + // no owner or same-run write position; its omission is authoritative. + // Same-run direct writes keep their fetch-generation precedence. + if ( + syncMode === `eager` && + persistence !== null && + !unownedDirectWritePositions.has(rowKey) + ) + ownedRows.add(rowKey) continue } @@ -2570,8 +2579,8 @@ export function queryCollectionOptions( ? queryCollectionSuccessfulFetchStarts.get(currentQuery) : undefined if (syncMode === `eager`) { - // A new complete snapshot replaces direct rows without subset owners. - // Hydrated rows with no claim are not necessarily this Query's rows. + // A later complete fetch also settles same-run ownerless direct rows. + // Restored ownerless rows are included in the hydrated baseline above. unownedDirectWritePositions.forEach((directPosition, key) => { if ( currentSyncedItems.has(key) && @@ -2779,13 +2788,16 @@ export function queryCollectionOptions( } else if ( !previouslyOwnedRows.has(key) && (result.isFetchedAfterMount || + (syncMode === `eager` && + persistence !== null && + directPosition === undefined) || (directPosition !== undefined && cachedRemoteFetchStart !== undefined && cachedRemoteFetchStart > directPosition)) && !deepEquals(currentSyncedItems.get(key), newItem) ) { - // A fetched row can first claim a directly inserted key. That - // claim also replaces the direct value with the server value. + // A result that first claims a restored peer row must apply its + // returned value too. Same-run direct rows retain fetch precedence. write({ type: `update`, value: newItem }) } if (addsOwner || insertsRow) { diff --git a/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts b/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts index ae3bdae2b9..961bce38e9 100644 --- a/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts +++ b/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts @@ -194,12 +194,15 @@ async function persisted( holder: Holder, keyCase: KeyCase, kind: StorageKind = `fake`, + dropFirstResult = false, ): Promise< Observation & { stored: Array timing: string waited: boolean commitsBeforeWrite: number + discardedWrites: number + readySignals: number } > { const { base, next } = scenario(keyCase) @@ -305,6 +308,9 @@ async function persisted( const errors = vi.spyOn(console, `error`).mockImplementation(() => {}) let sourceCommits = 0 + let discard = dropFirstResult + let discardedWrites = 0 + let readySignals = 0 const source = queryCollectionOptions({ id, queryKey, @@ -319,8 +325,17 @@ async function persisted( sync: (params) => sourceSync({ ...params, + write: (...args) => { + if (discard) discardedWrites++ + else params.write(...args) + }, + markReady: () => { + readySignals++ + params.markReady() + }, commit: (...args) => { sourceCommits++ + discard = false return params.commit(...args) }, }), @@ -373,6 +388,8 @@ async function persisted( timing, waited, commitsBeforeWrite, + discardedWrites, + readySignals, visible: sortRows(collection.values()), cache: sortRows(queryClient.getQueryData(queryKey) ?? []), stored: await storage.storedRows(id), @@ -396,8 +413,19 @@ async function compareWriteWithReference( holder: Holder, keyCase: KeyCase, kind: StorageKind = `fake`, + dropFirstResult = false, ): Promise { - const actual = await persisted(op, holder, keyCase, kind) + const actual = await persisted(op, holder, keyCase, kind, dropFirstResult) + if (dropFirstResult) { + expect( + actual.discardedWrites, + `mutant must discard source rows`, + ).toBeGreaterThan(0) + expect( + actual.readySignals, + `mutant must report source readiness`, + ).toBeGreaterThan(0) + } if (holder !== `startup`) expect( actual.commitsBeforeWrite, @@ -430,6 +458,19 @@ declareOracle({ ], }) +// A mutant that reports readiness but discards the first source transaction +// commits its empty metadata transaction and must fail the same comparison. +// Selecting its causal schedule +// cannot excuse missing source rows at the final observation cut. +for (const kind of [`fake`, `sqlite`] as const) + for (const op of ops) + it(`rejects a discarded first result for ${op} through ${kind}`, async () => { + oracleRoute(`calibration`) + await expect( + compareWriteWithReference(op, `startup`, `refetch-only`, kind, true), + ).rejects.toThrow(/reference/) + }) + describe(`persisted direct writes behind a waiting refetch`, () => { for (const holder of holders) { for (const keyCase of keyCases) { diff --git a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts index e4841b4f9b..374a148e9c 100644 --- a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts +++ b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts @@ -49,10 +49,21 @@ const queryStartupHistoryArb: fc.Arbitrary = fc .map(({ membership, ...schedule }) => ({ ...membership, ...schedule })) .chain((h) => fc - .tuple(persistedStartupRenderArb, fc.boolean(), fc.boolean()) - .map(([network, fallback, mutation]) => + .tuple( + persistedStartupRenderArb, + fc.boolean(), + fc.boolean(), + fc.boolean(), + ) + .map(([network, fallback, mutation, ownerless]) => withStartupRender( - { ...h, ...(mutation ? { mutation: `persisting` as const } : {}) }, + { + ...h, + ...(mutation ? { mutation: `persisting` as const } : {}), + ...(ownerless && h.previous + ? { previousOwner: `none` as const } + : {}), + }, network, fallback && h.origin === `fetch`, ), diff --git a/packages/query-db-collection/tests/persisted-startup-driver.ts b/packages/query-db-collection/tests/persisted-startup-driver.ts index 9388c5aa46..6c1890ac25 100644 --- a/packages/query-db-collection/tests/persisted-startup-driver.ts +++ b/packages/query-db-collection/tests/persisted-startup-driver.ts @@ -10,6 +10,7 @@ import { queryCollectionOptions } from '../src/query' import { sortedStartupRows, startupRows, + uniqueStartupHistories, withStartupMutations, withStartupRender, } from '../../db-adapter-conformance/tests/persisted-startup-oracle' @@ -98,14 +99,35 @@ const baseQueryStartupHistories: Array = [ peer: true, response: `before-restore`, }, + ...( + [ + `initial-disabled`, + `initial-pending`, + `cached-disabled`, + `cached-pending`, + ] as const + ).map((origin): QueryStartupHistory => ({ + origin, + previous: 3, + next: 3, + peer: true, + response: `before-restore`, + })), ] -export const queryStartupHistories = withStartupMutations( - baseQueryStartupHistories.flatMap((h) => [ - h, - withStartupRender(h, true, false), - ...(h.origin === `fetch` ? [withStartupRender(h, true, true)] : []), - ]), +export const queryStartupHistories = uniqueStartupHistories( + withStartupMutations( + baseQueryStartupHistories + .flatMap((h): Array => [ + h, + ...(h.previous ? [{ ...h, previousOwner: `none` as const }] : []), + ]) + .flatMap((h) => [ + h, + withStartupRender(h, true, false), + ...(h.origin === `fetch` ? [withStartupRender(h, true, true)] : []), + ]), + ), ) type QueryMetadata = @@ -121,7 +143,7 @@ export const queryStartupAdapter: PersistedStartupAdapter = seedMetadata: (h, id, row) => ({ queryCollection: { owners: { - [hashKey([id])]: true, + ...(h.previousOwner === `none` ? {} : { [hashKey([id])]: true }), ...(h.peer && row.id === startupRows(h.previous, 1)[0]!.id ? { peer: true } : {}), diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 071a5979dc..bd1159a187 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -7,7 +7,10 @@ import { rxStorageWriteErrorToRxError, } from 'rxdb/plugins/core' import DebugModule from 'debug' -import { withCollectionConfigFactory } from '@tanstack/db' +import { + validateSyncPersistenceCapability, + withCollectionConfigFactory, +} from '@tanstack/db' import { stripRxdbFields } from './helper' import type { FilledMangoQuery, @@ -127,6 +130,10 @@ export function rxdbCollectionOptions( const sync: SyncConfig = { sync: (params: SyncParams) => { const { begin, write, commit, markReady, fail, collection } = params + const persistence = + params.metadata === undefined + ? null + : validateSyncPersistenceCapability(params.metadata.persistence) let initialFetchComplete = false async function initialFetch() { @@ -142,7 +149,7 @@ export function rxdbCollectionOptions( // commit ordering with pending mutations. begin({ snapshot: - !!params.metadata?.persistence || + persistence !== null || !collection._state.acceptedSyncedEntries().next().done, }) diff --git a/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index bc4a5790cd..9a65865e93 100644 --- a/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -24,5 +24,6 @@ describe(`persisted startup conformance: RxDB`, () => { adapter: rxdbStartupAdapter, histories: persistedStartupHistories, arbitrary: historyArb, + scopedStartupCommit: true, }) }) diff --git a/packages/rxdb-db-collection/tests/persisted-startup-driver.ts b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts index b160b60859..2cde8fd806 100644 --- a/packages/rxdb-db-collection/tests/persisted-startup-driver.ts +++ b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts @@ -30,7 +30,7 @@ let sequence = 0 export const rxdbStartupAdapter: PersistedStartupAdapter = { scope: `collection`, - async create(persistence, history, id) { + async create(persistence, history, id, metadataTransform) { const database = await createRxDatabase<{ rows: RxCollection }>({ @@ -74,7 +74,7 @@ export const rxdbStartupAdapter: PersistedStartupAdapter Date: Tue, 10 Nov 2026 12:51:37 -0700 Subject: [PATCH 16/24] test: close persisted startup receiving gaps and measure costs Law: preserve direct-write fetch order, on-demand scope, complete reload replacement, and cleanup authority under the documented Query, SQLite, Collection reload, and TrailBase lifecycle contracts. Reach: add 104 eager Query, 48 scoped Query, two cached-peer, 24 TrailBase, and 24 histories per unwrapped RxDB/PowerSync receiver. Chromium receives all 226 shared histories with separate required reach. Node hosts also run fixed and fresh campaigns at their existing/new declared run budgets. RED/Mutant: exact immutable source transforms reject the missing derived baseline and six initial CLI survivors at public/durable checkpoints. Checked mutant replays fail and clean replays pass at the same coordinates. The full initial audit classifies 61 operators: 49 kills, ten survivors, two timeouts. Additive receiver experiments reject six more survivors. GREEN: unchanged production passes Query neighbors (565), scope/cache (16), TrailBase lifecycle (153), reload (54), capability registrations (176), and Chromium (226 histories). Types, ESLint, formatting and changed-file oracle lint pass. No production changes in this commit. Limits: finite values, histories and successful local SDK/storage paths. Four selected operators preserve the checked behavior within bounded domains; two timeouts remain separate outcomes. No universal-equivalence claim. Measurements: 112 fresh-process observations quantify snapshot-selection costs. PowerSync 10k matching startup median increases 16.5%; 50k sampled heap maxima increase for both adapters. Reports retain raw data, controlled baseline, warmed OS cache and sampled-memory limits. No parity claim. --- docs/contributing/oracle-coverage.md | 85 +++- .../2026-11-10-adapter-reload-gaps.md | 100 +++++ .../2026-11-10-persisted-precedence-gaps.md | 189 +++++++++ ...26-11-10-persisted-startup-measurements.md | 192 +++++++++ ...26-11-10-retained-scoped-hydration-gaps.md | 191 +++++++++ .../2026-11-10-startup-gap-current-head.md | 258 ++++++++++++ .../2026-11-10-trailbase-hydration-gap.md | 204 ++++++++++ .../eager-gap-conformance-oracle.opfs.spec.ts | 50 +++ .../e2e/eager-gap-conformance.opfs.html | 11 + .../e2e/eager-gap-conformance.opfs.ts | 179 ++++++++ .../playwright.opfs.config.ts | 1 + packages/db-adapter-conformance/README.md | 16 + .../tests/eager-reload-driver.ts | 162 ++++++++ .../tests/eager-reload-oracle-suite.ts | 109 +++++ .../tests/eager-reload-oracle.ts | 201 +++++++++ ...sted-capability-conformance-oracle.test.ts | 41 +- .../persisted-capability-oracle-suite.ts | 47 +++ packages/db/tests/oracle-config.ts | 7 + packages/db/tests/oracle-replay-manifest.ts | 47 ++- .../tests/eager-reload-browser.ts | 20 + .../tests/eager-reload-common.ts | 44 ++ ...reload-conformance-oracle.property.test.ts | 14 + .../tests/eager-reload-driver.ts | 34 ++ ...sted-capability-conformance-oracle.test.ts | 15 + packages/query-db-collection/package.json | 2 +- .../tests/direct-write-precedence-oracle.ts | 29 ++ .../tests/ownership-lifecycle.oracle.test.ts | 13 +- .../tests/persisted-precedence-driver.ts | 277 +++++++++++++ .../tests/persisted-precedence-oracle.test.ts | 170 ++++++++ .../tests/persisted-precedence-oracle.ts | 200 +++++++++ .../tests/retained-scoped-hydration-driver.ts | 319 +++++++++++++++ .../retained-scoped-hydration-oracle.test.ts | 230 +++++++++++ .../tests/retained-scoped-hydration-oracle.ts | 291 +++++++++++++ ...reload-conformance-oracle.property.test.ts | 14 + .../tests/eager-reload-driver.ts | 52 +++ ...sted-capability-conformance-oracle.test.ts | 15 + .../trailbase-db-collection/tests/ORACLE.md | 48 +++ .../tests/lifecycle-hydration-driver.ts | 285 +++++++++++++ .../tests/lifecycle-hydration-oracle.ts | 167 ++++++++ .../tests/lifecycle-oracle.property.test.ts | 89 +++- .../bench/persisted-startup-2026-11-10.csv | 177 ++++++++ scripts/bench/persisted-startup-worker.ts | 382 ++++++++++++++++++ scripts/bench/persisted-startup.mjs | 191 +++++++++ 43 files changed, 5103 insertions(+), 65 deletions(-) create mode 100644 docs/contributing/oracle-reviews/2026-11-10-adapter-reload-gaps.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-persisted-precedence-gaps.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-persisted-startup-measurements.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-retained-scoped-hydration-gaps.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-startup-gap-current-head.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-trailbase-hydration-gap.md create mode 100644 packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance-oracle.opfs.spec.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.ts create mode 100644 packages/db-adapter-conformance/tests/eager-reload-driver.ts create mode 100644 packages/db-adapter-conformance/tests/eager-reload-oracle-suite.ts create mode 100644 packages/db-adapter-conformance/tests/eager-reload-oracle.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-capability-oracle-suite.ts create mode 100644 packages/powersync-db-collection/tests/eager-reload-browser.ts create mode 100644 packages/powersync-db-collection/tests/eager-reload-common.ts create mode 100644 packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts create mode 100644 packages/powersync-db-collection/tests/eager-reload-driver.ts create mode 100644 packages/powersync-db-collection/tests/persisted-capability-conformance-oracle.test.ts create mode 100644 packages/query-db-collection/tests/direct-write-precedence-oracle.ts create mode 100644 packages/query-db-collection/tests/persisted-precedence-driver.ts create mode 100644 packages/query-db-collection/tests/persisted-precedence-oracle.test.ts create mode 100644 packages/query-db-collection/tests/persisted-precedence-oracle.ts create mode 100644 packages/query-db-collection/tests/retained-scoped-hydration-driver.ts create mode 100644 packages/query-db-collection/tests/retained-scoped-hydration-oracle.test.ts create mode 100644 packages/query-db-collection/tests/retained-scoped-hydration-oracle.ts create mode 100644 packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts create mode 100644 packages/rxdb-db-collection/tests/eager-reload-driver.ts create mode 100644 packages/rxdb-db-collection/tests/persisted-capability-conformance-oracle.test.ts create mode 100644 packages/trailbase-db-collection/tests/lifecycle-hydration-driver.ts create mode 100644 packages/trailbase-db-collection/tests/lifecycle-hydration-oracle.ts create mode 100644 scripts/bench/persisted-startup-2026-11-10.csv create mode 100644 scripts/bench/persisted-startup-worker.ts create mode 100644 scripts/bench/persisted-startup.mjs diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index e5d21c5327..ca6fe60f2b 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -7830,19 +7830,78 @@ and PowerSync sources supply no remote-replication claim. See [network-first evidence](oracle-reviews/2026-11-10-network-first-startup.md), and [external-review amendment](oracle-reviews/2026-11-10-pr-2394-external-review.md). -The [diff-focused gap CLI audit](oracle-reviews/2026-11-10-startup-gap-cli.md) -records mutation evidence separately from startup conformance. Open receiving -distinctions are unwrapped retained-row reload and empty-load mutation ordering -for RxDB/PowerSync (`M9`/`M164`, core subscription-lifecycle owner with adapter -receivers), cleanup during TrailBase's held persisted hydration (`M150`, its -lifecycle owner plus this persistence composition), and Query's redundant -hydration/ownership classification (`M1466`/`M1458`, ownership-lifecycle and -reload-work owners), older fetch generations crossing newer direct writes -(`M1778`/`M1779`, direct-write-window owner), and derived-select results without -an inverse cache mapping (`M1733`, ownership-lifecycle/direct-write-window). -The audit names each required history and observation. These survivors are -unresolved sensitivity signals, not approved equivalences or known product -counterexamples. They do not narrow the startup comparisons above. +The [original diff-focused gap CLI audit](oracle-reviews/2026-11-10-startup-gap-cli.md) +is historical; the [current audit](oracle-reviews/2026-11-10-startup-gap-current-head.md) +records subsequent classification separately from startup conformance. +The ownership oracle now shares its independent direct-write overlay judgment +with [persisted precedence receivers](../../packages/query-db-collection/tests/persisted-precedence-oracle.test.ts). +They cross raw/derived-select responses without an inverse cache mapping, +four direct-write operations, before-result and before/after captured baseline +reads, early insert/upsert before persisted restore, writes before the first +physical fetch, cached results without a physical fetch, and a later fetch. +Each Node host runs 104 fixed histories and fixed/fresh campaigns of 80 histories; +the [Chromium receiving page](../../packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance-oracle.opfs.spec.ts) +runs the same 104 histories and judgment. Comparisons cover the accepted write +when its baseline is already public, first-result public/durable/cache state, +and public/durable state at later fetch settlement. The original derived-baseline +survivor (`M1733`, now `M1748`) is rejected at first-result public and durable +cuts when an omitted saved row survives. See the [precedence amendment](oracle-reviews/2026-11-10-persisted-precedence-gaps.md). + +The [retained-scope receiver](../../packages/query-db-collection/tests/retained-scoped-hydration-oracle.test.ts) +judges on-demand demand restore and scope-local omission. It crosses two disjoint +exact scopes, empty/nonempty responses, an ownerless saved row in either scope, +and unbounded versus one-row ordered demand. Each controlled/Node SQLite host +runs 48 fixed histories plus fixed/fresh campaigns of 60. Pending public rows, +first demand public/durable rows, and second demand public/durable rows share +one independent set-selection judgment. Always hydrating the full baseline +(`M1481`) wrongly exposes unrequested rows; broadening ownerless reconciliation +(`M558/M559`) wrongly deletes saved direct rows from a limited result. The +comparison rejects each at its first affected cut. + +The same receiver adds a cached-peer first-claim judgment: a manual cache edit +or an older fetched cache cannot replace a continuously owned row established +by a later physical fetch. A new physical refetch can replace it. Two fixed +histories and fixed/fresh campaigns of 60 per host compare public and durable +values at claim and refetch. `M1376/M1377` fail at the claim cuts. Chromium +receives both shared grammars and comparisons (50 histories). Exact mutant and +clean replay coordinates are retained in the +[scope/provenance report](oracle-reviews/2026-11-10-retained-scoped-hydration-gaps.md). +This does not establish arbitrary overlapping predicates, concurrent writers, +or failed I/O behavior. + +The core reload/snapshot law also receives unwrapped real RxDB and PowerSync +through the shared [eager-reload judgment](../../packages/db-adapter-conformance/tests/eager-reload-oracle.ts), +[RxDB receiver](../../packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts) +and [PowerSync receiver](../../packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts). +They cross fresh and empty/nonempty retained reload, complete empty/change/omission +results, and a disjoint persisting optimistic insert. Each adapter runs 24 fixed +histories plus fixed/fresh campaigns of 80; Chromium receives the same 24. +Fulfillment compares one coherent permitted source projection with the required +optimistic overlay; readiness can precede application while a mutation persists. +After mutation settlement and applied receipts, exact source rows are required. +The exact historical snapshot-predicate negations (`M9`/`M164`) now fail +retained-row-to-empty-result histories. See the [reload amendment](oracle-reviews/2026-11-10-adapter-reload-gaps.md). +These unwrapped histories establish no persistence or remote-replication claim. + +TrailBase's [primary lifecycle owner](../../packages/trailbase-db-collection/tests/lifecycle-hydration-oracle.ts) +now crosses cleanup while actual persisted hydration completion is held, old +list completion/rejection, and replacement runs that are loading or ready. +Controlled and real Node SQLite receive 24 fixed histories plus fixed/fresh +campaigns of 50 per host; Chromium receives the same 24 histories. The comparisons +check public/durable replacement rows, a later event and reader/error ownership. +The exact old cancellation-guard removal (`M150`, now `M151`) reaches obsolete +begin attempts but preserves these observations through existing wrapper/core +fences: bounded equivalence for these histories, rather than a global claim +that cancellation checks are unnecessary. See the [lifecycle amendment](oracle-reviews/2026-11-10-trailbase-hydration-gap.md). + +Query's non-retained hydration classification and redundant-call operators remain +separate from the retained precedence receiver. Their physical-read budget belongs +to the reload-work owner and shared startup comparisons. The current audit records +55 observed assertion rejections after additive receiving experiments, four +bounded survivors, and two timeouts across the 61 changed-line operators. +Timeouts supply no behavioral rejection evidence. Startup latency/memory are +measured separately in the [performance report](oracle-reviews/2026-11-10-persisted-startup-measurements.md). +These additions preserve the startup comparisons and their limits above. ## Adapter conformance: eager live sources (#2118) diff --git a/docs/contributing/oracle-reviews/2026-11-10-adapter-reload-gaps.md b/docs/contributing/oracle-reviews/2026-11-10-adapter-reload-gaps.md new file mode 100644 index 0000000000..4a7769fb88 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-adapter-reload-gaps.md @@ -0,0 +1,100 @@ +# RxDB and PowerSync unwrapped eager reload gaps + +Starting head: `5feb26616a8357853454c2f9ff2ce0b0adb9c908`. +Historical gap IDs: RxDB `M9`, PowerSync `M164`. These names identify the +earlier operand-negation mutations; operator IDs can move when source changes. +This is a self-review and receiving-oracle extension, not a source repair. + +## Law and receiving boundary + +A reload keeps the old public rows until the new run supplies its result. A +complete eager result then determines source membership and returned values. +Only the adapter's snapshot or truncate replaces retained rows; core cannot +infer replacement from reload. Authority: the glossary's reload/snapshot +contract, the core source-health owner's opt-in snapshot contract, and eager +source READY-3/PUB-7. + +The shared persisted-startup owner already has the independent complete-result +set judgment. The new unwrapped eager-reload receiver reuses that judgment +rather than duplicating its row model. Core's source-health owner retains +status, health and arbitrary core reload histories. Its model is embedded in +its test runner; importing that runner as an adapter helper would register all +core tests. This receiver composes its established baseline-retention law with +the shared adapter result judgment at the real SDK boundary. + +The persisted startup tests could not see either mutant: the persistence +capability alone selects snapshot mode, masking the retained-row operand. The +new receiver deliberately uses an unwrapped Collection. It retires the previous +sync run, holds the next run before adapter entry, changes the actual provider +rows while no current SDK subscription can deliver those changes, then invokes +the real sync adapter. A live delete event cannot repair a failed omission sweep. +RxDB uses its real memory-storage SDK. PowerSync uses local SQLite on Node, and +the browser export uses the web SDK. + +## Grammar and observations + +Fresh load has no previous source rows. Reload independently permits empty and +nonempty retained rows. Every next subset of three source keys is legal, with +changed values on overlap. A disjoint optimistic insert can persist across the +source submission. Twenty-four fixed histories and fixed/fresh generated +campaigns use the same SDK driver and comparison. Generated lanes independently +weight fresh, empty reload, and retained reload, avoiding rare empty-reload reach. + +Before adapter entry, the public rows must retain the old baseline plus a +pending optimistic overlay. The load cannot fulfill before source entry. At +fulfillment, accepted source commits need not have applied while an optimistic +transaction is persisting. The source projection must be one coherent allowed +snapshot: the retained baseline or the complete result. Partial mixtures fail. +This follows the glossary's accepted-sync-transaction and reload/markReady +contracts; snapshot application can replace earlier, while ordinary committed +writes may remain queued. A still-pending optimistic row remains public until +its mutation settles. The driver records release of its external persistence +gate: before release the overlay is required; after release its presence can +race fulfillment. After mutation settlement and applied receipts, exact rows +must equal the complete result with no unconfirmed local row. Submitted +row/commit counts and completed acceptance receipts prove actual source entry. + +The first instrument incorrectly compared full fulfillment rows with the +source-only answer. A second version required applied source rows at fulfillment, +but an empty-baseline reload can legally fulfill at markReady while accepted +writes remain queued behind an optimistic transaction. These were instrument +failures. The correction follows the existing optimistic-overlay, acceptance, +and reload contracts; no production edit followed either failure. Initial logs +remain preserved as instrument corrections, not product RED. The reference +allows whole source snapshots, rather than selecting an expectation from +production commit counts. + +Removing reload loses the retained-row omission. Removing changed membership +loses stale omitted rows. Removing changed values loses overlap correctness. +Removing the mutation lane loses the empty-baseline scheduling boundary. +Fresh histories with retained source rows are constructively excluded. The +optimistic key cannot overlap the three source keys. + +## Evidence + +No source repair was needed. The expanded oracle passes unchanged production +on starting HEAD, and rejects both exact historical operand-negation mutants. +The new coverage closes a missing receiving distinction; it does not establish +a new product defect on this head. + +- **GREEN:** `pnpm --config.verifyDepsBeforeRun=false exec vitest run packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts` passes 54 checks. Per adapter: 24 fixed histories, one hostile-declaration calibration, and fixed/fresh generated campaigns of 80 histories each. Log: `.review-2367/reload-gaps/green-node-final3.log`. +- **Reach:** every one of the 11 declared states is positive separately in each adapter and generated campaign. Fixed-seed 2367 counts, for each adapter, are fresh 29, empty reload 28, retained reload 23, empty result 15, omission 14, changed overlap 13, persisting mutation 34, no mutation 46, fresh-held 12, empty-reload-held 14, retained-reload-held 8. Fresh-seed campaigns independently meet every state. +- **Mutant RED:** copied each real adapter module, changing only `!collection._state.acceptedSyncedEntries().next().done` to the unnegated operand; absolute import rewrites let the unchanged receiver load the copy. `pnpm --config.verifyDepsBeforeRun=false exec vitest run --config .review-2367/reload-gaps/mutant.config.ts packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts` rejects RxDB; the equivalent PowerSync path rejects PowerSync. Fixed generated seed 2367 reaches `{start: 'reload', previous: 1, next: 0, mutation: false}` and reports both fulfillment and final stale rows: observed `[{id: 'a', value: 1}]`, expected `[]`. This is a result mismatch after real source commit and acceptance, not a timeout. Logs: `rxdb-negation-mutant.log` and `negation-mutants2.log` under the same scratch directory. Initial import-resolution failures remain separately recorded as setup failures. +- **Real browser GREEN:** the shared Chromium receiving page passes 120 histories, including 24 RxDB and 24 PowerSync reload histories, with no page errors or remaining OPFS artifacts. Each reload receiver separately reaches fresh 6, empty reload 6, retained reload 12, empty result 8, omission 8, overlap 6, mutation 12, no mutation 12, fresh-held 3, empty-reload-held 3, retained-reload-held 6. Command: `pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/browser-db-sqlite-persistence exec playwright test --config playwright.opfs.config.ts eager-gap-conformance-oracle.opfs.spec.ts`. Log: `.review-2367/gap-fill/opfs-gap.log`. The browser shares the exact model, histories and comparison; RxDB uses its memory SDK and PowerSync its real browser SQLite SDK. +- **Replay:** `TANSTACK_DB_ORACLE_PROPERTY=adapter.rxdb.eager-reload TANSTACK_DB_ORACLE_SEED=42 TANSTACK_DB_ORACLE_PATH=0 node --import tsx packages/db/tests/oracle-replay.ts packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts --coverage.enabled=false` reports one execution and one successful generated run. The analogous PowerSync property and receiving file also pass. +- **Types:** `pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/db-adapter-conformance exec tsc --noEmit` passes with these new shared files included. Both receiving packages also pass their own `tsc --noEmit` commands. The shared receiver type exposes only the common config fields, preserving each SDK schema and utilities in the runtime spread. Targeted ESLint results are recorded in `.review-2367/reload-gaps/eslint-final.log`. Required `pnpm --config.verifyDepsBeforeRun=false lint:changed` also passes (`lint-changed-final.log`) after copied diagnostic CLI source trees were moved out of the checkout; no lint rule was disabled. + +The unchanged-source GREEN, exact negation-mutant RED, and whole-snapshot +comparison jointly supply sensitivity evidence. No production behavior or +existing run budget changed. The initial invalid instrument's RED results +are deliberately excluded from product-bug evidence. + +## Limits + +Three string keys and numeric values, one fresh load or one eager reload, one +disjoint optimistic insert, and local SDK storage. This receiving family does +not assert a new no-snapshot scheduling law on empty startup, remote provider +replication, persisted durability, arbitrary concurrent mutations, every +intermediate publication, or on-demand reload. The existing owners retain those +contracts. Worker/OPFS startup coverage remains a neighboring persisted path; +wrapping this receiver in persistence would mask the operand under judgment. diff --git a/docs/contributing/oracle-reviews/2026-11-10-persisted-precedence-gaps.md b/docs/contributing/oracle-reviews/2026-11-10-persisted-precedence-gaps.md new file mode 100644 index 0000000000..5fc9f6c1ee --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-persisted-precedence-gaps.md @@ -0,0 +1,189 @@ +# Persisted fetch/write precedence: gap amendment + +Production source: `5feb26616a8357853454c2f9ff2ce0b0adb9c908`. +This amendment adds oracle coverage without changing production. The previous +mutation [audit](2026-11-10-startup-gap-cli.md) remains historical evidence. + +## Law and independent judgment + +An older fetch cannot undo the keys of an accepted newer direct write. A later +physical fetch may replace those keys. A complete eager result removes saved +rows that it omits, while preserving the newer write. The authority is +`docs/collections/query-collection.md`, **Direct Writes and Query Sync**; +its **Individual Write Operations** section permits full-row inserts/upserts +before initial loading finishes. Updates/deletes need an accepted target row. + +The existing ownership oracle's keyed overlay computation now lives in +`direct-write-precedence-oracle.ts`. Both the ownership oracle and the new +persisted receiver use that computation. It overlays authored writes/deletions +on a complete fetched row set. It imports no Query generation classifier, +ownership machinery, persistence helper, or cache patch algorithm. + +The receiver additionally checks the documented cache boundary: raw responses +can represent the direct write, while a derived select without an inverse keeps +its fetched response envelope. Collection rows still obey write precedence. +All value comparisons project keyed `id`/`value` rows; virtual row properties, +ownership metadata, and event counts are outside this added judgment. + +## Histories, reach, and observation cuts + +`persisted-precedence-oracle.ts` defines 104 fixed histories. Fixed-seed 2394 and +fresh campaigns each generate 80 histories per Node host, using the same +grammar, production driver, comparison, and budget. The dimensions are: + +- Raw rows or an envelope whose select clones its nested nodes. +- Insert/update/delete/upsert after restore, before the old result returns. +- Those four operations while the result's retained-baseline scan waits, + either before the real read or after that read captured its rows. +- New-key insert/upsert authored while the initial persisted restore is held, + then the older result returns before restore is released. +- A direct write before the first physical fetch, after a disabled Query restores. The later complete fetch may retire that write. +- A cached success offered after restore, with no physical fetch, while its retained-baseline read is held. +- An optional later physical refetch and values from -10 through 10. + +The baseline has three source-owned saved keys retained until revalidation; +the fetched result changes two and omits the third. A direct insert/upsert +adds a fourth key. Removing the held-restore dimension loses the historical +derived-baseline mutant below. Removing derived select loses the no-inverse +branch. Removing the after-read hold loses captured-stale-baseline coverage. +Removing later fetch loses evidence that newer source authority can replace +the direct write. Update/delete before restore are excluded because no target +row has been accepted; they are a different write-validation history, already +owned by the direct-write-window oracle. + +The driver seeds real storage through a persisted Collection, then uses a real +QueryClient, Query Collection, and persistence wrapper. Gates delay real +collaborator completion; they supply no expected rows. The Node adapters hold +the real full-row restore, including its hydration-scoped adapter. Chromium +uses the existing OPFS snapshot-read seam, because its internal restore path +cannot be held by wrapping only the public adapter method. + +After-restore histories compare immediately after the accepted direct write, +while the old source result remains held. All histories compare public rows, +durable rows and cache projection at the first complete result's applied +receipts, before refetch can repair them. Optional refetch compares public rows +inside fulfillment and durable rows afterward. A held-restore history makes no +early assertion that saved rows were already public. + +Reach is enforced separately per host and campaign. Chromium receives all +104 histories, using the same judgment, in the shared eager-gap page. Its exact +counts are: + +| State | Histories | +| --- | ---: | +| raw / derived | 52 / 52 | +| fetched-result / cached-result | 72 / 32 | +| before-result / before-fetch / before-restore / during-baseline | 16 / 16 / 8 / 64 | +| before-read / after-read | 32 / 32 | +| insert / update / delete / upsert | 28 / 24 / 24 / 28 | +| later-fetch | 52 | + +The browser page also receives 48 on-demand Query scope histories, two cached-peer claim histories, 24 TrailBase and 24 histories for each unwrapped +RxDB/PowerSync reload receiver: 226 total. It enforces each receiver's reach, +checks page errors, and checks that fixture database artifacts were removed. + +## Experiments + +The new Node suite passes eight tests: for each controlled and real Node SQLite +host, 104 fixed histories, two 80-history campaigns, and a checker calibration +that removes the newer inserted row from a real observation. Current production +also passes the 226-history Chromium receiving run. No production edit preceded +these runs, and no production edit was needed afterward. + +The exact historical derived-baseline mutant, now `query.ts:M1748`, replaces +`needsPersistedBaseline(hashedQueryKey)` with false only in the deferred +no-inverse-select branch. An immutable module transform runs it in a separate +Vitest process; the shared source file is never overwritten. It passes the +earlier 48-history fixture, whose writes all waited for restored rows. After +the held-restore dimension is added, both Node hosts reject it at **first** and +**firstDurable**: `obsolete` survives even though the fetched result omitted it. +The newer inserted key remains, distinguishing the omission defect from write +loss. This demonstrates why counting a later refetch alone would be inadequate. + +The `M1793` cutoff mutant changes `??` to `&&` in the general-result branch. +The cached-success dimension rejects it at first-result public and durable +cuts: a newer update is replaced by the cached old value. Node SQLite shrinks +to raw cached success, before-read hold, update, no later fetch, value 0, +campaign seed -946755224 and path `5:1:1`. Both Node hosts reject the mutant in +a guarded replay and pass current production at the same coordinates. + +Other exact controls preserve legal implementation freedom: + +- `M563` includes a same-run ownerless direct row in the hydrated owner set. + It passes all 104 histories and both generated campaigns on both hosts, + including writes before the first physical fetch. Existing direct-row + lifetime/absence machinery still supplies the public and durable answer. + This is bounded eager equivalence, not on-demand evidence. +- `M1794` removes the stored fetch-start lookup but retains the captured direct + write position. It passes the same comparisons, including cached results and + writes during both baseline holds. At general-result enqueue, a direct write + already newer than the fetch enters the earlier deferred branch instead. + Otherwise, both cutoffs identify the same later same-Collection key writes. + Cached results already use the captured-position fallback. This argument + explains the observed equivalence within these legal result paths. +- `M1473` forces reconciliation after eager ownership exists. It passes 250 + registrations: the new precedence receiver and the shared startup suite, + including the startup owner's physical full-row-read budget. Its later + hydration request reuses the accepted baseline. Extra optional capability + calls alone do not violate a promised no-call law. + +These isolated runs disable Vitest's filesystem module cache, use a distinct +cache directory/plugin identity for each operator, and log the applied mutant's +SHA256. Initial standalone runs that reused a config identity are superseded; +they do not supply the above classifications. The ordinary gap CLI already +uses equivalent cache protections. The derived-baseline and cutoff controls +both fail in the isolated rerun; the three bounded-equivalence controls pass. + +The generated Node SQLite failure shrinks to derived select, before-restore, +insert, no later fetch, value 0. Its final-grammar campaign seed is 1337087813 and shrink path +`8:1:1:1`. The earlier 56-history grammar replay used seed 290176300 and +path `3:1:1:1:1`; that coordinate remains historical. The guarded replay records one failed execution on each host under +the mutant and one successful execution on each host with current production. +The fixed histories reconstruct the same ordering with value 9. + +Commands from the worktree root: + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/query-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false tests/persisted-precedence-oracle.test.ts +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/query-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false tests/ownership-lifecycle.oracle.test.ts tests/persisted-direct-write-window.oracle.test.ts tests/persisted-startup-conformance-oracle.test.ts tests/persisted-precedence-oracle.test.ts +``` + +The second command passes 565 tests with the final 104-history grammar. +From `packages/query-db-collection`, the clean checked replay is: + +```sh +TANSTACK_DB_ORACLE_SEED=1337087813 TANSTACK_DB_ORACLE_PATH=8:1:1:1 TANSTACK_DB_ORACLE_PROPERTY=query.persisted-write-precedence node --import tsx ../db/tests/oracle-replay.ts tests/persisted-precedence-oracle.test.ts --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false +``` + +From `packages/browser-db-sqlite-persistence`: + +```sh +pnpm --config.verifyDepsBeforeRun=false exec playwright test --config playwright.opfs.config.ts eager-gap-conformance-oracle.opfs.spec.ts +``` + +Scratch logs, the immutable mutant config, replay witnesses and the exact +assertion differences remain in `.review-2367/gap-fill/`. + +## Independent challenge and scope + +A separate agent reviewed the law, model, driver and comparisons. It identified +the initially fresh-read-only scan gate; the after-read dimension addresses that +concern. Mutation execution then identified the fixture's unconditional +restored-row wait; the held-restore dimension addresses that gap. Neither +correction changes a product expectation to match production. + +This receiver covers one eager sync run, one direct write, three saved string +keys and numeric values, optional later fetch, and no peer owners or failed I/O. +It does not establish on-demand window behavior, overlapping optimistic +transactions, every intermediate publication, arbitrary metadata, or a network +transport. The existing shared startup owner separately exercises non-retained +baselines, peers, ownerless rows, optimistic holds and network-first fallback. +This retained receiver supplies no evidence for non-retained hydration +classification merely by recording scan counts. + +The [current mutation audit](2026-11-10-startup-gap-current-head.md) separately +classifies redundant hydration and fetch-cutoff operators. It preserves +implementation freedom when a changed internal call cannot change the promised +public/durable result or the established physical-read budget. The separate +[measurement report](2026-11-10-persisted-startup-measurements.md) quantifies +startup costs; this correctness result does not establish performance parity. diff --git a/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-measurements.md b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-measurements.md new file mode 100644 index 0000000000..f68adb0327 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-measurements.md @@ -0,0 +1,192 @@ +# Persisted eager startup: latency and memory + +Measured source: `5feb26616a8357853454c2f9ff2ce0b0adb9c908`. +The measurement task is separate from the correctness oracle campaign. It +does not establish a new startup-time or memory contract. + +## Experiment + +The two source variants differ only in eager snapshot selection for RxDB and +PowerSync. The current variant uses the reviewed source. The prior-selection +variant removes the `persistence !== null ||` operand from each adapter's +selection, matching `origin/main`'s decision. Capability validation, core, +persistence, SDKs, dependencies, and the harness stay identical. This is a +controlled comparison of the PR's snapshot change, not a full main checkout. + +The runner bundles current TypeScript sources in production mode and minifies +them. It does not use the test runner, oracle histories, or fixture drivers. +It does not reproduce the Vite package build's private-property mangling. +Each observation starts a fresh Node process. Each source uses its actual SDK: +RxDB memory storage or PowerSync Node SQLite. The wrapped Collection persists +to a second, real file-backed Node SQLite database, using WAL and NORMAL +synchronous mode through the existing SQLite persistence adapter. + +Each worker prepares the provider's rows and saved SQLite rows before timing. +It closes and reopens the persisted database and creates a fresh persistence +adapter, so no seeding adapter schema/snapshot cache carries into startup. +The saved baseline is empty or exactly matches the provider. These inputs +avoid the known stale-row defect in the old selection and isolate work costs. +The OS file cache remains warm. Source SDK setup and process import time are +excluded from the elapsed startup interval. + +Rows contain a string ID, number, and a 256-byte string payload. Source reads +use a batch size of 1,000. The campaigns cross 1,000 and 10,000 source rows, +empty and matching saved baselines, and a 50,000-row matching-baseline case. +Additional 10,000-row matching-baseline cases hold restore for 300 ms, or use +network-first initial render with a 25 ms deadline while holding the source +for 350 ms. Those are controlled delays, not measured network latency. + +Each cell has one discarded process warmup per variant and three measured +processes per variant. The order alternates prior/current then current/prior. +Garbage collection runs once after fixtures are prepared and before timing; +normal GC remains enabled during startup. Team test/build/mutation workers +pause for the final paired runs. No CPU affinity or OS cache flush is used. + +The runner records these distinct observations: + +- SQLite's full-row restore-read completion. +- The existing `hydrateBaseline()` capability's settlement. Its work may + include queued source persistence, so it is not a first-paint measurement. +- Public `preload()` fulfillment and first source commit invocation. +- Full initial source result after all its applied receipts settle. +- Source and durable transaction/write/truncate counts. +- Heap, RSS, and external memory sampled at 2 ms intervals, source batches, + every 512 source writes, commits, and the completion boundaries. +- Process RSS high-water mark before and after the timed startup. This + lifetime measure also includes fixture preparation and module loading. + +Sampled memory maxima are lower bounds on the true instantaneous startup +peaks. The report retains baseline memory and absolute sampled maxima. It does +not subtract process RSS readings and label the difference an allocation count. +Both SDK modules are loaded by the common harness, so absolute process memory +also includes its common module overhead. Final row counts are checked in the +Collection and SQLite after the timed observation. + +## Results + +### Natural startup + +Times are full initial source result applied, in milliseconds. Each entry is +the median and [minimum–maximum] of three processes. Memory is the median of +the three absolute sampled startup maxima, in MiB. It includes retained source +SDK state and common harness modules. + +| Adapter | Rows | Saved baseline | Prior ms [range] | Current ms [range] | Prior/current heap MiB | Prior/current RSS MiB | +| --- | ---: | --- | --- | --- | --- | --- | +| rxdb | 1,000 | empty | 37.6 [35.7–41.9] | 30.3 [29.5–35.6] | 50.3 / 47.7 | 161.2 / 159.3 | +| rxdb | 1,000 | matching | 41.4 [39.9–42.7] | 44.6 [42.1–60.8] | 57.1 / 53.3 | 172.7 / 170.2 | +| rxdb | 10,000 | empty | 318.8 [317.8–323.4] | 284.6 [264.3–292.3] | 101.5 / 126.8 | 286.4 / 278.4 | +| rxdb | 10,000 | matching | 423.2 [420.9–491.4] | 428.3 [410.9–453.1] | 144.9 / 127.2 | 335.6 / 343.0 | +| powersync | 1,000 | empty | 40.3 [36.2–48.2] | 28.9 [27.5–33.5] | 47.1 / 44.5 | 268.9 / 265.2 | +| powersync | 1,000 | matching | 38.5 [37.2–45.5] | 38.6 [36.9–41.0] | 53.8 / 50.1 | 273.0 / 270.4 | +| powersync | 10,000 | empty | 182.5 [164.1–184.6] | 198.4 [180.2–198.9] | 72.3 / 89.0 | 321.7 / 347.3 | +| powersync | 10,000 | matching | 274.7 [269.6–283.0] | 320.1 [319.3–327.0] | 118.0 / 109.5 | 413.8 / 407.3 | +| rxdb | 50,000 | matching | 1757.8 [1667.8–1784.5] | 1714.3 [1606.0–1769.7] | 330.9 / 375.1 | 682.1 / 713.0 | +| powersync | 50,000 | matching | 1122.3 [853.0–1174.7] | 1148.1 [1001.3–1362.0] | 214.1 / 285.7 | 672.7 / 716.2 | + +### Controlled ordering and network-first fallback + +All cases have 10,000 matching saved rows. Values are medians in milliseconds. +The source-first case adds a 300 ms restore-read delay to both variants. +The fallback case holds the source for 350 ms and uses a 25 ms network deadline. + +| Adapter | Schedule | Prior/current preload ready ms | Prior/current source applied ms | Prior/current snapshots | +| --- | --- | --- | --- | --- | +| rxdb | source-first | 592.5 / 597.8 | 592.4 / 597.5 | 0 / 1 | +| rxdb | fallback | 55.9 / 56.2 | 626.2 / 615.1 | 0 / 1 | +| powersync | source-first | 529.7 / 543.7 | 529.5 / 543.6 | 0 / 1 | +| powersync | fallback | 43.3 / 46.4 | 542.1 / 541.4 | 1 / 1 | + +The final three campaigns contain **112 process observations: 84 measured and +28 warmups across 14 cells**. The accompanying [raw CSV](../../../scripts/bench/persisted-startup-2026-11-10.csv) +also retains 64 observations from the initial warm-adapter diagnostic. Warmups +and diagnostic observations do not enter the final medians above. Every worker +checks its final Collection and SQLite row counts. + +### Inference + +The snapshot change has measurable costs, with different effects by workload. +At 10,000 matching saved rows, PowerSync source-applied latency rises from +274.7 ms to 320.1 ms (+16.5%, +45.4 ms). All three current samples +(319.3–327.0 ms) exceed all three prior samples (269.6–283.0 ms). The initial +warm-adapter diagnostic also shows a slower median for this cell (364.0 ms to +448.1 ms), but it is a different experiment and is not pooled with the reopen +measurements. The 50,000-row PowerSync timing ranges overlap broadly. + +At 50,000 matching saved rows, sampled heap maxima increase from 330.9 to +375.1 MiB for RxDB (+44.2 MiB, +13.4%), and from 214.1 to 285.7 MiB for +PowerSync (+71.7 MiB, +33.5%). Sampled RSS maxima increase by 30.9 and +43.6 MiB, respectively. Those are sampled maxima, not exact allocation counts +or a promise about every device. Smaller cells have mixed memory changes. + +The network-first measurements preserve early persisted readiness for the +measured scenario: current medians are 56.2 ms for RxDB and 46.4 ms for +PowerSync, while their source is held for 350 ms. PowerSync selects snapshots +in both variants in that case, giving a same-selection control. These numbers +do not establish a browser first-paint result. + +Both variants send N source rows to durable storage. When prior selection is +not already a snapshot, current startup adds a truncate, and PowerSync changes +from batch commits to one snapshot commit. +At 50,000 rows, PowerSync prior selection invokes 51 source commits (50 with +rows plus an empty final batch); current invokes one snapshot. RxDB uses one +full-load commit in both variants. The wrapper's row-read budget remains a +separate correctness/work-oracle obligation; this benchmark does not count it. + +The results do not support a blanket performance-preserved claim. A repair +should preserve authoritative replacement and atomic snapshot publication. +Changing user-visible publication to progressive batches would require a +separate contract decision. A possible narrower experiment is to preserve +the snapshot while reducing redundant durable row writes against the existing +hydrated baseline; that is not implemented or measured here. + +### Environment and validation + +Apple M5 Max, 18 logical CPUs, 64 GiB RAM, macOS arm64, Node v24.19.0. +RxDB 17.6.0, PowerSync Node 0.21.0, and PowerSync common 2.5.0 supply the +local source SDKs. Both variants use the same installed dependency tree. +The source and persistence databases live under the local temporary directory. +No test/build/mutation workers run during the final timed pairs. + +The source-control matcher, minified bundle build, all workers, final row-count +checks, and the normal ESLint pass validate the executable harness. The worker +also passes TypeScript with the repository's strict compiler settings and +package-scoped SDK resolution. The benchmark does not enforce a new numeric +performance threshold. + +## Reproduction + +From the worktree root: + +```sh +node scripts/bench/persisted-startup.mjs --rows=1000,10000 --adapters=rxdb,powersync --schedules=natural --repetitions=3 --warmup=1 --out=.review-2367/startup-perf/final-natural.json +node scripts/bench/persisted-startup.mjs --rows=10000 --saved=true --adapters=rxdb,powersync --schedules=source-first,fallback --repetitions=3 --warmup=1 --out=.review-2367/startup-perf/final-controls.json +node scripts/bench/persisted-startup.mjs --rows=50000 --saved=true --adapters=rxdb,powersync --schedules=natural --repetitions=3 --warmup=1 --out=.review-2367/startup-perf/final-large.json +``` + +The baseline transform fails if it cannot match both snapshot selections. +Changes to those expressions require updating that explicit source control. +Raw JSON and logs remain in the task-local directory. The accompanying CSV +retains measured observations, warmups, and the earlier warm-adapter diagnostic +run, with their experiment labels. + +The initial diagnostic reused its seeding adapter and connection. It measured +warm Collection startup rather than an app reopen. It is retained separately +and does not supply the final app-reopen comparison. Smoke and one-repetition +pilots also remain local; they are harness development, not independent samples. + +## Limits + +This measures a Collection's local eager startup on one desktop with warm OS +file caches. It does not measure process boot, UI paint, browser OPFS, cold +device storage, remote replication, competing tab writers, failure recovery, +or arbitrary row shapes. Three measured repetitions reveal spread, but do not +estimate tail percentiles or prove a universal noise bound. The controlled +selection baseline can retain omitted saved rows; it is a cost comparator, +not a proposed correctness repair. + +Both variants use the same local dependency installation. The task's temporary +HTTP-403 recovery overrides remain uncommitted: query-core 5.107.1 → 5.106.0, +fake-indexeddb 6.2.8 → 6.2.7, and tailwindcss/postcss 4.3.5 → 4.3.4. Those +packages do not define the compared SDK or SQLite implementation. The exact +unrecovered lockfile still lacks a local measurement. diff --git a/docs/contributing/oracle-reviews/2026-11-10-retained-scoped-hydration-gaps.md b/docs/contributing/oracle-reviews/2026-11-10-retained-scoped-hydration-gaps.md new file mode 100644 index 0000000000..99f5b3953f --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-retained-scoped-hydration-gaps.md @@ -0,0 +1,191 @@ +# Persisted on-demand scope and cached-claim gaps + +Production source: `5feb26616a8357853454c2f9ff2ce0b0adb9c908`. +`packages/query-db-collection/src/query.ts` SHA256: +`6bbf1542e6daa38f33315445c5bb0ae3fbbf90d907cd388cb5d13f41c3b37cc5`. +This amendment adds receiving coverage. It changes no production code or +accepted product law. The [current CLI audit](2026-11-10-startup-gap-current-head.md) +preserves the initial survivor classifications separately from these additive +experiments. + +## Laws and independent judgments + +The SQLite persistence guide's **Persist a collection** section promises that +an on-demand Collection restores active demand rather than every saved row. +Query's **QueryFn and Predicate Push-Down** and **Direct Writes and Query Sync** +sections make complete-result omission local to the matching scope. A limited +response cannot prove an ownerless direct row absent. Those laws distinguish +persisted eager replacement from on-demand restoration. + +The shared model in `retained-scoped-hydration-oracle.ts` uses set selection +over two disjoint authored scopes. Its first judgment selects the demanded +saved rows, replaces only the scope certified by a complete unbounded result, +and preserves the other durable scope until demand acquires it. A limited +result overlays its returned rows without retiring a broader retained owner +or proving an ownerless row absent. It imports no production owner classifier, +hydration helper, queue, or persistence scan implementation. + +The same file separately judges cached first claims. Query's documented +per-key fetch order gives a continuously owned row the value from its later +physical fetch. A manually edited cache establishes no physical fetch order; +an older fetched cache establishes an older one. Neither may replace that +row merely because another Query first claims it. A later physical refetch +can replace it. The independent expected values come directly from these +three authored values and their causal order, never production's clocks or +observed rows. The primary ownership oracle remains the owner of this law; +these are its persisted receiving histories. + +## Grammar, paths and observations + +Each controlled and real Node SQLite host runs: + +- 48 scope histories, then the same property in fixed-seed 1481 and fresh + campaigns of 60 histories each; +- two cached-claim histories, then fixed-seed 1376 and fresh campaigns of + 60 histories each; +- one wrong-observation calibration for each judgment. + +The scope grammar crosses first scope, all/one-row window, both saved and +returned masks from 0 through 3, ownerless-row scope, and values from -10 through +10. The fixed histories reconstruct unrequested saved rows, complete omission, +and ownerless rows restored into a limited demand. Removing the limited-window +axis loses the witness for `M558/M559`; removing the second durable scope loses +the scope-leak witness for `M1481`. Empty and nonempty masks and negative/equal +values are legal marginal cases. The driver rejects a demand without the +authored equality predicate instead of silently assigning it a scope. + +The cached-claim grammar crosses a manual cache entry or an earlier physical +fetch, three independent values from -10 through 10, and a later explicit +refetch. Removing cached provenance leaves only ordinary fetched results, +which cannot distinguish `M1376/M1377`. Equal values remain legal; declared +unequal-value reach prevents an all-no-op campaign. Reversing the order so +that the cached fetch starts later would authorize its value and is outside +this particular cached-first-claim grammar. + +The real QueryClient, Query Collection and persistence wrapper receive both +properties. Scope histories seed two retained scopes through a real earlier +Collection, clean it up, and reopen with a fresh QueryClient. They acquire one +demand while its real response is held, then release that response and acquire +the other demand. The controlled adapter supplies only predicate/limit +evaluation on its stored facts; Node SQLite evaluates the real demand. + +Scope comparisons inspect public rows while the first response is pending, +public rows inside each demand's fulfillment callback, and durable rows after +each fulfillment. Cached-claim comparisons inspect the first peer's value, +public/durable values at cached acquisition, and public/durable values after +explicit refetch. Request counts prove cached acquisition did not start a +physical fetch. Optional hydration/scan counters are diagnostics: the comparison +does not invent a prohibition on redundant optional capability calls. + +The browser receiving page uses these same models, histories, drivers and +comparisons with real Chromium worker SQLite/OPFS. It receives 48 scope and +two cached-claim histories, requires each declared state, and removes database +artifacts after close. The final parent integration run passes all 226 histories +in 50.6 seconds, with every receiver's declared reach positive, no page errors +and no leftover fixture artifacts. The two cached histories supersede the +earlier 224-history registration. + +## Declared reach and clean results + +The final local Node run passes all 16 registrations. Every state is required +separately for each host and campaign. Exact counts from that run follow; +`C` means controlled storage and `N` means real Node SQLite. + +| Scope state | Fixed histories, C/N | Fixed campaign, C/N | Fresh C | Fresh N | +| --- | ---: | ---: | ---: | ---: | +| first-a | 24 | 25 | 22 | 27 | +| first-b | 24 | 35 | 38 | 33 | +| empty-result | 24 | 17 | 11 | 21 | +| nonempty-result | 24 | 43 | 49 | 39 | +| unrequested-saved-rows | 48 | 41 | 41 | 49 | +| omitted-owned-row | 24 | 25 | 27 | 27 | +| ownerless-first | 16 | 22 | 24 | 16 | +| ownerless-other | 16 | 25 | 23 | 15 | +| no-ownerless | 16 | 13 | 13 | 29 | +| unbounded | 24 | 31 | 29 | 29 | +| limited | 24 | 29 | 31 | 31 | +| limited-ownerless-restored | 8 | 12 | 16 | 8 | + +| Cached-claim state | Fixed histories, C/N | Fixed campaign, C/N | Fresh C | Fresh N | +| --- | ---: | ---: | ---: | ---: | +| manual | 1 | 41 | 31 | 32 | +| older-fetch | 1 | 19 | 29 | 28 | +| different-cached-value | 2 | 54 | 57 | 59 | +| later-value-change | 2 | 58 | 56 | 58 | + +Query and browser package TypeScript checks pass. Scoped ESLint, Prettier and +the parent's final `lint:changed` checks pass. Query's `test:oracles` command includes the new runner. Both +property IDs have guarded replay registration and distinct random-campaign +selectors. + +## Exact hostile mutants and checked replay + +Each experiment transforms an immutable source copy in a separate Vitest +process. The transformer checks the original operator and position, logs the +applied SHA256, disables filesystem module caching, and gives each mutant a +distinct plugin/cache identity. It never overwrites the shared source file. + +| Exact current mutant | Intended wrong behavior | Rejected observation | +| --- | --- | --- | +| `M558`, `M559` | Broaden ownerless-row inclusion outside eager persisted reconciliation. | A limited empty result deletes a restored ownerless direct row. Both first public and durable cuts reject it; the loss remains at the second demand. | +| `M1481` | Always call unconstrained baseline hydration before scoped reconciliation. | First demand publishes saved rows from the unrequested other scope. The first public cut rejects it while those rows correctly remain durable. | +| `M1376`, `M1377` | Broaden eager first-claim update authority to cached on-demand claims. | Acquiring a manually edited cached Query replaces a continuously owned later-fetched value. Both claimed public and durable cuts reject it. | + +All five exact mutants fail the intended comparison on both Node hosts. +These are assertion rejections, not setup failures, timeouts or inferred +kills. The following shrink coordinates replay those same violations: + +| Property | Mutants | Seed | Shrink path | Reduced distinction | +| --- | --- | ---: | --- | --- | +| `query.retained-scoped-hydration` | `M558/M559` | 1481 | `0:0:1:1:1:1:2` | One-row demand; only saved row is ownerless in the first scope; both source results empty. | +| `query.retained-scoped-hydration` | `M1481` | 1481 | `1:0:0:0:1:1` | First scope empty; only saved row belongs to the other scope. | +| `query.cached-peer-claim` | `M1376/M1377` | 1376 | `1:0:1:0:1` | Manual cached value 0, continuously owned fetched value -1; cached acquisition must preserve -1. | + +The guarded replay records two failed executions for each mutant, one per host, +at the intended comparison. Current production passes every coordinate, with +two successful executions per replay. A later refetch returns the expected +new value even under the cached-claim mutants, so comparing only after that +refetch would hide the defect. + +From `packages/query-db-collection`, the clean cached-claim replay is: + +```sh +TANSTACK_DB_ORACLE_SEED=1376 TANSTACK_DB_ORACLE_PATH=1:0:1:0:1 TANSTACK_DB_ORACLE_PROPERTY=query.cached-peer-claim node --import tsx ../db/tests/oracle-replay.ts tests/retained-scoped-hydration-oracle.test.ts --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false +``` + +From the worktree root, normal coverage runs with: + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/query-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false tests/retained-scoped-hydration-oracle.test.ts +``` + +Exact transforms, applied-source hashes, original/shrunk mismatches, replay +witnesses and validation logs remain under `.review-2367/gap-fill/`, including +`query-scoped-M*-replay.log` and `query-scoped-final-green.log`. + +## Review and limits + +This amendment enforces accepted laws with two small stateless judgments. +ORC-001/002/003 identify authority, independent models and literate roles. +ORC-004 grammar controls and ORC-005 public checkpoints are described above. +ORC-006 uses wrong-observation calibrations and five exact hostile mutants; +ORC-007 has parity campaigns and checked replay. ORC-008's stateful-model +trigger does not apply. ORC-009 uses authored scopes, demands and checkpoints +with their stated mapping. ORC-010 uses error-preserving cleanup and verifies +the same violation at replay. No concrete shared semantic-fault hypothesis +requiring an ORC-011 second formulation was identified; this is not a claim +that no such hypothesis exists. This record supplies ORC-012 evidence. +ORC-013 distinguishes all/limited demand, demanded/other scope, cached claim +and later physical fetch. ORC-014 reuses the same judgments for real storage; +ORC-015 requires the counts above. ORC-016 fixed histories and calibrations +use the same reusable judgments without separate expected answers. + +The finite domain is one cold reopen with two disjoint scopes or one +continuously owned overlapping key, successful I/O, and numeric values. +Overlapping arbitrary predicates, cache rotation, peer writers, optimistic +transactions, failed storage and arbitrary intermediate publications remain +outside these added histories. The existing ownership, scoped recovery, +startup and direct-write-window owners retain their separate laws. This +amendment does not convert capability-call counts into a work or latency +promise. The [startup measurements](2026-11-10-persisted-startup-measurements.md) +judge resource cost separately. diff --git a/docs/contributing/oracle-reviews/2026-11-10-startup-gap-current-head.md b/docs/contributing/oracle-reviews/2026-11-10-startup-gap-current-head.md new file mode 100644 index 0000000000..098d3cbb6e --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-startup-gap-current-head.md @@ -0,0 +1,258 @@ +# Persisted-startup current-head gap audit + +This audit starts at `5feb26616a8357853454c2f9ff2ce0b0adb9c908` and +preserves the earlier [gap audit](2026-11-10-startup-gap-cli.md). Its author +also contributes to this PR, so this is a sensitivity audit, not independent +review. All 61 changed-line operators have final classifications. All five +original-defect semantic controls are rejected; later receiver extensions are +recorded separately below. + +## Selection + +The installed CLI's `schemata.mjs` selects mutations on lines changed from +`origin/main` in the four production entry points. The current selection is +41 Query, six RxDB, six PowerSync and eight TrailBase operators (61 total). +This is a changed-line audit, not a whole-file audit. Selection JSON and source +SHA256 records are retained under `.review-2367/gap-current/`. + +The five previously unclassified Query operators moved from `M1451–M1455` +to `M1466–M1470` after earlier repairs added 15 preceding operators. + +## Installed CLI failure + +The unmodified, installed CLI still aborts on the five-operator selection: + +```sh +pnpm --config.verifyDepsBeforeRun=false gap-report \ + --package packages/query-db-collection --source src/query.ts \ + --diff-base origin/main --seed 2367 --contexts 1 \ + --only query.ts:M1466,query.ts:M1467,query.ts:M1468,query.ts:M1469,query.ts:M1470 \ + --out .review-2367/gap-current/original-five +``` + +Both unmutated baselines pass: 840 tests in 131.3 seconds and 132.6 seconds, +with zero nondeterministic tests. The run then exits with status 1: + +```text +Error: query.ts:M1468: tests/ssr-dehydration-oracle.test.ts +has no unit and no earlier kill +at runOwners (.../oracle-gaps/check.mjs:1550:19) +``` + +The retained log is +`.review-2367/gap-current/original-five.log`; `state.json` identifies the +last eight-copy batch for `M1468` and `M1469`. The installed CLI deletes the +raw batch capture on exit. This aborted run classifies none of the five +operators. + +The preserved diagnostic proves the cause. In +`refetch-ordered-prefix-oracle.test.ts`, the existing offset-only preload +witness is registered with `it.fails` (line 1853). During its +`onTestFinished` hook, Vitest's task still has `state: "fail"`, its assertion +verdict and route `case`. `capture-setup.mjs` therefore marks the mutant killed +and skips later test-file copies. Vitest then applies expected-failure +semantics: the final `TestCase.result()` is `{state: "passed"}`. The CLI's +`outcomeOf` correctly sees no failed test and no kill. `unitsBatch` discards the +later skipped copies; `runOwners` then raises the missing-unit exception. + +The same diagnostic run reproduces the installed error after two clean +840-test baselines (131.3 and 136.0 seconds, zero nondeterministic tests). +The capture for `M1468` contains the failing expected-failure assertion while +its final Vitest record says passed; both later SSR copies are marked skipped. +The reduced record is +`.review-2367/gap-current/expected-failure-accounting-bug.json`; the complete +capture and module results remain under `diagnostic-five-old-owner-set/`. +This diagnostic excludes only the newly added precedence registration to retain +the owner set that originally triggered the exception. No source or test file +was swapped. + +The CLI repair belongs in early-stop accounting for expected-failure tests. +The isolated audit runner adds `!context.task.fails` to the `finishTest` +early-stop condition. Normal early stopping, final-result classification, +assertions, routes and source instrumentation remain unchanged. The patch is +retained as `.review-2367/gap-current/expected-failure-guard.patch`. +The installed dependency remains byte-for-byte unchanged. +The installed runner already passes `fsModuleCache: false`, invalidates changed +served-source hashes and resets Vitest modules between unskipped copies. The +isolated guard preserves those mechanisms. Standalone Vite mutant experiments +need their own cache controls; they do not inherit the CLI's isolation. + +The earlier isolated no-cross-file-skip experiment is not used for final +results: removing only the cross-file optimization would still let an +expected failure skip the remaining tests in its own file. All isolated CLI +copies now live outside the checkout under +`/private/tmp/tanstack-db-2394-gap-cli-current/`, so dependency fixture sources +do not enter changed-file linting. + +## Operator results + +| Receiver | Operators | Law rejections | Survived | Timeout | Unclassified | +| --- | ---: | ---: | ---: | ---: | ---: | +| Query | 41 | 30 | 9 | 2 | 0 | +| RxDB | 6 | 6 | 0 | 0 | 0 | +| PowerSync | 6 | 6 | 0 | 0 | 0 | +| TrailBase | 8 | 7 | 1 | 0 | 0 | +| Total | 61 | 49 | 10 | 2 | 0 | + +Ordinary tests are not credited as kills. A kill is a reproducible oracle +assertion verdict rejected by the final Vitest result. Timeouts are separate +finite-budget outcomes; they are not behavioral oracle kills. The table +classifies every selected operator, not every possible source mutation. + +The complete operator account is: + +- Query killed: `M551–M552`, `M556–M557`, `M560–M562`, `M564`, `M1374`, + `M1378–M1380`, `M1466–M1467`, `M1470–M1472`, `M1477–M1480`, + `M1482–M1485`, `M1748`, `M1790–M1792`, `M1795`. +- Query survived: `M558`, `M559`, `M563`, `M1376`, `M1377`, `M1473`, + `M1481`, `M1793`, `M1794`. +- Query timeout: `M1468`, `M1469`. +- RxDB killed: `M3–M4`, `M7–M10`. +- PowerSync killed: `M35–M36`, `M162–M165`. +- TrailBase killed: `M36–M37`, `M146–M150`; survived: `M151`. + +Thus the five previously unclassified Query operators now have finite +outcomes: historical `M1451/M1452/M1455` (current `M1466/M1467/M1470`) are +killed; historical `M1453/M1454` (current `M1468/M1469`) time out. None is +still hidden behind the CLI accounting exception. + +The installed-CLI reports are `ba8aca53b708` (RxDB), `8d4a3c3b3330` +(PowerSync), and `8ca1218ab1ed` (TrailBase). Each reports zero +nondeterministic tests, setup/runtime classifications, and timeouts. Their +unmutated test counts and times are 135 in 27.0/26.3 seconds, 135 in +153.2/158.0 seconds, and 217 in 22.5/22.2 seconds, respectively. +Query's guarded-run report is `b56831e3d650`. Its baselines pass 848 tests +in 134.2/136.9 seconds with zero nondeterministic tests. + +RxDB/PowerSync's historical retained-row reload survivors are now `M10` and +`M165`. The new eager-reload receiver rejects each on a retained nonempty +load followed by an empty complete result. The capability operands are judged +by package-local receiving registrations: inverted metadata guards fail the +absent-metadata law; omitted validation fails missing-capability validation. +The shared model remains shared. See [adapter reload evidence](2026-11-10-adapter-reload-gaps.md). + +## Surviving distinctions and additive coverage + +A survivor records a distinction the finite campaign did not judge. It is +not a production defect or an approved global equivalence. + +- TrailBase `M151` removes cancellation after held baseline hydration. The + lifecycle receiver now reaches obsolete work. Exact immutable source-copy + experiments preserve current public/durable rows and resource ownership + across controlled and real Node SQLite hosts, including replacement-ready + histories with a key outside the old result. Existing wrapper/core epochs + fence its publication. This is scoped equivalence in the exercised domain; + see [TrailBase hydration evidence](2026-11-10-trailbase-hydration-gap.md). +- Query `M558/M559` broaden inclusion of ownerless accepted rows beyond eager + persisted reconciliation. The new retained-scope receiver rejects both: + a limited response cannot remove an omitted ownerless saved row. + `M1376/M1377` broaden first-claim updates for cached results. The new cached + peer receiver rejects both when an older or manually populated cache would + override a continuously owned row established by a later physical fetch. +- Query `M1481` always takes hydrated-baseline reconciliation. The new + on-demand receiver rejects it when acquiring one scope exposes unrequested + saved rows from another scope. Public rows supply the decisive comparison, + rather than an invented restriction on optional capability calls. +- Query `M1473` forces eager persisted reconciliation after ownership already + exists. The eager hydration capability caches its resume-baseline promise; + repeated calls do not establish repeated physical row reads. Later-result + ordering and public/durable observations judge any stronger distinction. +- Query `M563` omits the same-run direct-row exclusion. The initial grammar + starts the first physical fetch before every direct write, so its older-fetch + merge independently preserves the direct row. A before-first-fetch ordering + is a distinct legal neighbor. +- Query `M1793/M1794` alter the recorded fetch-start generation fallback. + Physical-fetch histories can supply redundant ordering evidence. Cached + successful results without a physical-fetch clock are a distinct neighbor. + +The Query full audit started with the 56-history precedence receiver. During +its mutation phase, the parent added legal before-first-fetch histories and +cached-result histories, plus a separate on-demand receiving extension. These +are additive coverage, not an unchanged frozen baseline. Source hashes remain +unchanged. The initial baseline/test manifest and final input manifest are +retained separately; exact survivor runs after clean new baselines must state +which extension they judge. The table above preserves the initial full audit's +reported classifications rather than replacing survivors with inferred kills. + +## Additive survivor experiments + +After the main baseline, the precedence owner extended the eager grammar to +104 fixed histories and generated campaigns of 80 fixed-seed plus 80 fresh +histories on each controlled/Node SQLite host. Separate immutable source +transforms log distinct applied-source hashes, disable Vitest's disk module +cache, and give each operator a separate plugin/cache identity. Earlier +standalone GREEN runs without those controls were retracted and are not +closure evidence here. See the current +[precedence gap report](2026-11-10-persisted-precedence-gaps.md) for commands, +reach, replay coordinates and exact public/durable checkpoints. + +- `M1793` is now rejected: a cached successful result has no physical + fetch clock; a newer update accepted during its held baseline must survive + first-result application and persistence. The mutant loses it on both hosts. +- `M1748` remains rejected by the legal pre-restore insert/derived-result + ordering. The expanded receiver observes obsolete rows at both first public + and durable cuts when that reconciliation is omitted. +- Exact `M563` still passes after positively reaching same-run ownerless + insertion before the first physical fetch, then a complete omission. + Direct-row lifetime/absence machinery supplies redundant protection at the + exercised eager public/durable cuts. This does not classify on-demand paths. +- Exact `M1794` still passes. At the admitted general-result branch, the + original threshold and enqueue-time direct generation identify the same set + of later same-Collection key claims; writes pending afterward remain newer + under either threshold. This is a bounded redundancy argument with the + exercised cached/fetched, raw/derived and write-order histories. +- Exact `M1473` still passes the 250-check precedence/startup selection, + including the actual one-full-row-read budget. The extra capability call + does not establish an extra physical scan. + +The on-demand scope/provenance receiver rejects `M558/M559`, `M1376/M1377` +and `M1481` on both controlled and real Node SQLite hosts. Each has a checked +mutant replay that fails at its public/durable comparison and a clean replay +that passes at the same coordinates. The scope grammar runs 48 fixed histories +and fixed/fresh campaigns of 60 per host. The cached-peer grammar runs two +fixed histories and fixed/fresh campaigns of 60 per host. Chromium receives +those same 50 histories and shared judgments. +The [scope/provenance report](2026-11-10-retained-scoped-hydration-gaps.md) +retains exact transforms, reach counts, and replay coordinates. + +These six additive rejections bring the selected operators' observed outcomes +to 55 assertion rejections, four bounded survivors (`M563`, `M1473`, `M1794`, +and TrailBase `M151`), and two finite-budget timeouts. The original audit table +and its frozen test manifest remain above. This combines executed evidence +from the separate campaigns, rather than claiming one final full-CLI rerun. + +## Semantic controls and limits + +TrailBase's original-defect control `sweep-before-baseline` is rejected by the +shared persisted-startup comparison. RxDB's current-text +`pending-baseline-is-not-snapshot` control is also rejected (report +`6e74c7931eb4`, 135-test baselines in 26.4/25.8 seconds, zero nondeterminism). +RxDB and PowerSync's older authored find text no longer applies after the +persistence operand was assigned to a validated local capability. Those +`does-not-apply` results are preserved and excluded from scores; the current +control changes only its find text, retaining the same wrong behavior. + +Both Query semantic controls are rejected by the shared persisted-startup +comparison on fetched-response-before-restore histories (previous mask 1, +next mask 2). Its behavior-preserving control, redundant parentheses around +the hydration condition, survives. These known-kill and expected-survivor +controls calibrate the isolated early-stop guard without changing the final +classifier. PowerSync's adapted semantic control is rejected by that same +history (report `eeaa2f8c06a9`; 135-test baselines in 173.3/160.6 seconds, +zero nondeterminism). All five original-defect controls are therefore rejected. +The behavior-preserving control is excluded from mutation scores. + +The starting and final source hashes match for all four production files and +both installed CLI files. `.review-2367/gap-current/final-input-manifest.json` +records those hashes, the isolated guarded runner's hashes and the final test +inputs. The starting Query test manifest remains separate because additive +receiver work happened after its clean baseline. The exact commands are +retained as `*-command.json` beside completed `results.json` reports. No CLI +worker remains active. + +The evidence is finite: changed lines in four entry points, declared package +owners, seed 2367, configured histories and public/durable checkpoints. It is +not a whole-file audit, a universal proof, or a claim that untested integration +paths are equivalent. No production change follows from the operator audit. + +Validation for this audit record: `pnpm lint:changed` and the Markdown formatter pass. diff --git a/docs/contributing/oracle-reviews/2026-11-10-trailbase-hydration-gap.md b/docs/contributing/oracle-reviews/2026-11-10-trailbase-hydration-gap.md new file mode 100644 index 0000000000..c9b0dbf2cf --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-trailbase-hydration-gap.md @@ -0,0 +1,204 @@ +# TrailBase persisted hydration lifecycle gap + +Starting head: `5feb26616a8357853454c2f9ff2ce0b0adb9c908`. +This is the author's executable gap investigation, not an independent review. +It follows the earlier diff-focused audit's surviving M150 cancellation guard. + +## Law and missing observation + +Cleanup retires a sync run. A list or persisted-baseline completion from that +run cannot change a cleaned-up Collection or a replacement run. The replacement +must retain its own reader and continue accepting events. Accepted durable rows +survive cleanup; the new complete eager result replaces them. Authority: +TrailBase's existing lifecycle law in `tests/ORACLE.md`, the glossary's cleanup +and sync-run contracts, and the shared persisted-startup complete-result law. +The SQLite guide's "Render after SQLite restore" section separately governs +readiness. Restored rows alone do not imply adapter readiness. + +The previous persisted-startup grammar never cleaned up during the added +`hydrateBaseline()` await. The lifecycle grammar tested late subscribe/list +results without this persisted composition. Thus neither comparison established +the guard's exact public/durable boundary. A surviving mutant was a real gap in +the evidence, but did not itself establish a production defect. + +## Primary-oracle extension + +`lifecycle-oracle.property.test.ts` remains the primary lifecycle runner. Its new +directly named companion `lifecycle-hydration-oracle.ts` supplies a small row-set +projection of that law. `lifecycle-hydration-driver.ts` runs real TrailBase sync +inside `persistedCollectionOptions` on each receiving host. It calls actual +baseline hydration before holding its completion. This permits the new run to +use the database while the old continuation is pending, instead of trapping +cleanup behind an unrelated SQLite writer lock. + +Three key-subset inputs vary saved, old-listed, and replacement rows. Old work +either completes its list and waits at hydration, or rejects its pending list +after cleanup. The latter is an adjacent earlier boundary and is not counted as +hydration reach. Resume cuts are cleaned-up, replacement loading, or replacement +ready. Default rendering and network-first rendering before its deadline both +remain loading until the source marks ready. Every history finally delivers a +new replacement event. + +The shared judgment compares public rows, durable rows, and status after old +work resumes, after replacement readiness, and after the later event. It also +checks cleanup's old `AbortError`, exactly one old source cancellation, reader +release/continued ownership, final replacement release, and error reports +through final cleanup. Obsolete old `begin` attempts are counted for mutant +reach, but the law does not promise zero internal work after cancellation. + +## Grammar controls + +The original M150 witness reconstructs as saved mask 3, completed old mask 1, +replacement mask 6, and old hydration resumed after replacement readiness. +The fixed corpus reconstructs this in both render configurations and also +resumes at cleaned-up and replacement-loading cuts. Adjacent histories reject +the old list, or make the replacement empty. A rejected list carries old mask +0 because it delivers no row payload; varying an ignored payload would only +duplicate the same history. + +Saved rows distinguish durable retention across cleanup. Old listed rows +distinguish accepted pre-hydration writes from the pending omission sweep. +Replacement rows distinguish obsolete insertion, overwrite, and deletion. +The three resume cuts separate authority after cleanup, during a new load, and +after its complete result. List completion versus rejection separates the +hydration continuation from its adjacent error path. The two render options +exercise the same cleanup law through both documented startup configurations; +the long network deadline deliberately leaves the fallback transition to its +existing startup owner. + +All eight subsets of three string keys are legal, including empty, identical, +disjoint, and partially overlapping sets. The later key 3 distinguishes a live +replacement reader from a correct result followed by a dead listener. Negative +or out-of-range row masks and resume-before-cleanup histories are outside the +grammar. A rejected list cannot enter the hydration-held state; reach labels +for listed-row overlap/omission also require actual list completion. + +## Executed evidence + +The initial receiving setup used numeric keys with a controlled host whose +fixture accepts only string keys. That was a fixture failure, not product RED. +An initial readiness assumption also disagreed with the documented SQLite +contract. The model now follows that section: ordinary restore leaves status +loading, and the unfinished old preload rejects when cleanup retires it. +Neither correction changed production or relaxed a prior comparison. + +The first corrected unmutated run passes 54 selected checks: 48 fixed histories, +four generated registrations of 50 histories each, and two checker calibrations. +The 99 unselected neighboring tests are not credited by this filtered run. +All nine states are reached separately in every generated host/campaign. + +The final unchanged-production command also ran every neighboring registration: + +```sh +pnpm --config.verifyDepsBeforeRun=false exec vitest run \ + --config .review-2367/shared-startup.config.ts --maxWorkers=1 \ + packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts +``` + +Result: **153 passed**, including the 99 neighbors excluded from the first +selected run. The new campaigns each execute 50 histories. Reach from that +full run is recorded below; every declared minimum is one per campaign. + +| Declared state | Controlled fixed | Controlled fresh | Node SQLite fixed | Node SQLite fresh | +| --- | ---: | ---: | ---: | ---: | +| hydration-held-cleanup | 24 | 22 | 24 | 22 | +| late-list-rejection | 26 | 28 | 26 | 28 | +| resume-cleaned-up | 20 | 21 | 20 | 19 | +| resume-replacement-loading | 13 | 16 | 13 | 12 | +| resume-replacement-ready | 17 | 13 | 17 | 19 | +| empty-replacement | 5 | 7 | 5 | 9 | +| nonempty-replacement | 45 | 43 | 45 | 41 | +| new-row-outside-old-list | 18 | 11 | 18 | 12 | +| old-row-omitted | 11 | 17 | 11 | 10 | + +The package's `tsc --noEmit` and scoped ESLint commands pass. No campaign run +budget was lowered. Production remains unchanged. + +## M150 experiment and calibration + +An isolated copy of `trailbase.ts` removes only the cancellation return after +`await persistence.hydrateBaseline()`. The unchanged source file was never +overwritten. The first attempt failed during scratch-module resolution of +`@tanstack/store`, before any test ran; it is setup failure, not a mutant kill. +After resolving that copy's import through the package's installed dependency, +the same 24 fixed histories per Node host all pass: **48 comparisons GREEN**. + +The replacement-ready witness uses saved mask 3, old mask 1, replacement mask 6. +Both hosts and both render configurations record exactly one obsolete `begin` +attempt. Thus this is reached continuation work, not an unreached mutation. +Public and durable rows remain the replacement rows with value 3. The later +key 3 with value 4 arrives, the old reader stays released, the replacement +reader remains usable and releases at final cleanup, and no error is reported. + +The outcome is **equivalence within this tested persisted lifecycle domain**. +The wrapper's `sourceParamsFor` retires old `begin`, `write`, `commit`, status, +and metadata authority after cleanup. Core's sync-callback epoch independently +fences retired callbacks. Removing the adapter's local return permits redundant +internal attempts but grants no authority to publish or persist them. A +zero-internal-work promise would be a new law, so it was not invented to kill +this mutant. The cancellation return remains useful and remains in production. +The updated gap CLI calls this same post-hydration edit M151 and independently +reports `survived` / `no-observed-difference`. + +The permanent wrong-observation controls erase replacement rows from the +`retiredResume` checkpoint and require the common comparison to reject them. +A scratch driver transformation applies that same row loss to the generated +property. It fails at the intended public-row comparison and shrinks to: + +```json +{"previous":0,"replacement":1,"initialRender":"default","resumeAt":"replacement-ready","oldList":"complete","old":0} +``` + +Captured campaign seed: `-703652111`; shrink path: `0:0:1:0:2:2`. +The guarded replay reproduces that exact `retiredResume` mismatch, with one +execution witness reporting `failed: true`. Removing the scratch row loss and +replaying the same coordinates passes through each Node host, each with one +execution witness reporting `failed: false`: + +```sh +TANSTACK_DB_ORACLE_SEED=-703652111 \ +TANSTACK_DB_ORACLE_PATH=0:0:1:0:2:2 \ +TANSTACK_DB_ORACLE_PROPERTY=trailbase.lifecycle.persisted-hydration.controlled \ +pnpm --config.verifyDepsBeforeRun=false exec node --import tsx \ + packages/db/tests/oracle-replay.ts \ + --config .review-2367/shared-startup.config.ts --maxWorkers=1 \ + packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts +``` + +The second receiving replay changes only the property suffix to `node-sqlite`. +The failing calibration uses `.review-2367/trailbase-gap/replay-calibration.config.ts` +instead of the ordinary config. Scratch logs retain the source-mutant, +calibration, replay, typecheck, and lint results. + +## Author's rubric audit + +This is a self-audit of the new projection against production head +`5feb26616a8357853454c2f9ff2ce0b0adb9c908`, before production edits. + +| Requirement | Evidence | +| --- | --- | +| ORC-001 | Existing cleanup/sync-run, complete eager-result, and documented SQLite readiness authorities are stated beside the model; finite limits follow. | +| ORC-002 | Pure set/Map recomputation uses history scalars, with no production transition or classifier imports. | +| ORC-003 | Opening prose, reference computation, grammar, named production driver, and three-cut comparison remain visible in directly named modules. | +| ORC-004 | Reconstruction, ablation, finite ranges, and rejected nearby forms are documented above. Rejected lists have no redundant payload axis. | +| ORC-005 | Real TrailBase and persisted wrapper execute. Actual hydration is held after its work; old completion, replacement readiness, and subsequent event are observed. No missing-input hypothesis remains at those cuts. | +| ORC-006 | Permanent row-loss controls fail the shared comparison. The isolated guard edit reaches obsolete work and is classified as scoped equivalence. | +| ORC-007 | Equal 50-history fixed/fresh campaigns per host, campaign seed 728159, and checked seed-plus-path failure and clean replays. No commands grammar is used. | +| ORC-008 | Not triggered: this projection recomputes observations from a complete finite history rather than introducing persistent reference-machine state. | +| ORC-009 | The file identifies a row-set projection of the primary lifecycle law. Its three checkpoint names map directly to cleanup/replacement boundaries. | +| ORC-010 | Common failure-preserving cleanup retains primary and cleanup errors; error reports remain append-only through cleanup and final reader release is checked. | +| ORC-011 | Not triggered: no named shared semantic fault requires a different formulation. The readiness assumption was checked against the public SQLite contract. | +| ORC-012 | This versioned record names the production head, each applicable outcome, exact mutation classification, and remaining receiving limits. | +| ORC-013 | Replacement-ready and replacement-loading witnesses distinguish late old deletion/overwrite from preserved new rows; empty and nonempty replacement both run. | +| ORC-014 | Controlled persistence and real Node SQLite use the same law, grammar, driver, and comparison. Chromium OPFS receiving results are recorded by the parent integration. RecordApi remains controlled. | +| ORC-015 | All nine actual-entry states have enforced minimum-one reach separately per host and campaign, with counts above. | +| ORC-016 | Fixed histories replay the shared model and comparison. Calibration supplies wrong observations to that same judgment; no separate concrete product answers are embedded in examples. | + +## Limits + +This projection exercises one cleanup and one replacement run with three string +keys and numeric values, plus a distinct later event key. The RecordApi boundary +and native stream are controlled. It claims no deployed TrailBase SDK-wire +behavior, arbitrary I/O failure, concurrent user mutation, overlapping loads, +unbounded row count, or every intermediate publication. Those broader contracts +remain with their existing primary owners. diff --git a/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance-oracle.opfs.spec.ts b/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance-oracle.opfs.spec.ts new file mode 100644 index 0000000000..bebfc4cad3 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance-oracle.opfs.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { precedenceHistories } from '../../query-db-collection/tests/persisted-precedence-oracle' +import { + cachedPeerClaimHistories, + scopedHydrationHistories, +} from '../../query-db-collection/tests/retained-scoped-hydration-oracle' +import { hydrationLifecycleHistories } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' +import { eagerReloadHistories } from '../../db-adapter-conformance/tests/eager-reload-oracle' +import type { EagerGapResult } from './eager-gap-conformance.opfs' +// The browser page applies the owners' shared comparisons and local reach. +// Count every receiver here so a missing run cannot pass as conformance. +function checkEagerGapReceiving(result: EagerGapResult) { + expect(result.error).toBeUndefined() + expect(result.results.filter((r) => r.owner === `query`)).toHaveLength( + precedenceHistories.length, + ) + expect(result.results.filter((r) => r.owner === `query-scoped`)).toHaveLength( + scopedHydrationHistories.length, + ) + expect( + result.results.filter((r) => r.owner === `query-cached-claim`), + ).toHaveLength(cachedPeerClaimHistories.length) + expect(result.results.filter((r) => r.owner === `trailbase`)).toHaveLength( + hydrationLifecycleHistories.length, + ) + for (const owner of [`rxdb`, `powersync`]) + expect(result.results.filter((r) => r.owner === owner)).toHaveLength( + eagerReloadHistories.length, + ) + expect(result.results.flatMap((r) => r.violations)).toEqual([]) + expect(result.artifacts).toEqual([]) +} +declareOracle({ check: [checkEagerGapReceiving] }) +test(`persisted precedence, retired hydration, and unwrapped reload obey their owners in Chromium`, async ({ + page, +}) => { + test.setTimeout(300_000) + const errors: Array = [] + page.on(`pageerror`, (error) => errors.push(error.message)) + page.on(`console`, (message) => + console.info(`browser ${message.type()}: ${message.text()}`), + ) + await page.goto(`/e2e/eager-gap-conformance.opfs.html`) + await page.waitForFunction(() => window.__eagerGapConformance !== undefined) + expect(errors).toEqual([]) + checkEagerGapReceiving( + await page.evaluate(() => window.__eagerGapConformance!), + ) +}) diff --git a/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.html b/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.html new file mode 100644 index 0000000000..55a952dd9e --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.html @@ -0,0 +1,11 @@ + + + + + + Eager gap receiving oracles + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.ts new file mode 100644 index 0000000000..1cc90a4708 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.ts @@ -0,0 +1,179 @@ +// Receiving environment only: each law, grammar, and judgment comes from its +// primary owner. OPFS supplies real persisted scheduling for Query/TrailBase; +// the reload receivers remain unwrapped so persistence cannot mask selection. +import { + createBrowserWASQLitePersistence, + openBrowserWASQLiteOPFSDatabase, +} from '../src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' +import { observePersistedPrecedence } from '../../query-db-collection/tests/persisted-precedence-driver' +import { + observeCachedPeerClaim, + observeRetainedScopedHydration, +} from '../../query-db-collection/tests/retained-scoped-hydration-driver' +import { + cachedPeerClaimHistories, + cachedPeerClaimReached, + cachedPeerClaimStates, + cachedPeerClaimViolations, + scopedHydrationHistories, + scopedHydrationReached, + scopedHydrationStates, + scopedHydrationViolations, +} from '../../query-db-collection/tests/retained-scoped-hydration-oracle' +import { + precedenceHistories, + precedenceReached, + precedenceStates, + precedenceViolations, +} from '../../query-db-collection/tests/persisted-precedence-oracle' +import { observeHydrationLifecycle } from '../../trailbase-db-collection/tests/lifecycle-hydration-driver' +import { + hydrationLifecycleHistories, + hydrationLifecycleStates, + hydrationLifecycleViolations, +} from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' +import { + eagerReloadHistories, + eagerReloadReached, + eagerReloadStates, + eagerReloadViolations, +} from '../../db-adapter-conformance/tests/eager-reload-oracle' +import { observeEagerReload } from '../../db-adapter-conformance/tests/eager-reload-driver' +import { rxdbEagerReloadAdapter } from '../../rxdb-db-collection/tests/eager-reload-driver' +import { powerSyncBrowserEagerReloadAdapter } from '../../powersync-db-collection/tests/eager-reload-browser' +import { + removeStartupOPFSArtifacts, + startupOPFSArtifacts, +} from './persisted-startup-opfs-cleanup' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' + +export type EagerGapResult = { + results: Array<{ owner: string; history: unknown; violations: Array }> + reach: Record> + artifacts: Array + error?: string +} +declare global { + interface Window { + __eagerGapConformance?: EagerGapResult + } +} +const result: EagerGapResult = { results: [], reach: {}, artifacts: [] } +function counter(owner: string, states: ReadonlyArray) { + const counts = Object.fromEntries(states.map((state) => [state, 0])) + result.reach[owner] = counts + return (reached: Array) => { + for (const state of reached) counts[state] = (counts[state] ?? 0) + 1 + } +} +async function database( + name: string, + body: (p: PersistedCollectionPersistence) => Promise, +): Promise { + await removeStartupOPFSArtifacts(name) + const db = await openBrowserWASQLiteOPFSDatabase({ databaseName: name }) + return withOracleCleanup( + () => body(createBrowserWASQLitePersistence({ database: db })), + [() => db.close?.(), () => removeStartupOPFSArtifacts(name)], + ) +} +async function removeReloadFiles() { + // This page owns an isolated Playwright origin and these SDK fixture names. + const root = + (await navigator.storage.getDirectory()) as FileSystemDirectoryHandle & { + keys: () => AsyncIterableIterator + } + for await (const name of root.keys()) + if (name.startsWith(`powersync-eager-reload-`)) + await root.removeEntry(name, { recursive: true }) + await removeStartupOPFSArtifacts(`ps-gap-closed.sqlite`) +} +async function run() { + try { + const queryReach = counter(`query`, precedenceStates) + for (const [i, h] of precedenceHistories.entries()) { + const observed = await database(`ps-gap-query-${i}.sqlite`, (p) => + observePersistedPrecedence( + p, + h, + `query-gap-${i}`, + inProcessSeams.holdFirstRowSnapshot, + ), + ) + queryReach(precedenceReached(h)) + result.results.push({ + owner: `query`, + history: h, + violations: precedenceViolations(h, observed), + }) + } + const scopedReach = counter(`query-scoped`, scopedHydrationStates) + for (const [i, h] of scopedHydrationHistories.entries()) { + const observed = await database(`ps-gap-scoped-${i}.sqlite`, (p) => + observeRetainedScopedHydration(p, h, `query-scoped-gap-${i}`), + ) + scopedReach(scopedHydrationReached(h)) + result.results.push({ + owner: `query-scoped`, + history: h, + violations: scopedHydrationViolations(h, observed), + }) + } + const claimReach = counter(`query-cached-claim`, cachedPeerClaimStates) + for (const [i, h] of cachedPeerClaimHistories.entries()) { + const observed = await database(`ps-gap-claim-${i}.sqlite`, (p) => + observeCachedPeerClaim(p, h, `query-claim-gap-${i}`), + ) + claimReach(cachedPeerClaimReached(h)) + result.results.push({ + owner: `query-cached-claim`, + history: h, + violations: cachedPeerClaimViolations(h, observed), + }) + } + const trailReach = counter(`trailbase`, hydrationLifecycleStates) + for (const [i, h] of hydrationLifecycleHistories.entries()) { + const observed = await database(`ps-gap-trail-${i}.sqlite`, (p) => + observeHydrationLifecycle(p, h, `trail-gap-${i}`), + ) + trailReach(observed.reached) + result.results.push({ + owner: `trailbase`, + history: h, + violations: hydrationLifecycleViolations(h, observed), + }) + } + for (const [owner, adapter] of [ + [`rxdb`, rxdbEagerReloadAdapter], + [`powersync`, powerSyncBrowserEagerReloadAdapter], + ] as const) { + const reach = counter(owner, eagerReloadStates) + for (const [i, h] of eagerReloadHistories.entries()) { + const observed = await observeEagerReload( + adapter, + h, + `${owner}-gap-${i}`, + () => reach(eagerReloadReached(h)), + ) + result.results.push({ + owner, + history: h, + violations: eagerReloadViolations(h, observed), + }) + await removeReloadFiles() + } + } + for (const [owner, counts] of Object.entries(result.reach)) { + console.info(`${owner}.browser-gap`, JSON.stringify(counts)) + for (const [state, count] of Object.entries(counts)) + if (count === 0) throw new Error(`${owner} did not reach ${state}`) + } + result.artifacts = await startupOPFSArtifacts() + } catch (error) { + result.error = error instanceof Error ? `${error.stack}` : String(error) + } + window.__eagerGapConformance = result +} +void run() diff --git a/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts b/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts index 0a3c2a180b..6925b29ca4 100644 --- a/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts +++ b/packages/browser-db-sqlite-persistence/playwright.opfs.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ `opfs-request-order-oracle.opfs.spec.ts`, `reload-class-oracle.opfs.spec.ts`, `persisted-startup-conformance-oracle.opfs.spec.ts`, + `eager-gap-conformance-oracle.opfs.spec.ts`, `persisted-on-demand-coverage.opfs.spec.ts`, ], timeout: 60_000, diff --git a/packages/db-adapter-conformance/README.md b/packages/db-adapter-conformance/README.md index bc8bbfdd2c..e2a3ba08ee 100644 --- a/packages/db-adapter-conformance/README.md +++ b/packages/db-adapter-conformance/README.md @@ -97,6 +97,22 @@ persistence capability protocol through the real RxDB/PowerSync sync boundary. The persistence-loss calibration wraps hydration-scoped adapters, requires an actual dropped write, and fails if its clean candidate fails. +Package-local RxDB/PowerSync registrations also invoke the shared capability +suite, so package-scoped test discovery and mutation audits include the protocol +owner. The 22-history grammar and judgment stay in this package. + +`tests/eager-reload-oracle.ts` reuses the complete-result judgment for unwrapped +RxDB/PowerSync receivers. These histories cross fresh startup and empty/nonempty +reload baselines with an optional persisting optimistic insert. Unwrapped sources +are necessary: persistence's own snapshot choice can mask a broken retained-row +predicate. After mutation settlement and applied receipts, source rows must equal +the complete result. Readiness alone proves acceptance, so fulfillment during a +persisting mutation can still expose the coherent retained baseline and overlay. +Chromium's `eager-gap-conformance-oracle.opfs.spec.ts` receives the same 24 +histories per adapter, plus 104 eager Query precedence, 48 on-demand Query scope, +two cached-peer claims, and 24 TrailBase lifecycle histories from their primary owners. These 226 histories enforce separate reach +and share their owners' comparisons. + ```sh pnpm --filter @tanstack/query-db-collection exec vitest run --coverage.enabled=false --typecheck.enabled=false tests/persisted-startup-conformance-oracle.test.ts pnpm --filter @tanstack/convex-db-collection exec vitest run --coverage.enabled=false --typecheck.enabled=false tests/persisted-startup-conformance-oracle.property.test.ts diff --git a/packages/db-adapter-conformance/tests/eager-reload-driver.ts b/packages/db-adapter-conformance/tests/eager-reload-driver.ts new file mode 100644 index 0000000000..edfbe32a98 --- /dev/null +++ b/packages/db-adapter-conformance/tests/eager-reload-driver.ts @@ -0,0 +1,162 @@ +/** + * Real SDK receiving composition. The second sync invocation is held before + * adapter entry, after core retired the old run. Replacing provider rows in + * that gap supplies the missed-live-change premise without a mock row answer. + * The actual sync then reads the real SDK. The recorder observes commit + * invocation, public rows before delivery, and rows inside load fulfillment. + */ +import { + createCollection, + createTransaction, + whenSyncAccepted, +} from '@tanstack/db' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { sortedStartupRows, startupRows } from './persisted-startup-oracle' +import { untilStartup } from './persisted-startup-driver' +import type { CollectionConfig, SyncConfig } from '@tanstack/db' +import type { StartupRow } from './persisted-startup-oracle' +import type { + EagerReloadHistory, + EagerReloadObservation, +} from './eager-reload-oracle' + +export type EagerReloadSDK = { + // Preserve each adapter's full runtime configuration. The receiver only + // depends on these common fields; schema and utility generics stay private + // to their adapter instead of requiring every SDK to share one schema type. + options: Pick< + CollectionConfig, + `id` | `getKey` | `sync` | `syncMode` + > + replace: (rows: Array) => Promise + dispose: () => Promise +} +export type EagerReloadAdapter = { + open: (initial: Array, id: string) => Promise +} + +export async function observeEagerReload( + adapter: EagerReloadAdapter, + h: EagerReloadHistory, + id: string, + reached: () => void = () => {}, + invertSnapshot = false, +): Promise { + const sdk = await adapter.open(startupRows(h.previous, 1), id) + const source = sdk.options.sync.sync + let enter!: () => void + let entered = false + let actualCleanup: ReturnType + let run = 0 + let submittedRows = 0 + let submittedCommits = 0 + const receipts: Array> = [] + let ended = false + const targetRun = h.start === `fresh` ? 1 : 2 + const sync: SyncConfig = { + ...sdk.options.sync, + sync: (params) => { + run++ + if (run !== targetRun) return source(params) + enter = () => { + actualCleanup = source({ + ...params, + // Calibration changes only the source's declaration at its public + // sync boundary. With no persistence this inverts the retained-row + // predicate, as the historical operand mutant does. + begin: (options) => + params.begin( + invertSnapshot ? { snapshot: !options?.snapshot } : options, + ), + write: (message) => { + submittedRows++ + params.write(message) + }, + commit: (signal) => { + submittedCommits++ + const receipt = params.commit(signal) + receipts.push(receipt) + return receipt + }, + }) + } + entered = true + return () => { + ended = true + if (typeof actualCleanup === `function`) return actualCleanup() + return actualCleanup?.cleanup?.() + } + }, + } + const collection = createCollection({ ...sdk.options, sync, startSync: true }) + let releaseMutation!: () => void + const mutationGate = new Promise((resolve) => { + releaseMutation = resolve + }) + let mutation: ReturnType | undefined + let mutationReleased = false + let mutationSettled = !h.mutation + return withOracleCleanup(async () => { + if (h.start === `reload`) await collection.preload() + let settled = false + let error: string | undefined + let atFulfillment: Array = [] + let mutationReleasedBeforeFulfillment = false + let mutationSettledBeforeFulfillment = false + const pending = ( + h.start === `reload` ? collection.reload() : collection.preload() + ).then( + () => { + settled = true + mutationReleasedBeforeFulfillment = mutationReleased + mutationSettledBeforeFulfillment = mutationSettled + atFulfillment = sortedStartupRows(collection.values()) + }, + (cause: unknown) => { + settled = true + error = String(cause) + }, + ) + await untilStartup(() => entered) + await sdk.replace(startupRows(h.next, 2)) + if (h.mutation) { + mutation = createTransaction({ mutationFn: () => mutationGate }) + mutation.mutate(() => collection.insert({ id: `optimistic`, value: 99 })) + void mutation.when(`settled`).then(() => { + mutationSettled = true + }) + } + const before = sortedStartupRows(collection.values()) + const beforeStatus = collection.status + const settledBeforeSource = settled + if (ended) throw new Error(`Target sync ended before source entry`) + enter() + await untilStartup( + () => + settled || + (submittedCommits > 0 && + submittedRows >= startupRows(h.next, 2).length), + ) + await Promise.all(receipts.map(whenSyncAccepted)) + reached() + mutationReleased = true + releaseMutation() + await mutation?.when(`settled`) + await pending + await Promise.all(receipts.map(whenSyncAccepted)) + await Promise.all(receipts) + return { + before, + beforeStatus, + settledBeforeSource, + submittedRows, + submittedCommits, + acceptedCommits: receipts.length, + atFulfillment, + mutationReleasedBeforeFulfillment, + mutationSettledBeforeFulfillment, + final: sortedStartupRows(collection.values()), + ...(error === undefined ? {} : { error }), + } + }, [() => releaseMutation(), () => collection.cleanup(), () => sdk.dispose()]) +} diff --git a/packages/db-adapter-conformance/tests/eager-reload-oracle-suite.ts b/packages/db-adapter-conformance/tests/eager-reload-oracle-suite.ts new file mode 100644 index 0000000000..9621390d4f --- /dev/null +++ b/packages/db-adapter-conformance/tests/eager-reload-oracle-suite.ts @@ -0,0 +1,109 @@ +/** Registration and campaigns for the unwrapped eager-reload receiving law. */ +import fc from 'fast-check' +import { afterAll, expect, it } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { + oracleFixedSeed, + oraclePropertyOptions, + oracleReplayTest, + oracleRoute, + oracleRouteOf, + oracleRuns, +} from '../../db/tests/oracle-config' +import { + eagerReloadHistories, + eagerReloadHistoryArb, + eagerReloadReached, + eagerReloadStates, + eagerReloadViolations, +} from './eager-reload-oracle' +import { observeEagerReload } from './eager-reload-driver' +import type { EagerReloadAdapter } from './eager-reload-driver' +import type { EagerReloadHistory } from './eager-reload-oracle' + +declareOracle({ + check: [eagerReloadViolations], + grammar: [eagerReloadHistoryArb], +}) + +export function registerEagerReloadConformance( + property: string, + adapter: EagerReloadAdapter, +) { + let sequence = 0 + const fixedReach = createOracleReach(`${property}.fixed-histories`, [ + ...eagerReloadStates, + ]) + afterAll(() => fixedReach.assertReached()) + const check = async (h: EagerReloadHistory, reach = fixedReach) => { + reach.beginHistory() + const observed = await observeEagerReload( + adapter, + h, + `${property}-${sequence++}`, + () => { + for (const state of eagerReloadReached(h)) reach.reach(state) + }, + ) + expect( + eagerReloadViolations(h, observed), + JSON.stringify({ h, observed }), + ).toEqual([]) + } + oracleReplayTest(it).each(eagerReloadHistories)( + `replaces the complete result: %j`, + async (h) => { + oracleRoute(`fixed history`) + await check(h) + }, + ) + oracleReplayTest(it)( + `rejects an inverted snapshot decision on retained rows`, + async () => { + oracleRoute(`calibration`) + const h = eagerReloadHistories.find( + (candidate) => + candidate.previous === 1 && + candidate.next === 2 && + !candidate.mutation, + )! + const observed = await observeEagerReload( + adapter, + h, + `${property}-inverted`, + undefined, + true, + ) + expect(eagerReloadViolations(h, observed)).not.toEqual([]) + }, + ) + for (const campaign of [`fixed`, `random`] as const) { + const register = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + register( + `reconciles generated reload histories (${campaign})`, + async () => { + const parameters = + campaign === `fixed` + ? oracleFixedSeed(2367, { numRuns: oracleRuns(80) }) + : oraclePropertyOptions(80, property) + oracleRoute(oracleRouteOf(parameters)) + const reach = createOracleReach(`${property}.${campaign}`, [ + ...eagerReloadStates, + ]) + await fc.assert( + fc.asyncProperty(eagerReloadHistoryArb, (h) => check(h, reach)), + parameters, + ) + reach.assertReached() + process.stdout.write( + `${property}.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }, + 120_000, + ) + } +} diff --git a/packages/db-adapter-conformance/tests/eager-reload-oracle.ts b/packages/db-adapter-conformance/tests/eager-reload-oracle.ts new file mode 100644 index 0000000000..b2248cdb62 --- /dev/null +++ b/packages/db-adapter-conformance/tests/eager-reload-oracle.ts @@ -0,0 +1,201 @@ +/** + * # Does a complete eager reload replace retained rows? + * + * A reload keeps the previous public rows until its new sync run supplies a + * complete result. That result determines the new keyed rows and values, + * including deletions. Authority: the glossary's reload/snapshot contract, + * source-health-oracle.test.ts's opt-in snapshot law, and READY-3/PUB-7 of the + * eager-source contract. The adapter chooses replacement; core never infers it + * merely from reload. This receiving grammar reuses the startup owner's + * independent complete-result set judgment, without a second replacement model. + * + * Unwrapped receivers matter: persistence independently selects a snapshot and + * would mask a broken retained-row predicate. The provider changes between SDK + * subscriptions, so later change events cannot repair an omitted-row defect. + * A disjoint optimistic insert may persist during delivery. Its settlement + * drops its unconfirmed row (optimistic-history-oracle.ts). The law permits + * either core-legal application schedule while that transaction persists. + * + * Limits: three source keys, numeric values, one fresh load or one reload, one + * disjoint optimistic insert, and real local SDK reads. No remote replication, + * persistence durability, arbitrary concurrent mutations, or every intermediate + * publication claim. Drivers record before-source and fulfillment cuts. + */ +import fc from 'fast-check' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { + expectedPersistedStartup, + sortedStartupRows, + startupRows, +} from './persisted-startup-oracle' +import type { StartupRow } from './persisted-startup-oracle' + +export type EagerReloadHistory = { + start: `fresh` | `reload` + previous: number + next: number + mutation: boolean +} + +// Fresh startup has no accepted baseline. Reload can retain an empty or +// nonempty baseline; every next subset is legal, including empty and overlap. +// Removing reload loses omissions from retained rows. Removing mutation loses +// the ordinary-commit versus snapshot scheduling boundary on an empty baseline. +export const eagerReloadHistoryArb: fc.Arbitrary = fc + .oneof( + fc.record({ + start: fc.constant(`fresh` as const), + previous: fc.constant(0), + }), + fc.record({ + start: fc.constant(`reload` as const), + previous: fc.constant(0), + }), + fc.record({ + start: fc.constant(`reload` as const), + previous: fc.integer({ min: 1, max: 7 }), + }), + ) + .chain((prefix) => + fc + .record({ + next: fc.integer({ min: 0, max: 7 }), + mutation: fc.boolean(), + }) + .map((suffix) => ({ ...prefix, ...suffix })), + ) + +export const eagerReloadHistories: Array = [ + { start: `fresh` as const, previous: 0 }, + ...[0, 1, 3].map((previous) => ({ start: `reload` as const, previous })), +].flatMap((prefix) => + [0, 2, 3].flatMap((next) => + [false, true].map((mutation) => ({ ...prefix, next, mutation })), + ), +) + +export const eagerReloadStates = [ + `fresh`, + `reload-empty`, + `reload-retained`, + `empty-result`, + `omitted-row`, + `changed-overlap`, + `persisting-mutation`, + `no-mutation`, + `fresh-held`, + `reload-empty-held`, + `reload-retained-held`, +] as const +export function eagerReloadReached( + h: EagerReloadHistory, +): Array<(typeof eagerReloadStates)[number]> { + const reached: Array<(typeof eagerReloadStates)[number]> = [ + h.start === `fresh` + ? `fresh` + : h.previous + ? `reload-retained` + : `reload-empty`, + h.mutation ? `persisting-mutation` : `no-mutation`, + ] + if (h.next === 0) reached.push(`empty-result`) + if (h.previous & ~h.next) reached.push(`omitted-row`) + if (h.previous & h.next) reached.push(`changed-overlap`) + if (h.mutation) + reached.push( + h.start === `fresh` + ? `fresh-held` + : h.previous + ? `reload-retained-held` + : `reload-empty-held`, + ) + return reached +} +export type EagerReloadObservation = { + before: Array + beforeStatus: string + settledBeforeSource: boolean + submittedRows: number + submittedCommits: number + acceptedCommits: number + atFulfillment: Array + mutationReleasedBeforeFulfillment: boolean + mutationSettledBeforeFulfillment: boolean + final: Array + error?: string +} +export function eagerReloadViolations( + h: EagerReloadHistory, + observed: EagerReloadObservation, +): Array { + const violations: Array = [] + const initial = sortedStartupRows([ + ...startupRows(h.previous, 1), + ...(h.mutation ? [{ id: `optimistic`, value: 99 }] : []), + ]) + const expected = expectedPersistedStartup( + { + previous: h.previous, + next: h.next, + peer: false, + response: `after-restore`, + }, + `collection`, + ).rows + const equal = (a: unknown, b: unknown) => + JSON.stringify(a) === JSON.stringify(b) + if (!equal(observed.before, initial)) violations.push(`before-source rows`) + if (observed.beforeStatus !== (h.start === `reload` ? `ready` : `loading`)) + violations.push(`before-source status`) + if (observed.settledBeforeSource) + violations.push(`settled before source entry`) + if (observed.error !== undefined) + violations.push(`source failed: ${observed.error}`) + if ( + observed.submittedRows < expected.length || + observed.submittedCommits === 0 + ) + violations.push(`complete source submission not reached`) + if (observed.acceptedCommits !== observed.submittedCommits) + violations.push(`source acceptance not reached`) + // An optimistic overlay remains public until settlement. Releasing its + // external persistence gate can race load fulfillment, so after release + // either overlay presence is legal at that cut. Readiness carries acceptance, + // not visibility: while that mutation is unsettled, an ordinary transaction + // can leave the old baseline public, while a snapshot can already replace it. + // Each whole source projection must fit one of those two legal schedules. + // After settlement and applied receipts, only the complete result is legal. + const fulfillmentSource = observed.atFulfillment.filter( + (row) => row.id !== `optimistic`, + ) + const overlay = observed.atFulfillment.filter( + (row) => row.id === `optimistic`, + ) + const retained = startupRows(h.previous, 1) + if ( + !equal(fulfillmentSource, expected) && + !( + h.mutation && + !observed.mutationSettledBeforeFulfillment && + equal(fulfillmentSource, retained) + ) + ) + violations.push(`fulfillment source rows`) + if ( + (!h.mutation && overlay.length > 0) || + (overlay.length > 0 && !equal(overlay, [{ id: `optimistic`, value: 99 }])) + ) + violations.push(`fulfillment optimistic overlay`) + if ( + h.mutation && + !observed.mutationReleasedBeforeFulfillment && + overlay.length !== 1 + ) + violations.push(`pending optimistic overlay disappeared`) + if (!equal(observed.final, expected)) violations.push(`final rows`) + return violations +} +declareOracle({ + grammar: [eagerReloadHistoryArb, eagerReloadHistories], + check: [eagerReloadViolations], +}) diff --git a/packages/db-adapter-conformance/tests/persisted-capability-conformance-oracle.test.ts b/packages/db-adapter-conformance/tests/persisted-capability-conformance-oracle.test.ts index 87002e8677..45753307d6 100644 --- a/packages/db-adapter-conformance/tests/persisted-capability-conformance-oracle.test.ts +++ b/packages/db-adapter-conformance/tests/persisted-capability-conformance-oracle.test.ts @@ -1,46 +1,13 @@ -/** Real RxDB/PowerSync receiving paths for the finite capability grammar. */ -import { afterAll, describe, expect, it } from 'vitest' -import { createOracleReach } from '../../db/tests/oracle-reach' +/** The structural capability law receives both actual adapters and storage hosts. */ import { declareOracle } from '../../db/tests/oracle-declaration' -import { oracleRoute } from '../../db/tests/oracle-config' import { rxdbStartupAdapter } from '../../rxdb-db-collection/tests/persisted-startup-driver' import { powerSyncStartupAdapter } from '../../powersync-db-collection/tests/persisted-startup-driver' -import { createStartupHost } from './persisted-startup-host' +import { registerCapabilityConformance } from './persisted-capability-oracle-suite' import { capabilityHistories, capabilityViolations, - observeCapability, } from './persisted-capability-oracle' declareOracle({ check: [capabilityViolations], grammar: [capabilityHistories] }) -for (const [name, adapter] of [ - [`rxdb`, rxdbStartupAdapter], - [`powersync`, powerSyncStartupAdapter], -] as const) - for (const host of [`controlled`, `node-sqlite`] as const) - describe(`${name} capability through ${host}`, () => { - const reach = createOracleReach(`${name}.capability.${host}`, [ - ...new Set(capabilityHistories.map((h) => h.kind)), - ]) - afterAll(() => reach.assertReached()) - it.each(capabilityHistories)( - `validates persistence protocol: %j`, - async (h) => { - oracleRoute(`fixed history`) - reach.beginHistory() - const storage = createStartupHost(host) - try { - const error = await observeCapability( - adapter, - storage.persistence, - h, - `${name}-${host}-${JSON.stringify(h)}`, - ) - reach.reach(h.kind) - expect(capabilityViolations(h, error)).toEqual([]) - } finally { - storage.close() - } - }, - ) - }) +registerCapabilityConformance(`rxdb`, rxdbStartupAdapter) +registerCapabilityConformance(`powersync`, powerSyncStartupAdapter) diff --git a/packages/db-adapter-conformance/tests/persisted-capability-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-capability-oracle-suite.ts new file mode 100644 index 0000000000..3fc2dd4989 --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-capability-oracle-suite.ts @@ -0,0 +1,47 @@ +/** Shared receiving registration for the finite capability owner. */ +import { afterAll, describe, expect, it } from 'vitest' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { oracleRoute } from '../../db/tests/oracle-config' +import { createStartupHost } from './persisted-startup-host' +import { + capabilityHistories, + capabilityViolations, + observeCapability, +} from './persisted-capability-oracle' +import type { PersistedStartupAdapter } from './persisted-startup-driver' +import type { PersistedStartupHistory } from './persisted-startup-oracle' + +declareOracle({ check: [capabilityViolations], grammar: [capabilityHistories] }) +export function registerCapabilityConformance( + name: string, + adapter: PersistedStartupAdapter, +) { + for (const host of [`controlled`, `node-sqlite`] as const) + describe(`${name} capability through ${host}`, () => { + const reach = createOracleReach(`${name}.capability.${host}`, [ + ...new Set(capabilityHistories.map((h) => h.kind)), + ]) + afterAll(() => reach.assertReached()) + it.each(capabilityHistories)( + `validates persistence protocol: %j`, + async (h) => { + oracleRoute(`fixed history`) + reach.beginHistory() + const storage = createStartupHost(host) + try { + const error = await observeCapability( + adapter, + storage.persistence, + h, + `${name}-${host}-${JSON.stringify(h)}`, + ) + reach.reach(h.kind) + expect(capabilityViolations(h, error)).toEqual([]) + } finally { + storage.close() + } + }, + ) + }) +} diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 70fc97cc4d..67ddba24f4 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -330,6 +330,13 @@ const staticOracleProperties = [ `persisted-demand-coverage.required`, `sqlite-reload.class`, `query.startup-ownership`, + `query.persisted-write-precedence`, + `query.retained-scoped-hydration`, + `query.cached-peer-claim`, + `adapter.rxdb.eager-reload`, + `adapter.powersync.eager-reload`, + `trailbase.lifecycle.persisted-hydration.controlled`, + `trailbase.lifecycle.persisted-hydration.node-sqlite`, `adapter.convex.persisted-startup`, `adapter.electric.persisted-startup`, `adapter.trailbase.persisted-startup`, diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index c354a69c6f..71c2270700 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -245,8 +245,27 @@ const ownerGroups: ReadonlyArray< [ `trailbase-db-collection/tests/lifecycle-oracle.property.test.ts`, `trailbase`, - `lifecycle`, + `lifecycle lifecycle.persisted-hydration.controlled lifecycle.persisted-hydration.node-sqlite`, ], + [ + `query-db-collection/tests/persisted-precedence-oracle.test.ts`, + `query`, + `persisted-write-precedence`, + ], + [ + `query-db-collection/tests/retained-scoped-hydration-oracle.test.ts`, + `query`, + `retained-scoped-hydration cached-peer-claim`, + ], + ...[`rxdb`, `powersync`].map( + (adapter) => + [ + `${adapter}-db-collection/tests/eager-reload-conformance-oracle.property.test.ts`, + `adapter.${adapter}`, + `eager-reload`, + `db-adapter-conformance/tests/eager-reload-oracle-suite.ts`, + ] as const, + ), [ `electric-db-collection/tests/electric-descriptor-isolation-oracle.test.ts`, `electric`, @@ -969,6 +988,32 @@ export type OracleReplayManifestEntry = { // Pagination cells carry their property name in the test title so the guard // can select one cell without running the rest of the matrix. const replayTestNames = new Map([ + [ + `query.persisted-write-precedence`, + `orders generated fetch/write histories \\(random\\)`, + ], + [ + `query.retained-scoped-hydration`, + `revalidates generated retained scopes \\(random\\)`, + ], + [ + `query.cached-peer-claim`, + `orders generated cached peer claims \\(random\\)`, + ], + ...[`controlled`, `node-sqlite`].map( + (host) => + [ + `trailbase.lifecycle.persisted-hydration.${host}`, + `retires generated persisted hydration through ${host} with random seed`, + ] as const, + ), + ...[`rxdb`, `powersync`].map( + (adapter) => + [ + `adapter.${adapter}.eager-reload`, + `reconciles generated reload histories \\(random\\)`, + ] as const, + ), ...[ `query.startup-ownership`, ...[`convex`, `electric`, `trailbase`, `rxdb`, `powersync`].map( diff --git a/packages/powersync-db-collection/tests/eager-reload-browser.ts b/packages/powersync-db-collection/tests/eager-reload-browser.ts new file mode 100644 index 0000000000..5debcb6b5e --- /dev/null +++ b/packages/powersync-db-collection/tests/eager-reload-browser.ts @@ -0,0 +1,20 @@ +import { PowerSyncDatabase } from '@powersync/web' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { powerSyncEagerReloadAdapterFor } from './eager-reload-common' + +let sequence = 0 +export const powerSyncBrowserEagerReloadAdapter = + powerSyncEagerReloadAdapterFor((schema) => { + const database = new PowerSyncDatabase({ + schema, + database: { dbFilename: `powersync-eager-reload-${sequence++}.sqlite` }, + }) + return Promise.resolve({ + database, + dispose: () => + withOracleCleanup( + () => database.disconnectAndClear(), + [() => database.close()], + ), + }) + }) diff --git a/packages/powersync-db-collection/tests/eager-reload-common.ts b/packages/powersync-db-collection/tests/eager-reload-common.ts new file mode 100644 index 0000000000..a929503022 --- /dev/null +++ b/packages/powersync-db-collection/tests/eager-reload-common.ts @@ -0,0 +1,44 @@ +/** Real PowerSync SQLite table reads receive the unwrapped reload judgment. */ +import { Schema, Table, column } from '@powersync/common' +import { z } from 'zod' +import { powerSyncCollectionOptions } from '../src/powersync' +import type { CommonPowerSyncDatabase } from '@powersync/common' +import type { StartupRow } from '../../db-adapter-conformance/tests/persisted-startup-oracle' +import type { EagerReloadAdapter } from '../../db-adapter-conformance/tests/eager-reload-driver' + +const schema = new Schema({ rows: new Table({ value: column.integer }) }) +const rowSchema = z.object({ id: z.string(), value: z.number() }) +export function powerSyncEagerReloadAdapterFor( + open: (schema: Schema) => Promise<{ + database: CommonPowerSyncDatabase + dispose: () => Promise + }>, +): EagerReloadAdapter { + return { + async open(initial, id) { + const { database, dispose } = await open(schema) + const replace = (next: Array) => + database.writeTransaction(async (tx) => { + await tx.execute(`DELETE FROM rows`) + for (const row of next) + await tx.execute(`INSERT INTO rows(id, value) VALUES (?, ?)`, [ + row.id, + row.value, + ]) + }) + await replace(initial) + return { + options: powerSyncCollectionOptions({ + id, + database, + table: schema.props.rows, + schema: rowSchema, + syncBatchSize: 1, + onDeserializationError: () => {}, + }), + replace, + dispose, + } + }, + } +} diff --git a/packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts b/packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts new file mode 100644 index 0000000000..b9f796b999 --- /dev/null +++ b/packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts @@ -0,0 +1,14 @@ +/** PowerSync receives complete eager reload replacement through local SQLite. */ +import { declareOracle } from '../../db/tests/oracle-declaration' +import { eagerReloadHistoryArb } from '../../db-adapter-conformance/tests/eager-reload-oracle' +import { registerEagerReloadConformance } from '../../db-adapter-conformance/tests/eager-reload-oracle-suite' +import { powerSyncEagerReloadAdapter } from './eager-reload-driver' + +declareOracle({ + owns: [`src/powersync.ts#powerSyncCollectionOptions`], + grammar: [eagerReloadHistoryArb], +}) +registerEagerReloadConformance( + `adapter.powersync.eager-reload`, + powerSyncEagerReloadAdapter, +) diff --git a/packages/powersync-db-collection/tests/eager-reload-driver.ts b/packages/powersync-db-collection/tests/eager-reload-driver.ts new file mode 100644 index 0000000000..ec8117a255 --- /dev/null +++ b/packages/powersync-db-collection/tests/eager-reload-driver.ts @@ -0,0 +1,34 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { PowerSyncDatabase } from '@powersync/node' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { powerSyncEagerReloadAdapterFor } from './eager-reload-common' +import { TEST_DATABASE_IMPLEMENTATION } from './test-db-implementation' + +export const powerSyncEagerReloadAdapter = powerSyncEagerReloadAdapterFor( + async (schema) => { + if (!TEST_DATABASE_IMPLEMENTATION) + throw new Error(`No PowerSync SQLite implementation available`) + const directory = await mkdtemp(join(tmpdir(), `powersync-reload-`)) + const database = new PowerSyncDatabase({ + database: { + dbFilename: `source.sqlite`, + dbLocation: directory, + implementation: TEST_DATABASE_IMPLEMENTATION, + }, + schema, + }) + return { + database, + dispose: () => + withOracleCleanup( + () => database.disconnectAndClear(), + [ + () => database.close(), + () => rm(directory, { force: true, recursive: true }), + ], + ), + } + }, +) diff --git a/packages/powersync-db-collection/tests/persisted-capability-conformance-oracle.test.ts b/packages/powersync-db-collection/tests/persisted-capability-conformance-oracle.test.ts new file mode 100644 index 0000000000..bc66c821f6 --- /dev/null +++ b/packages/powersync-db-collection/tests/persisted-capability-conformance-oracle.test.ts @@ -0,0 +1,15 @@ +/** Package-local ownership makes the real protocol receiver visible to gap audits. */ +import { declareOracle } from '../../db/tests/oracle-declaration' +import { registerCapabilityConformance } from '../../db-adapter-conformance/tests/persisted-capability-oracle-suite' +import { + capabilityHistories, + capabilityViolations, +} from '../../db-adapter-conformance/tests/persisted-capability-oracle' +import { powerSyncStartupAdapter } from './persisted-startup-driver' + +declareOracle({ + owns: [`src/powersync.ts#powerSyncCollectionOptions`], + check: [capabilityViolations], + grammar: [capabilityHistories], +}) +registerCapabilityConformance(`powersync`, powerSyncStartupAdapter) diff --git a/packages/query-db-collection/package.json b/packages/query-db-collection/package.json index 6d2da5ab31..db891e1819 100644 --- a/packages/query-db-collection/package.json +++ b/packages/query-db-collection/package.json @@ -21,7 +21,7 @@ "dev": "vite build --watch", "lint": "eslint . --fix", "test": "vitest run", - "test:oracles": "vitest run --coverage.enabled=false tests/includes-work-counter-oracle.test.ts tests/ownership-lifecycle.oracle.test.ts tests/persisted-direct-write-window.oracle.test.ts tests/ssr-dehydration-oracle.test.ts tests/cursor-pagination.oracle.test.ts tests/cursor-pagination.cache-oracle.test.ts tests/cursor-pagination.publication-oracle.test.ts tests/cursor-pagination.boundary-oracle.test.ts tests/cursor-pagination-oracle.integration.test.ts tests/persisted-startup-conformance-oracle.test.ts", + "test:oracles": "vitest run --coverage.enabled=false tests/includes-work-counter-oracle.test.ts tests/ownership-lifecycle.oracle.test.ts tests/persisted-direct-write-window.oracle.test.ts tests/persisted-precedence-oracle.test.ts tests/retained-scoped-hydration-oracle.test.ts tests/ssr-dehydration-oracle.test.ts tests/cursor-pagination.oracle.test.ts tests/cursor-pagination.cache-oracle.test.ts tests/cursor-pagination.publication-oracle.test.ts tests/cursor-pagination.boundary-oracle.test.ts tests/cursor-pagination-oracle.integration.test.ts tests/persisted-startup-conformance-oracle.test.ts", "test:e2e": "vitest run --config vitest.e2e.config.ts" }, "type": "module", diff --git a/packages/query-db-collection/tests/direct-write-precedence-oracle.ts b/packages/query-db-collection/tests/direct-write-precedence-oracle.ts new file mode 100644 index 0000000000..ab1caab7d7 --- /dev/null +++ b/packages/query-db-collection/tests/direct-write-precedence-oracle.ts @@ -0,0 +1,29 @@ +/** + * A complete fetched result supplies untouched keys. An accepted direct write + * authored after that fetch started supplies its own key, including absence + * after deletion. A later physical fetch may replace that write. Authority: + * query-collection.md, Direct Writes and Query Sync. This is the ownership + * oracle's existing overlay judgment, shared with its persisted receivers. + * It models authored rows and writes, never Query generations or cache events. + */ +import { declareOracle } from '../../db/tests/oracle-declaration' + +export type MergeRow = { id: string; value: Value } +export type PrecedenceWrite = + MergeRow | { id: string; deleted: true } + +export function mergeDirectWritesOverFetch( + fetched: Array>, + writes: Array>, +): Record { + const rows = new Map(fetched.map((row) => [row.id, row])) + for (const row of writes) { + if (`deleted` in row) rows.delete(row.id) + else rows.set(row.id, row) + } + return Object.fromEntries( + [...rows.values()].map((row) => [row.id, row.value]), + ) +} + +declareOracle({ model: [mergeDirectWritesOverFetch] }) diff --git a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts index 72084d0978..52dc5e4866 100644 --- a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts +++ b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts @@ -30,6 +30,7 @@ import { import { InvalidQueryResultError } from '../src/errors.js' import { queryCollectionOptions } from '../src/query.js' import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { mergeDirectWritesOverFetch } from './direct-write-precedence-oracle' import { categorySubset, cleanups, @@ -1609,18 +1610,6 @@ async function runOverlappingSubsetHistory({ * wins for its key. This reference overlays the writes on the fetched rows in * order. It is the expected visible rows of the merge-cut histories below. */ -type MergeRow = { id: string; value: string } -function mergeDirectWritesOverFetch( - fetched: Array, - writes: Array, -): Record { - const rows = new Map(fetched.map((row) => [row.id, row])) - for (const row of writes) rows.set(row.id, row) - return Object.fromEntries( - [...rows.values()].map((row) => [row.id, row.value]), - ) -} - // A pending application obligation can include persistence after publication. // While its receipt is held, allow either complete source snapshot; never infer // receipt settlement merely from visible rows. Once it fulfills, the waiter diff --git a/packages/query-db-collection/tests/persisted-precedence-driver.ts b/packages/query-db-collection/tests/persisted-precedence-driver.ts new file mode 100644 index 0000000000..8e38e6f631 --- /dev/null +++ b/packages/query-db-collection/tests/persisted-precedence-driver.ts @@ -0,0 +1,277 @@ +// Real Query + persisted-wrapper receiver. Gates delay collaborator completion, +// never manufacture expected rows. Public/durable comparisons live in the owner. +import { QueryClient, hashKey } from '@tanstack/query-core' +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { + trackStartupSync, + untilStartup, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { queryCollectionOptions } from '../src/query' +import { + precedenceFetched, + precedenceLater, + precedenceSaved, + precedenceWrites, +} from './persisted-precedence-oracle' +import type { + PersistedCollectionPersistence, + PersistenceAdapter, +} from '../../db-sqlite-persistence-core/src' +import type { MergeRow } from './direct-write-precedence-oracle' +import type { + PrecedenceHistory, + PrecedenceObservation, +} from './persisted-precedence-oracle' +import type { HeldCall } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' + +type Row = MergeRow +type Response = Array | { edges: Array<{ node: Row }>; token: string } +function gate() { + let release!: () => void + const promise = new Promise((resolve) => { + release = resolve + }) + return { promise, release } +} +const snapshot = (rows: Iterable) => + Array.from(rows, (row) => ({ ...row })).sort((a, b) => + a.id.localeCompare(b.id), + ) +export async function observePersistedPrecedence( + persistence: PersistedCollectionPersistence, + h: PrecedenceHistory, + id: string, + holdBaseline?: (id: string) => HeldCall, +): Promise { + const queryHash = hashKey([id]) + const seed = createCollection( + persistedCollectionOptions({ + id, + getKey: (row) => row.id, + persistence, + sync: { + sync: ({ begin, write, commit, markReady, metadata }) => { + begin() + for (const row of precedenceSaved) { + write({ type: `insert`, value: { ...row } }) + metadata!.row.set(row.id, { + queryCollection: { owners: { [queryHash]: true } }, + }) + } + metadata!.collection.set(`queryCollection:gc:${queryHash}`, { + queryHash, + mode: `until-revalidated`, + }) + commit() + markReady() + }, + }, + }), + ) + await withOracleCleanup(() => seed.preload(), [() => seed.cleanup()]) + const client = new QueryClient({ + defaultOptions: { + queries: { retry: false, gcTime: Infinity, staleTime: Infinity }, + }, + }) + const resultGate = gate() + const scanGate = gate() + const restoreGate = gate() + const hostRestoreHold = + h.phase === `before-restore` ? holdBaseline?.(id) : undefined + let heldRestore = false + const wrapRestore = (adapter: PersistenceAdapter): PersistenceAdapter => + new Proxy(adapter, { + get(target, property) { + if (property === `loadResumeSnapshot`) + return async ( + ...args: Parameters + ) => { + if ( + h.phase === `before-restore` && + args[1]?.includeRows !== false && + !heldRestore + ) { + heldRestore = true + await restoreGate.promise + } + return target.loadResumeSnapshot(...args) + } + if (property === `runInHydrationScope` && target.runInHydrationScope) + return (task: (scoped: PersistenceAdapter) => Promise) => + target.runInHydrationScope!((scoped) => task(wrapRestore(scoped))) + const value: unknown = Reflect.get(target, property) + return typeof value === `function` ? value.bind(target) : value + }, + }) + let responseAvailable = false + let scans = 0 + let heldScan = false + let fetches = 0 + const encode = (rows: Array): Response => + h.shape === `raw` + ? rows.map((row) => ({ ...row })) + : { + edges: rows.map((row) => ({ node: { ...row } })), + token: `preserved-envelope`, + } + const decode = (response: Response) => + Array.isArray(response) + ? response + : response.edges.map((edge) => ({ ...edge.node })) + const options = queryCollectionOptions({ + id, + queryKey: [id], + queryClient: client, + queryFn: async () => { + fetches++ + if (fetches === 1 && h.origin !== `cache`) await resultGate.promise + responseAvailable = true + return encode( + fetches === 1 && h.origin !== `cache` + ? precedenceFetched + : precedenceLater, + ) + }, + select: decode, + getKey: (row: Row) => row.id, + persistedGcTime: Infinity, + enabled: h.phase !== `before-fetch` && h.origin !== `cache`, + }) + const original = options.sync.sync + const tracked = trackStartupSync({ + ...options.sync, + sync: (params) => { + const capability = params.metadata!.persistence! + return original({ + ...params, + metadata: { + ...params.metadata!, + persistence: { + ...capability, + scanPersistedRows: async (...args) => { + scans++ + if ( + h.phase === `during-baseline` && + responseAvailable && + !heldScan + ) { + // Capture first in the after-read case: a newer direct write must not + // depend on a conveniently fresh storage read to survive the fetch. + const rows = + h.scanCut === `after-read` + ? await capability.scanPersistedRows(...args) + : undefined + heldScan = true + await scanGate.promise + if (rows) return rows + } + return capability.scanPersistedRows(...args) + }, + }, + }, + }) + }, + }) + const collection = createCollection( + persistedCollectionOptions({ + ...options, + sync: tracked.sync, + persistence: { + ...persistence, + adapter: hostRestoreHold + ? persistence.adapter + : wrapRestore(persistence.adapter), + }, + }), + ) + const durable = async () => { + if (!persistence.adapter.scanRows) + throw new Error(`Persisted precedence requires row scans`) + return snapshot( + ( + await persistence.adapter.scanRows({ kind: `eager`, collectionId: id }) + ).map((row) => row.value as Row), + ) + } + return withOracleCleanup(async () => { + const preload = collection.preload() + void preload.catch(() => {}) + await untilStartup( + () => + (h.origin === `cache` || h.phase === `before-fetch` || fetches === 1) && + (h.phase === `before-restore` + ? (hostRestoreHold?.isHeld() ?? heldRestore) + : collection.has(`k`)), + ) + if (hostRestoreHold?.isHeld()) heldRestore = true + if (h.origin === `cache`) { + responseAvailable = true + client.setQueryData([id], encode(precedenceFetched)) + } + if (h.phase === `during-baseline`) { + resultGate.release() + await untilStartup(() => heldScan) + } + const applyWrites = async () => { + for (const write of precedenceWrites(h)) { + if (`deleted` in write) await collection.utils.writeDelete(write.id) + else if (h.op === `insert`) await collection.utils.writeInsert(write) + else if (h.op === `update`) await collection.utils.writeUpdate(write) + else await collection.utils.writeUpsert(write) + } + } + const written = applyWrites() + if (h.phase === `before-restore`) { + resultGate.release() + await untilStartup(() => responseAvailable) + restoreGate.release() + hostRestoreHold?.release() + } + await written + const afterWrite = + h.phase === `before-restore` ? undefined : snapshot(collection.values()) + const firstFetch = + h.phase === `before-fetch` + ? collection.utils.refetch({ throwOnError: true }) + : undefined + void firstFetch?.catch(() => {}) + resultGate.release() + scanGate.release() + await tracked.initialResult + await firstFetch + await preload + const first = snapshot(collection.values()) + const firstDurable = await durable() + const firstCache = snapshot(decode(client.getQueryData([id])!)) + let later: Array | undefined + let laterDurable: Array | undefined + if (h.laterFetch) { + later = await collection.utils + .refetch({ throwOnError: true }) + .then(() => snapshot(collection.values())) + laterDurable = await durable() + } + return { + afterWrite, + first, + firstDurable, + firstCache, + later, + laterDurable, + scans, + heldScan, + heldRestore, + fetches, + } + }, [ + resultGate.release, + scanGate.release, + restoreGate.release, + () => hostRestoreHold?.uninstall(), + () => collection.cleanup(), + () => client.clear(), + ]) +} diff --git a/packages/query-db-collection/tests/persisted-precedence-oracle.test.ts b/packages/query-db-collection/tests/persisted-precedence-oracle.test.ts new file mode 100644 index 0000000000..3aa6c81dfb --- /dev/null +++ b/packages/query-db-collection/tests/persisted-precedence-oracle.test.ts @@ -0,0 +1,170 @@ +/** + * The ownership model receives real Query and storage on both Node hosts. + * Fixed and fresh generated histories share the same driver and comparison. + * Each host/campaign independently requires each shape, held boundary, operation, + * and the later-fetch neighbor. Browser receivers import these same parts. + */ +import fc from 'fast-check' +import { describe, expect, it } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { + oracleFixedSeed, + oraclePropertyOptions, + oracleReplayTest, + oracleRoute, + oracleRouteOf, + oracleRuns, +} from '../../db/tests/oracle-config' +import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' +import { observePersistedPrecedence } from './persisted-precedence-driver' +import { + precedenceHistories, + precedenceReached, + precedenceStates, + precedenceViolations, +} from './persisted-precedence-oracle' +import type { PrecedenceHistory } from './persisted-precedence-oracle' + +const historyArb: fc.Arbitrary = fc.oneof( + fc + .tuple( + fc.record({ + shape: fc.constantFrom(`raw`, `derived`), + op: fc.constantFrom(`insert`, `update`, `delete`, `upsert`), + laterFetch: fc.boolean(), + value: fc.integer({ min: -10, max: 10 }), + }), + fc.oneof( + fc.constant({ phase: `before-result` } as const), + fc.constant({ phase: `before-fetch` } as const), + fc.record({ + phase: fc.constant(`during-baseline` as const), + scanCut: fc.constantFrom( + `before-read` as const, + `after-read` as const, + ), + }), + ), + ) + .map(([history, cut]) => ({ ...history, ...cut })), + fc.record({ + shape: fc.constantFrom(`raw` as const, `derived` as const), + phase: fc.constant(`before-restore` as const), + op: fc.constantFrom(`insert` as const, `upsert` as const), + laterFetch: fc.boolean(), + value: fc.integer({ min: -10, max: 10 }), + }), + fc.record({ + shape: fc.constantFrom(`raw` as const, `derived` as const), + origin: fc.constant(`cache` as const), + phase: fc.constant(`during-baseline` as const), + scanCut: fc.constantFrom(`before-read` as const, `after-read` as const), + op: fc.constantFrom( + `insert` as const, + `update` as const, + `delete` as const, + `upsert` as const, + ), + laterFetch: fc.boolean(), + value: fc.integer({ min: -10, max: 10 }), + }), +) +declareOracle({ + owns: [`src/query.ts#queryCollectionOptions`], + grammar: [historyArb, precedenceHistories], + check: [precedenceViolations], +}) +for (const host of [`controlled`, `node-sqlite`] as const) + describe(`persisted precedence through ${host}`, () => { + let sequence = 0 + const check = async ( + h: PrecedenceHistory, + reach: ReturnType, + ) => { + reach.beginHistory() + const storage = createStartupHost(host) + try { + const observed = await observePersistedPrecedence( + storage.persistence, + h, + `precedence-${host}-${sequence++}`, + ) + for (const state of precedenceReached(h)) reach.reach(state) + expect(precedenceViolations(h, observed)).toEqual([]) + } finally { + storage.close() + } + } + oracleReplayTest(it)(`orders fixed fetch/write histories`, async () => { + oracleRoute(`fixed history`) + const reach = createOracleReach( + `query.precedence.${host}.corpus`, + precedenceStates, + ) + for (const h of precedenceHistories) await check(h, reach) + reach.assertReached() + console.info( + `query.precedence.${host}.corpus`, + Object.fromEntries(reach.counts()), + ) + }) + it(`rejects a first-result observation that lost the newer inserted row`, async () => { + oracleRoute(`calibration`) + const h: PrecedenceHistory = { + shape: `derived`, + phase: `before-result`, + op: `insert`, + laterFetch: false, + value: 9, + } + const storage = createStartupHost(host) + try { + const observed = await observePersistedPrecedence( + storage.persistence, + h, + `precedence-calibration-${host}`, + ) + expect(precedenceViolations(h, observed)).toEqual([]) + expect( + precedenceViolations(h, { + ...observed, + first: observed.first.filter((row) => row.id !== `new`), + }).some((v) => v.startsWith(`first:`)), + ).toBe(true) + } finally { + storage.close() + } + }) + for (const campaign of [`fixed`, `random`] as const) { + const property = `query.persisted-write-precedence` + const register = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + register( + `orders generated fetch/write histories (${campaign})`, + async () => { + const parameters = + campaign === `fixed` + ? oracleFixedSeed(2394, { numRuns: oracleRuns(80) }) + : oraclePropertyOptions(80, property) + oracleRoute(oracleRouteOf(parameters)) + const reach = createOracleReach( + `query.precedence.${host}.${campaign}`, + precedenceStates, + ) + await fc.assert( + fc.asyncProperty(historyArb, (h) => check(h, reach)), + parameters, + ) + reach.assertReached() + console.info( + `query.precedence.${host}.${campaign}`, + Object.fromEntries(reach.counts()), + ) + }, + 60_000, + ) + } + }) diff --git a/packages/query-db-collection/tests/persisted-precedence-oracle.ts b/packages/query-db-collection/tests/persisted-precedence-oracle.ts new file mode 100644 index 0000000000..3bc5f8b7c4 --- /dev/null +++ b/packages/query-db-collection/tests/persisted-precedence-oracle.ts @@ -0,0 +1,200 @@ +/** + * Persisted receivers of the ownership oracle's fetch/write precedence law. + * Saved rows are source-owned and retained until revalidation. A complete + * response must remove an omitted saved row while preserving newer writes. + * Histories cross raw rows and a derived select without an inverse cache + * mapping, write before response versus before/after its held baseline read, four + * write operations, and a later physical refetch. Insert/upsert also cross an + * older result with a write authored while persisted restore is still held. + * Update/delete require their saved target to be accepted first. Values vary + * independently. These legality constraints avoid duplicate or invalid histories. + * Three saved keys, one inserted key, one sync run, no failed I/O or peer claims. + */ +import { declareOracle } from '../../db/tests/oracle-declaration' +import { mergeDirectWritesOverFetch } from './direct-write-precedence-oracle' +import type { + MergeRow, + PrecedenceWrite, +} from './direct-write-precedence-oracle' + +export type PrecedenceHistory = { + shape: `raw` | `derived` + origin?: `fetch` | `cache` + laterFetch: boolean + value: number +} & ( + | (( + | { phase: `before-result` } + | { phase: `before-fetch` } + | { + phase: `during-baseline` + scanCut: `before-read` | `after-read` + } + ) & { op: `insert` | `update` | `delete` | `upsert` }) + | { + phase: `before-restore` + op: `insert` | `upsert` + } +) +const baselineCuts = [ + { phase: `before-result` }, + { phase: `before-fetch` }, + { phase: `during-baseline`, scanCut: `before-read` }, + { phase: `during-baseline`, scanCut: `after-read` }, +] as const +export const precedenceHistories: Array = [ + ...([`raw`, `derived`] as const).flatMap((shape) => + baselineCuts.flatMap((cut) => + ([`insert`, `update`, `delete`, `upsert`] as const).flatMap((op) => + [false, true].map((laterFetch) => ({ + shape, + ...cut, + op, + laterFetch, + value: 9, + })), + ), + ), + ), + ...([`raw`, `derived`] as const).flatMap((shape) => + ([`insert`, `upsert`] as const).flatMap((op) => + [false, true].map((laterFetch) => ({ + shape, + phase: `before-restore` as const, + op, + laterFetch, + value: 9, + })), + ), + ), + ...([`raw`, `derived`] as const).flatMap((shape) => + ([`before-read`, `after-read`] as const).flatMap((scanCut) => + ([`insert`, `update`, `delete`, `upsert`] as const).flatMap((op) => + [false, true].map((laterFetch) => ({ + shape, + origin: `cache` as const, + phase: `during-baseline` as const, + scanCut, + op, + laterFetch, + value: 9, + })), + ), + ), + ), +] +export const precedenceSaved: Array> = [ + { id: `k`, value: 1 }, + { id: `z`, value: 1 }, + { id: `obsolete`, value: 1 }, +] +export const precedenceFetched: Array> = [ + { id: `k`, value: 2 }, + { id: `z`, value: 3 }, +] +export const precedenceLater: Array> = [ + { id: `k`, value: 6 }, + { id: `z`, value: 7 }, +] +export function precedenceWrites( + h: PrecedenceHistory, +): Array> { + const id = h.op === `insert` || h.op === `upsert` ? `new` : `k` + return h.op === `delete` ? [{ id, deleted: true }] : [{ id, value: h.value }] +} +export type PrecedenceObservation = { + afterWrite?: Array> + first: Array> + firstDurable: Array> + firstCache: Array> + later?: Array> + laterDurable?: Array> + scans: number + heldScan: boolean + heldRestore: boolean + fetches: number +} +const values = (rows: Array>) => + Object.fromEntries(rows.map((r) => [r.id, r.value])) +export function precedenceViolations( + h: PrecedenceHistory, + o: PrecedenceObservation, +): Array { + const failures: Array = [] + const same = ( + cut: string, + actual: Array> | undefined, + expected: Record, + ) => { + if ( + !actual || + JSON.stringify(Object.entries(values(actual)).sort()) !== + JSON.stringify(Object.entries(expected).sort()) + ) + failures.push( + `${cut}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + ) + } + const writes = precedenceWrites(h) + // With restore held, saved rows are not yet promised as a public base. Those + // histories compare the first complete applied result and its durable state; + // the other histories also compare the accepted write while the source waits. + if (h.phase !== `before-restore`) + same( + `afterWrite`, + o.afterWrite, + mergeDirectWritesOverFetch(precedenceSaved, writes), + ) + const first = mergeDirectWritesOverFetch( + precedenceFetched, + h.phase === `before-fetch` ? [] : writes, + ) + same(`first`, o.first, first) + same(`firstDurable`, o.firstDurable, first) + same( + `firstCache`, + o.firstCache, + h.shape === `raw` ? first : values(precedenceFetched), + ) + if (h.laterFetch) { + same(`later`, o.later, values(precedenceLater)) + same(`laterDurable`, o.laterDurable, values(precedenceLater)) + } + if (h.phase === `during-baseline` && !o.heldScan) + failures.push(`missing held baseline scan`) + if (h.phase === `before-restore` && !o.heldRestore) + failures.push(`missing held persisted restore`) + if (o.fetches !== (h.origin === `cache` ? 0 : 1) + (h.laterFetch ? 1 : 0)) + failures.push(`unexpected fetch count ${o.fetches}`) + return failures +} +export const precedenceStates = [ + `raw`, + `derived`, + `fetched-result`, + `cached-result`, + `before-result`, + `before-fetch`, + `before-restore`, + `during-baseline`, + `before-read`, + `after-read`, + `insert`, + `update`, + `delete`, + `upsert`, + `later-fetch`, +] +export const precedenceReached = (h: PrecedenceHistory) => [ + h.shape, + h.origin === `cache` ? `cached-result` : `fetched-result`, + h.phase, + ...(h.phase === `during-baseline` ? [h.scanCut] : []), + h.op, + ...(h.laterFetch ? [`later-fetch`] : []), +] +declareOracle({ + model: [mergeDirectWritesOverFetch], + grammar: [precedenceHistories], + check: [precedenceViolations], +}) diff --git a/packages/query-db-collection/tests/retained-scoped-hydration-driver.ts b/packages/query-db-collection/tests/retained-scoped-hydration-driver.ts new file mode 100644 index 0000000000..43427f6cdd --- /dev/null +++ b/packages/query-db-collection/tests/retained-scoped-hydration-driver.ts @@ -0,0 +1,319 @@ +/** + * Real Query / persisted-wrapper receiving path. The first instance acquires + * both scopes and persists retention through cleanup. A fresh QueryClient then + * acquires only one scope. A response gate supplies the pending observation; + * fulfillment callbacks capture the public answer before a later load can + * repair it. Capability counters describe execution and impose no call law. + */ +import { QueryClient } from '@tanstack/query-core' +import { IR, createCollection, getLoadSubsetDemandKey } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { queryCollectionOptions } from '../src/query' +import { + otherRetainedScope, + scopedHydrationPage, + scopedHydrationRows, + sortScopedRows, +} from './retained-scoped-hydration-oracle' +import type { LoadSubsetOptions } from '@tanstack/db' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' +import type { + CachedPeerClaimHistory, + CachedPeerClaimObservation, + RetainedScope, + ScopedHydrationHistory, + ScopedHydrationObservation, + ScopedHydrationRow, +} from './retained-scoped-hydration-oracle' + +export const retainedScopeDemand = ( + scope: RetainedScope, +): LoadSubsetOptions => ({ + where: new IR.Func(`eq`, [new IR.PropRef([`scope`]), new IR.Value(scope)]), +}) +export function requestedRetainedScope( + options: LoadSubsetOptions, +): RetainedScope { + const where = options.where + if ( + where?.type !== `func` || + where.name !== `eq` || + where.args[0]?.type !== `ref` || + where.args[0].path[0] !== `scope` || + where.args[1]?.type !== `val` || + (where.args[1].value !== `a` && where.args[1].value !== `b`) + ) + throw new Error(`Scoped hydration fixture requires one scope equality`) + return where.args[1].value +} + +/** Overlapping Query claims receive one continuously owned keyed row. */ +export async function observeCachedPeerClaim( + persistence: PersistedCollectionPersistence, + h: CachedPeerClaimHistory, + id: string, + sourceFactory = queryCollectionOptions, +): Promise { + const client = new QueryClient({ + defaultOptions: { + queries: { retry: false, staleTime: Infinity, gcTime: Infinity }, + }, + }) + const broad = retainedScopeDemand(`a`) + const narrow = { ...broad, limit: 1 } + const narrowKey = [id, getLoadSubsetDemandKey(narrow)] + let calls = 0 + let durableValue!: () => Promise + const row = (value: number): ScopedHydrationRow => ({ + id: `shared`, + scope: `a`, + value, + }) + const options = sourceFactory({ + id, + queryKey: [id], + queryClient: client, + syncMode: `on-demand`, + startSync: true, + getKey: (value: ScopedHydrationRow) => value.id, + queryFn: () => Promise.resolve([row(++calls === 1 ? h.current : h.later)]), + }) + const collection = createCollection( + persistedCollectionOptions({ + ...options, + persistence, + sync: { + ...options.sync, + sync: (params) => { + durableValue = async () => { + const saved = ( + await params.metadata!.persistence!.scanPersistedRows() + ).find((entry) => entry.key === `shared`)?.value as + ScopedHydrationRow | undefined + return saved?.value + } + return options.sync.sync(params) + }, + }, + }), + ) + return withOracleCleanup(async () => { + await collection.preload() + if (h.cache === `older-fetch`) + await client.fetchQuery({ + queryKey: narrowKey, + queryFn: () => Promise.resolve([row(h.cached)]), + }) + await collection._sync.loadSubset(broad) + const before = collection.get(`shared`)?.value + if (h.cache === `manual`) client.setQueryData(narrowKey, [row(h.cached)]) + const claimed = await Promise.resolve( + collection._sync.loadSubset(narrow), + ).then(() => collection.get(`shared`)?.value) + const claimedDurable = await durableValue() + const callsBeforeRefetch = calls + const refetched = await Promise.resolve( + collection._sync.loadSubset({ ...narrow, refetch: true }), + ).then(() => collection.get(`shared`)?.value) + const refetchedDurable = await durableValue() + return { + before, + claimed, + claimedDurable, + refetched, + refetchedDurable, + callsBeforeRefetch, + calls, + } + }, [() => collection.cleanup(), () => client.clear()]) +} +export async function observeRetainedScopedHydration( + persistence: PersistedCollectionPersistence, + h: ScopedHydrationHistory, + id: string, + sourceFactory = queryCollectionOptions, +): Promise { + const makeClient = () => + new QueryClient({ + defaultOptions: { + queries: { retry: false, staleTime: Infinity, gcTime: Infinity }, + }, + }) + const other = otherRetainedScope(h.first) + const seedFirstDemand = retainedScopeDemand(h.first) + const firstDemand = + h.window === `all` + ? seedFirstDemand + : { + ...seedFirstDemand, + orderBy: [ + { + expression: new IR.PropRef([`id`]), + compareOptions: { + direction: `desc` as const, + nulls: `last` as const, + stringSort: `lexical` as const, + }, + }, + ], + limit: 1, + } + const otherDemand = retainedScopeDemand(other) + const seedClient = makeClient() + const seedReceipts: Array> = [] + const seedOptions = queryCollectionOptions({ + id, + queryKey: [id], + queryClient: seedClient, + syncMode: `on-demand`, + startSync: true, + persistedGcTime: Infinity, + getKey: (row: ScopedHydrationRow) => row.id, + queryFn: (context) => { + const scope = requestedRetainedScope( + context.meta?.loadSubsetOptions ?? {}, + ) + return Promise.resolve( + scopedHydrationRows( + scope, + scope === h.first ? h.previous : h.otherPrevious, + 1, + ), + ) + }, + }) + const seed = createCollection( + persistedCollectionOptions({ + ...seedOptions, + sync: { + ...seedOptions.sync, + sync: (params) => + seedOptions.sync.sync({ + ...params, + commit: (...args) => { + const receipt = params.commit(...args) + seedReceipts.push(receipt) + return receipt + }, + }), + }, + persistence, + }), + ) + await withOracleCleanup(async () => { + await seed._sync.loadSubset(seedFirstDemand) + await seed._sync.loadSubset(otherDemand) + if (h.ownerless !== `none`) + await seed.utils.writeInsert({ + id: `saved-direct`, + scope: h.ownerless === `first` ? h.first : other, + value: 1, + }) + seed._sync.unloadSubset(seedFirstDemand) + seed._sync.unloadSubset(otherDemand) + await Promise.all(seedReceipts) + }, [() => seed.cleanup(), () => seedClient.clear()]) + + const client = makeClient() + let release!: () => void + const responseGate = new Promise((resolve) => { + release = resolve + }) + let fetches = 0 + let retainedScopes = 0 + let hydrateCalls = 0 + let scanCalls = 0 + let durableRows: (() => Promise>) | undefined + const options = sourceFactory({ + id, + queryKey: [id], + queryClient: client, + syncMode: `on-demand`, + startSync: true, + persistedGcTime: Infinity, + getKey: (row: ScopedHydrationRow) => row.id, + queryFn: async (context) => { + const scope = requestedRetainedScope( + context.meta?.loadSubsetOptions ?? {}, + ) + fetches++ + if (fetches === 1) await responseGate + return scopedHydrationPage( + scopedHydrationRows( + scope, + scope === h.first ? h.next : h.otherNext, + h.value, + ), + scope === h.first ? h.window : `all`, + ) + }, + }) + const original = options.sync.sync + const collection = createCollection( + persistedCollectionOptions({ + ...options, + persistence, + sync: { + ...options.sync, + sync: (params) => { + retainedScopes = + params.metadata!.collection.list(`queryCollection:gc:`).length + const capability = params.metadata!.persistence! + // The capability resolves the managed generation that this run claimed. + // Diagnostic reads bypass the counter for source reconciliation calls. + durableRows = async () => + sortScopedRows( + (await capability.scanPersistedRows()).map( + (row) => row.value as ScopedHydrationRow, + ), + ) + return original({ + ...params, + metadata: { + ...params.metadata!, + persistence: { + ...capability, + hydrateBaseline: () => { + hydrateCalls++ + return capability.hydrateBaseline() + }, + scanPersistedRows: (...args) => { + scanCalls++ + return capability.scanPersistedRows(...args) + }, + }, + }, + }) + }, + }, + }), + ) + return withOracleCleanup(async () => { + const firstLoad = Promise.resolve( + collection._sync.loadSubset(firstDemand), + ).then(() => sortScopedRows(collection.values())) + void firstLoad.catch(() => {}) + await untilStartup(() => fetches === 1) + const pending = sortScopedRows(collection.values()) + release() + const first = await firstLoad + const firstDurable = await durableRows!() + const second = await Promise.resolve( + collection._sync.loadSubset(otherDemand), + ).then(() => sortScopedRows(collection.values())) + const secondDurable = await durableRows!() + return { + pending, + first, + firstDurable, + second, + secondDurable, + retainedScopes, + fetches, + hydrateCalls, + scanCalls, + } + }, [release, () => collection.cleanup(), () => client.clear()]) +} diff --git a/packages/query-db-collection/tests/retained-scoped-hydration-oracle.test.ts b/packages/query-db-collection/tests/retained-scoped-hydration-oracle.test.ts new file mode 100644 index 0000000000..b90f241188 --- /dev/null +++ b/packages/query-db-collection/tests/retained-scoped-hydration-oracle.test.ts @@ -0,0 +1,230 @@ +/** The scope law receives controlled storage and the real SQLite demand path. */ +import fc from 'fast-check' +import { describe, expect, it } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { + oracleFixedSeed, + oraclePropertyOptions, + oracleReplayTest, + oracleRoute, + oracleRouteOf, + oracleRuns, +} from '../../db/tests/oracle-config' +import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' +import { + observeCachedPeerClaim, + observeRetainedScopedHydration, + requestedRetainedScope, +} from './retained-scoped-hydration-driver' +import { + cachedPeerClaimHistories, + cachedPeerClaimHistoryArb, + cachedPeerClaimReached, + cachedPeerClaimStates, + cachedPeerClaimViolations, + scopedHydrationHistories, + scopedHydrationHistoryArb, + scopedHydrationReached, + scopedHydrationStates, + scopedHydrationViolations, +} from './retained-scoped-hydration-oracle' +import type { + CachedPeerClaimHistory, + ScopedHydrationHistory, +} from './retained-scoped-hydration-oracle' +import type { PersistenceAdapter } from '../../db-sqlite-persistence-core/src' + +declareOracle({ + owns: [`src/query.ts#queryCollectionOptions`], + grammar: [ + scopedHydrationHistoryArb, + scopedHydrationHistories, + cachedPeerClaimHistoryArb, + cachedPeerClaimHistories, + ], + check: [scopedHydrationViolations, cachedPeerClaimViolations], +}) +for (const host of [`controlled`, `node-sqlite`] as const) + describe(`retained scoped hydration through ${host}`, () => { + let sequence = 0 + async function check( + h: ScopedHydrationHistory, + reach?: ReturnType, + ) { + const storage = createStartupHost(host) + if (host === `controlled`) { + // The controlled persistence adapter supplies the same scope predicate + // that real SQLite evaluates. It returns stored facts, not model rows. + const adapter: PersistenceAdapter = storage.persistence.adapter + const loadSubset = adapter.loadSubset + adapter.loadSubset = async (target, options, context) => { + const entries = await loadSubset(target, options, context) + if (!options.where) return entries + const scope = requestedRetainedScope(options) + const matching = entries.filter((row) => row.value.scope === scope) + if (options.limit === undefined) return matching + return matching + .sort((a, b) => String(b.key).localeCompare(String(a.key))) + .slice(0, options.limit) + } + } + return withOracleCleanup(async () => { + reach?.beginHistory() + const actual = await observeRetainedScopedHydration( + storage.persistence, + h, + `scoped-${host}-${sequence++}`, + ) + for (const state of scopedHydrationReached(h)) reach?.reach(state) + expect( + scopedHydrationViolations(h, actual), + JSON.stringify({ h, actual }), + ).toEqual([]) + return actual + }, [() => storage.close()]) + } + oracleReplayTest(it)( + `revalidates only the acquired retained scope`, + async () => { + oracleRoute(`fixed history`) + const reach = createOracleReach( + `query.scoped-hydration.${host}.histories`, + [...scopedHydrationStates], + ) + for (const h of scopedHydrationHistories) await check(h, reach) + reach.assertReached() + process.stdout.write( + `query.scoped-hydration.${host}.histories ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }, + 60_000, + ) + oracleReplayTest(it)( + `rejects a full-store restore that publishes unrequested saved rows`, + async () => { + oracleRoute(`calibration`) + const h = scopedHydrationHistories[0]! + const actual = await check(h) + expect( + scopedHydrationViolations(h, { + ...actual, + first: actual.firstDurable, + }).some((v) => v.startsWith(`first:`)), + ).toBe(true) + }, + ) + for (const campaign of [`fixed`, `random`] as const) { + const property = `query.retained-scoped-hydration` + const register = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + register( + `revalidates generated retained scopes (${campaign})`, + async () => { + const parameters = + campaign === `fixed` + ? oracleFixedSeed(1481, { numRuns: oracleRuns(60) }) + : oraclePropertyOptions(60, property) + oracleRoute(oracleRouteOf(parameters)) + const reach = createOracleReach( + `query.scoped-hydration.${host}.${campaign}`, + [...scopedHydrationStates], + ) + await fc.assert( + fc.asyncProperty(scopedHydrationHistoryArb, async (h) => { + await check(h, reach) + }), + parameters, + ) + reach.assertReached() + process.stdout.write( + `query.scoped-hydration.${host}.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }, + 120_000, + ) + } + async function checkCached( + h: CachedPeerClaimHistory, + reach?: ReturnType, + ) { + const storage = createStartupHost(host) + return withOracleCleanup(async () => { + reach?.beginHistory() + const actual = await observeCachedPeerClaim( + storage.persistence, + h, + `cached-claim-${host}-${sequence++}`, + ) + for (const state of cachedPeerClaimReached(h)) reach?.reach(state) + expect( + cachedPeerClaimViolations(h, actual), + JSON.stringify({ h, actual }), + ).toEqual([]) + return actual + }, [() => storage.close()]) + } + oracleReplayTest(it)( + `keeps a peer value when a cached Query first claims it`, + async () => { + oracleRoute(`fixed history`) + const reach = createOracleReach( + `query.cached-claim.${host}.histories`, + [...cachedPeerClaimStates], + ) + for (const h of cachedPeerClaimHistories) await checkCached(h, reach) + reach.assertReached() + process.stdout.write( + `query.cached-claim.${host}.histories ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }, + ) + oracleReplayTest(it)( + `rejects stale cache overwrite at first claim`, + async () => { + oracleRoute(`calibration`) + const h = cachedPeerClaimHistories[0]! + const actual = await checkCached(h) + expect( + cachedPeerClaimViolations(h, { ...actual, claimed: h.cached }).some( + (v) => v.startsWith(`claimed:`), + ), + ).toBe(true) + }, + ) + for (const campaign of [`fixed`, `random`] as const) { + const property = `query.cached-peer-claim` + const register = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + register( + `orders generated cached peer claims (${campaign})`, + async () => { + const parameters = + campaign === `fixed` + ? oracleFixedSeed(1376, { numRuns: oracleRuns(60) }) + : oraclePropertyOptions(60, property) + oracleRoute(oracleRouteOf(parameters)) + const reach = createOracleReach( + `query.cached-claim.${host}.${campaign}`, + [...cachedPeerClaimStates], + ) + await fc.assert( + fc.asyncProperty(cachedPeerClaimHistoryArb, async (h) => { + await checkCached(h, reach) + }), + parameters, + ) + reach.assertReached() + process.stdout.write( + `query.cached-claim.${host}.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }, + 120_000, + ) + } + }) diff --git a/packages/query-db-collection/tests/retained-scoped-hydration-oracle.ts b/packages/query-db-collection/tests/retained-scoped-hydration-oracle.ts new file mode 100644 index 0000000000..4ffadd762b --- /dev/null +++ b/packages/query-db-collection/tests/retained-scoped-hydration-oracle.ts @@ -0,0 +1,291 @@ +/** + * An on-demand persisted Collection restores active demand, rather than every + * saved row. Revalidating one retained Query cannot publish an unrequested + * scope. Authority: SQLite persistence guide, "Persist a collection" (on-demand + * demand restore), and Query "QueryFn and Predicate Push-Down" / "Direct Writes + * and Query Sync" (exact-scope revalidation and scope-local omission). + * + * The model is set selection, independent of Query owners, hydration queues, + * and persistence scans. Two disjoint authored scopes contain two keys each. + * A saved ownerless direct row may belong to either scope. A later complete + * response removes an omitted direct row only in its own scope. The unrequested + * scope stays durable and becomes public only when its own demand is acquired. + * A one-row ordered demand cannot prove omission of an ownerless direct row; + * nor can it retire ownership of the retained unbounded Query. + * + * Limits: one cold reopen, two disjoint scopes, unbounded or one-row demand, + * numeric values, one + * persisted writer, and successful I/O. No peer writer, overlapping predicate, + * cache rotation, or intermediate-publication claim beyond the held response. + */ +import fc from 'fast-check' +import { declareOracle } from '../../db/tests/oracle-declaration' + +export type RetainedScope = `a` | `b` +export type ScopedHydrationRow = { + id: string + scope: RetainedScope + value: number +} +export type ScopedHydrationHistory = { + first: RetainedScope + window: `all` | `one` + previous: number + otherPrevious: number + next: number + otherNext: number + ownerless: `none` | `first` | `other` + value: number +} +export const otherRetainedScope = (scope: RetainedScope): RetainedScope => + scope === `a` ? `b` : `a` +export const scopedHydrationRows = ( + scope: RetainedScope, + mask: number, + value: number, +): Array => + [0, 1].flatMap((index) => + mask & (1 << index) ? [{ id: `${scope}${index}`, scope, value }] : [], + ) +export function scopedSavedRows( + h: ScopedHydrationHistory, +): Array { + return [ + ...scopedHydrationRows(h.first, h.previous, 1), + ...scopedHydrationRows(otherRetainedScope(h.first), h.otherPrevious, 1), + ...(h.ownerless === `none` + ? [] + : [ + { + id: `saved-direct`, + scope: + h.ownerless === `first` ? h.first : otherRetainedScope(h.first), + value: 1, + }, + ]), + ] +} +export const sortScopedRows = (rows: Iterable) => + [...rows] + .map(({ id, scope, value }) => ({ id, scope, value })) + .sort((a, b) => a.id.localeCompare(b.id)) +export const scopedHydrationPage = ( + rows: Array, + window: `all` | `one`, +) => (window === `all` ? rows : sortScopedRows(rows).reverse().slice(0, 1)) +export type ScopedHydrationObservation = { + pending: Array + first: Array + firstDurable: Array + second: Array + secondDurable: Array + retainedScopes: number + fetches: number + hydrateCalls: number + scanCalls: number +} +export function expectedScopedHydration(h: ScopedHydrationHistory) { + const saved = scopedSavedRows(h) + const fetched = scopedHydrationPage( + scopedHydrationRows(h.first, h.next, h.value), + h.window, + ) + const pending = scopedHydrationPage( + saved.filter((row) => row.scope === h.first), + h.window, + ) + const overlay = ( + before: Array, + after: Array, + ) => + sortScopedRows( + new Map([...before, ...after].map((row) => [row.id, row])).values(), + ) + const first = h.window === `all` ? fetched : overlay(pending, fetched) + const other = scopedHydrationRows( + otherRetainedScope(h.first), + h.otherNext, + h.value, + ) + return { + pending: sortScopedRows(pending), + first: sortScopedRows(first), + firstDurable: + h.window === `all` + ? sortScopedRows([ + ...first, + ...saved.filter((row) => row.scope !== h.first), + ]) + : overlay(saved, fetched), + second: sortScopedRows([...first, ...other]), + secondDurable: + h.window === `all` + ? sortScopedRows([...first, ...other]) + : overlay( + saved.filter((row) => row.scope === h.first), + [...fetched, ...other], + ), + } +} +export function scopedHydrationViolations( + h: ScopedHydrationHistory, + actual: ScopedHydrationObservation, +): Array { + const expected = expectedScopedHydration(h) + const violations: Array = [] + for (const cut of [ + `pending`, + `first`, + `firstDurable`, + `second`, + `secondDurable`, + ] as const) + if (JSON.stringify(actual[cut]) !== JSON.stringify(expected[cut])) + violations.push( + `${cut}: expected ${JSON.stringify(expected[cut])}, got ${JSON.stringify(actual[cut])}`, + ) + if (actual.retainedScopes !== 2) + violations.push(`two retained scopes not reached`) + if (actual.fetches !== 2) + violations.push(`expected two exact-scope fetches, got ${actual.fetches}`) + return violations +} +export const scopedHydrationHistoryArb: fc.Arbitrary = + fc.record({ + first: fc.constantFrom(`a`, `b`), + window: fc.constantFrom(`all`, `one`), + previous: fc.integer({ min: 0, max: 3 }), + otherPrevious: fc.integer({ min: 0, max: 3 }), + next: fc.integer({ min: 0, max: 3 }), + otherNext: fc.integer({ min: 0, max: 3 }), + ownerless: fc.constantFrom(`none`, `first`, `other`), + value: fc.integer({ min: -10, max: 10 }), + }) +export const scopedHydrationHistories: Array = ( + [`a`, `b`] as const +).flatMap((first) => + ([`all`, `one`] as const).flatMap((window) => + ([`none`, `first`, `other`] as const).flatMap((ownerless) => + [0, 3].flatMap((previous) => + [0, 1].map((next) => ({ + first, + window, + previous, + otherPrevious: 3, + next, + otherNext: 2, + ownerless, + value: 9, + })), + ), + ), + ), +) +export const scopedHydrationStates = [ + `first-a`, + `first-b`, + `empty-result`, + `nonempty-result`, + `unrequested-saved-rows`, + `omitted-owned-row`, + `ownerless-first`, + `ownerless-other`, + `no-ownerless`, + `unbounded`, + `limited`, + `limited-ownerless-restored`, +] as const +export function scopedHydrationReached( + h: ScopedHydrationHistory, +): Array { + return [ + `first-${h.first}`, + h.window === `all` ? `unbounded` : `limited`, + h.next ? `nonempty-result` : `empty-result`, + ...(h.otherPrevious ? [`unrequested-saved-rows`] : []), + ...(h.previous & ~h.next ? [`omitted-owned-row`] : []), + h.ownerless === `none` ? `no-ownerless` : `ownerless-${h.ownerless}`, + ...(h.window === `one` && h.ownerless === `first` + ? [`limited-ownerless-restored`] + : []), + ] +} +declareOracle({ + model: [expectedScopedHydration], + grammar: [scopedHydrationHistoryArb, scopedHydrationHistories], + check: [scopedHydrationViolations], +}) + +/** + * A newly acquired cached Query does not override a continuously owned row + * established by a later physical fetch. A manual cache edit establishes no + * fetch order at all. A later physical refetch of the new Query may replace + * that value. Authority: Query "Direct Writes and Query Sync", per-key fetch + * order and cache edits without server-result authority. This is the primary + * ownership oracle's first-claim law at the real persisted boundary. + */ +export type CachedPeerClaimHistory = { + cache: `manual` | `older-fetch` + cached: number + current: number + later: number +} +export type CachedPeerClaimObservation = { + before: number | undefined + claimed: number | undefined + claimedDurable: number | undefined + refetched: number | undefined + refetchedDurable: number | undefined + callsBeforeRefetch: number + calls: number +} +export const cachedPeerClaimHistoryArb: fc.Arbitrary = + fc.record({ + cache: fc.constantFrom(`manual`, `older-fetch`), + cached: fc.integer({ min: -10, max: 10 }), + current: fc.integer({ min: -10, max: 10 }), + later: fc.integer({ min: -10, max: 10 }), + }) +export const cachedPeerClaimHistories: Array = ( + [`manual`, `older-fetch`] as const +).map((cache) => ({ cache, cached: 1, current: 2, later: 3 })) +export const cachedPeerClaimStates = [ + `manual`, + `older-fetch`, + `different-cached-value`, + `later-value-change`, +] as const +export const cachedPeerClaimReached = (h: CachedPeerClaimHistory) => [ + h.cache, + ...(h.cached !== h.current ? [`different-cached-value`] : []), + ...(h.current !== h.later ? [`later-value-change`] : []), +] +export function cachedPeerClaimViolations( + h: CachedPeerClaimHistory, + actual: CachedPeerClaimObservation, +): Array { + const expected = { + before: h.current, + claimed: h.current, + claimedDurable: h.current, + refetched: h.later, + refetchedDurable: h.later, + } + const violations: Array = [] + for (const cut of [ + `before`, + `claimed`, + `claimedDurable`, + `refetched`, + `refetchedDurable`, + ] as const) + if (actual[cut] !== expected[cut]) + violations.push(`${cut}: expected ${expected[cut]}, got ${actual[cut]}`) + if (actual.callsBeforeRefetch !== 1 || actual.calls !== 2) + violations.push(`cached claim started an unexpected physical fetch`) + return violations +} +declareOracle({ + grammar: [cachedPeerClaimHistoryArb, cachedPeerClaimHistories], + check: [cachedPeerClaimViolations], +}) diff --git a/packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts b/packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts new file mode 100644 index 0000000000..e67b31d877 --- /dev/null +++ b/packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts @@ -0,0 +1,14 @@ +/** RxDB receives complete eager reload replacement through its real local SDK. */ +import { declareOracle } from '../../db/tests/oracle-declaration' +import { eagerReloadHistoryArb } from '../../db-adapter-conformance/tests/eager-reload-oracle' +import { registerEagerReloadConformance } from '../../db-adapter-conformance/tests/eager-reload-oracle-suite' +import { rxdbEagerReloadAdapter } from './eager-reload-driver' + +declareOracle({ + owns: [`src/rxdb.ts#rxdbCollectionOptions`], + grammar: [eagerReloadHistoryArb], +}) +registerEagerReloadConformance( + `adapter.rxdb.eager-reload`, + rxdbEagerReloadAdapter, +) diff --git a/packages/rxdb-db-collection/tests/eager-reload-driver.ts b/packages/rxdb-db-collection/tests/eager-reload-driver.ts new file mode 100644 index 0000000000..4348167278 --- /dev/null +++ b/packages/rxdb-db-collection/tests/eager-reload-driver.ts @@ -0,0 +1,52 @@ +/** Real local RxDB documents receive the complete-result reload judgment. */ +import { createRxDatabase } from 'rxdb/plugins/core' +import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' +import { rxdbCollectionOptions } from '../src/rxdb' +import type { RxCollection } from 'rxdb/plugins/core' +import type { StartupRow } from '../../db-adapter-conformance/tests/persisted-startup-oracle' +import type { EagerReloadAdapter } from '../../db-adapter-conformance/tests/eager-reload-driver' + +let sequence = 0 +export const rxdbEagerReloadAdapter: EagerReloadAdapter = { + async open(initial, id) { + const database = await createRxDatabase<{ rows: RxCollection }>( + { + name: `eagerreload${sequence++}`, + storage: getRxStorageMemory(), + multiInstance: false, + }, + ) + const { rows } = await database.addCollections({ + rows: { + schema: { + version: 0, + type: `object`, + primaryKey: `id`, + properties: { + id: { type: `string`, maxLength: 20 }, + value: { type: `number` }, + }, + required: [`id`, `value`], + }, + }, + }) + const replace = async (next: Array) => { + const docs = await rows.find().exec() + if (docs.length) await rows.bulkRemove(docs.map((row) => row.id)) + const inserted = await rows.bulkInsert(next) + if (inserted.error.length) + throw new Error(`RxDB reload fixture insert failed`) + } + await replace(initial) + return { + options: rxdbCollectionOptions({ + id, + rxCollection: rows, + syncMode: `eager`, + syncBatchSize: 1, + }), + replace, + dispose: () => database.remove(), + } + }, +} diff --git a/packages/rxdb-db-collection/tests/persisted-capability-conformance-oracle.test.ts b/packages/rxdb-db-collection/tests/persisted-capability-conformance-oracle.test.ts new file mode 100644 index 0000000000..ac9c321aaa --- /dev/null +++ b/packages/rxdb-db-collection/tests/persisted-capability-conformance-oracle.test.ts @@ -0,0 +1,15 @@ +/** Package-local ownership makes the real protocol receiver visible to gap audits. */ +import { declareOracle } from '../../db/tests/oracle-declaration' +import { registerCapabilityConformance } from '../../db-adapter-conformance/tests/persisted-capability-oracle-suite' +import { + capabilityHistories, + capabilityViolations, +} from '../../db-adapter-conformance/tests/persisted-capability-oracle' +import { rxdbStartupAdapter } from './persisted-startup-driver' + +declareOracle({ + owns: [`src/rxdb.ts#rxdbCollectionOptions`], + check: [capabilityViolations], + grammar: [capabilityHistories], +}) +registerCapabilityConformance(`rxdb`, rxdbStartupAdapter) diff --git a/packages/trailbase-db-collection/tests/ORACLE.md b/packages/trailbase-db-collection/tests/ORACLE.md index dbab31084d..d3fb302c58 100644 --- a/packages/trailbase-db-collection/tests/ORACLE.md +++ b/packages/trailbase-db-collection/tests/ORACLE.md @@ -255,6 +255,54 @@ coverage or a proof of completeness. The 10× campaign passed 800 generated histories (fixed campaign seed 714203, random campaign seed 127535183) plus the 32 corpus cases. No service-backed E2E run is claimed. +## Persisted hydration and retired work + +The primary lifecycle runner also receives real persistence through +`lifecycle-hydration-driver.ts`. Its directly named +`lifecycle-hydration-oracle.ts` projects the same cleanup law onto public and +durable rows: cleanup clears public rows and retires source authority; a new +eager result replaces saved rows; late old work changes neither observation. +This projection preserves row provenance only until the replacement. It does +not reproduce the adapter's cancellation flags, wrapper queues, or core fences. + +The driver calls the real old run's `hydrateBaseline()` and holds its completion +after hydration finishes. A complete old list then waits at the exact await +before its omission sweep. A neighboring history instead rejects an old list +after cleanup, so it does not claim hydration reach. Old work resumes while the +Collection is cleaned up, after a new run restores rows but still holds its +list, or after the new complete result is ready. Each history then supplies a +new change event to prove the replacement reader remains usable. + +The grammar varies all subsets of three string keys for saved, old-listed, and +replacement rows. A rejected old list has no payload, encoded as old mask 0. +It crosses empty and nonempty replacements, overlap and +omission, both old outcomes, all three resume cuts, and default versus +network-first rendering. Network-first uses a deadline beyond this controlled +history. Under the SQLite guide's "Render after SQLite restore" contract, +both forms stay loading before adapter readiness here; cleanup rejects the +unfinished old preload with `AbortError`. + +Twenty-four fixed histories and 50-history fixed-seed/fresh campaigns per host +run the same independent judgment through controlled storage and real Node +SQLite. Each campaign requires all nine named states. Public rows, durable +rows, and status are compared immediately after old work resumes, at replacement +readiness, and after the new event. The same judgment also requires old-reader +cancel/release, continued replacement ownership, final replacement release, and +append-only errors through cleanup. Obsolete `begin` attempts are diagnostics, +not a zero-work product promise. Browser receiving code imports the same model, +fixed histories, driver, and comparison. + +Use property ids `trailbase.lifecycle.persisted-hydration.controlled` and +`trailbase.lifecycle.persisted-hydration.node-sqlite` with the same checked replay +runner above. The fixed campaign seed is 728159. The +[hydration-gap record](../../../docs/contributing/oracle-reviews/2026-11-10-trailbase-hydration-gap.md) +records comparison results and the exact M150 cancellation-guard experiment. +That edit is reached but equivalent in the tested persisted composition: +old callbacks attempt a sync transaction after cleanup, while the wrapper and +core deny them publication authority. The shared row-loss calibration rejects +a changed replacement observation. This result does not add a promise of zero +internal work after cancellation. + ## Shared conformance driver `eager-live-source-conformance-oracle.property.test.ts` runs the shared eager diff --git a/packages/trailbase-db-collection/tests/lifecycle-hydration-driver.ts b/packages/trailbase-db-collection/tests/lifecycle-hydration-driver.ts new file mode 100644 index 0000000000..d49871c46e --- /dev/null +++ b/packages/trailbase-db-collection/tests/lifecycle-hydration-driver.ts @@ -0,0 +1,285 @@ +/** + * Real TrailBase sync and persistence receiving path for the primary lifecycle + * oracle's retired-work judgment. RecordApi supplies a native stream and gated + * list per sync run. The old baseline capability calls real hydration first, + * then withholds its completion. Cleanup therefore occurs inside the exact + * await added by persisted startup, without holding a database writer lock. + * The replacement can restore, list, become ready, and process a new event. + */ +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { trailBaseCollectionOptions } from '../src/trailbase' +import { hydrationLifecycleRows } from './lifecycle-hydration-oracle' +import type { Event, ListResponse, RecordApi } from 'trailbase' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' +import type { + HydrationLifecycleHistory, + HydrationLifecycleObservation, + HydrationLifecycleRow, + HydrationLifecycleSnapshot, +} from './lifecycle-hydration-oracle' + +function gate() { + let resolve!: (value: T) => void + let reject!: (error: unknown) => void + const promise = new Promise((yes, no) => { + resolve = yes + reject = no + }) + void promise.catch(() => undefined) + return { resolve, reject, promise } +} +const turn = () => new Promise((resolve) => setTimeout(resolve, 0)) +async function until(predicate: () => boolean) { + for (let attempt = 0; attempt < 100; attempt++) { + if (predicate()) return + await turn() + } + throw new Error(`Lifecycle receiving checkpoint was not reached`) +} +function providerSource() { + let controller!: ReadableStreamDefaultController + let cancellations = 0 + const stream = new ReadableStream({ + start(value) { + controller = value + }, + cancel() { + cancellations++ + }, + }) + return { + stream, + get controller() { + return controller + }, + get cancellations() { + return cancellations + }, + list: gate>(), + calls: 0, + } +} + +export async function observeHydrationLifecycle( + persistence: PersistedCollectionPersistence, + h: HydrationLifecycleHistory, + id: string, + sourceFactory = trailBaseCollectionOptions, +): Promise { + const seed = createCollection( + persistedCollectionOptions({ + id, + getKey: (row: HydrationLifecycleRow) => row.id, + persistence, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (const row of hydrationLifecycleRows(h.previous, 1)) + write({ type: `insert`, value: row }) + commit() + markReady() + }, + }, + }), + ) + await withOracleCleanup(() => seed.preload(), [() => seed.cleanup()]) + + const old = providerSource() + const replacement = providerSource() + let active = old + const api = { + subscribe: () => Promise.resolve(active.stream), + list: () => { + active.calls++ + return active.list.promise + }, + } as unknown as RecordApi + const options = sourceFactory({ + id, + recordApi: api, + getKey: (row: HydrationLifecycleRow) => row.id, + parse: {}, + serialize: {}, + syncMode: `eager`, + }) + const oldHydration = gate() + const reached: Array = [] + let held = false + let retired = false + let obsoleteBegins = 0 + let run = 0 + let currentHydrate: (() => Promise) | undefined + const receipts: Array | true> = [] + const reports: Array = [] + const detached = (error: unknown) => { + reports.push(`detached rejection: ${String(error)}`) + } + const browserDetached = (event: PromiseRejectionEvent) => + detached(event.reason) + const nodeEvents = + typeof process !== `undefined` && typeof process.on === `function` + if (nodeEvents) process.on(`unhandledRejection`, detached) + else globalThis.addEventListener(`unhandledrejection`, browserDetached) + const report = console.error + console.error = (...args: Array) => + reports.push(args.map(String).join(` `)) + const collection = createCollection( + persistedCollectionOptions({ + ...options, + persistence, + ...(h.initialRender === `network-first` + ? { + initialRender: { + strategy: `network-first` as const, + networkTimeoutMs: 60_000, + }, + } + : {}), + sync: { + ...options.sync, + sync: (params) => { + const oldRun = run++ === 0 + const capability = params.metadata!.persistence! + if (!oldRun) currentHydrate = () => capability.hydrateBaseline() + return options.sync.sync({ + ...params, + begin: (...args) => { + if (oldRun && retired) obsoleteBegins++ + return params.begin(...args) + }, + commit: (...args) => { + const receipt = params.commit(...args) + receipts.push(receipt) + return receipt + }, + metadata: { + ...params.metadata!, + persistence: { + ...capability, + hydrateBaseline: async () => { + await capability.hydrateBaseline() + if (oldRun) { + held = true + await oldHydration.promise + } + }, + }, + }, + }) + }, + }, + }), + ) + let oldPreload = `pending` + let observation: HydrationLifecycleObservation | undefined + const oldReady = collection.preload() + void oldReady.then( + () => { + oldPreload = `fulfilled` + }, + (error: unknown) => { + oldPreload = error instanceof Error ? error.name : String(error) + }, + ) + const snapshot = async (): Promise => { + const stored = await persistence.adapter.loadResumeSnapshot( + { kind: `eager`, collectionId: id }, + { includeRows: true }, + ) + const sort = (rows: Iterable) => + [...rows] + .map(({ id: key, value }) => ({ id: key, value })) + .sort((a, b) => a.id.localeCompare(b.id)) + return { + rows: sort(collection.values()), + durable: sort( + stored.rows.map((row) => row.value as HydrationLifecycleRow), + ), + status: collection.status, + } + } + return withOracleCleanup(async () => { + await until(() => old.calls > 0) + if (h.oldList === `complete`) { + old.list.resolve({ records: hydrationLifecycleRows(h.old, 2) }) + await until(() => held) + await Promise.all(receipts) + reached.push(`hydration-held-cleanup`) + } + retired = true + await collection.cleanup() + await until(() => !old.stream.locked) + + let replacementReady: Promise | undefined + const startReplacement = async () => { + active = replacement + replacementReady = collection.preload() + await until(() => replacement.calls > 0 && currentHydrate !== undefined) + await currentHydrate!() + } + const deliverReplacement = async () => { + replacement.list.resolve({ + records: hydrationLifecycleRows(h.replacement, 3), + }) + await replacementReady + await Promise.all(receipts) + } + if (h.resumeAt !== `cleaned-up`) await startReplacement() + if (h.resumeAt === `replacement-ready`) await deliverReplacement() + if (h.oldList === `reject`) { + old.list.reject(new Error(`retired list failure`)) + reached.push(`late-list-rejection`) + } else oldHydration.resolve() + // All old provider promises are now settled. A host turn drains their + // native-stream and async continuation work before public/durable capture. + await turn() + await turn() + reached.push(`resume-${h.resumeAt}`) + const retiredResume = await snapshot() + if (h.resumeAt === `cleaned-up`) await startReplacement() + if (h.resumeAt !== `replacement-ready`) await deliverReplacement() + const ready = await snapshot() + reached.push(h.replacement ? `nonempty-replacement` : `empty-replacement`) + if (h.oldList === `complete`) { + if (h.replacement & ~h.old) reached.push(`new-row-outside-old-list`) + if (h.old & ~h.replacement) reached.push(`old-row-omitted`) + } + const earlierReceipts = receipts.length + replacement.controller.enqueue({ Insert: { id: `3`, value: 4 } }) + await until(() => receipts.length > earlierReceipts) + await Promise.all(receipts) + const afterEvent = await snapshot() + observation = { + retiredResume, + ready, + afterEvent, + oldPreload, + oldCancelCalls: old.cancellations, + oldLocked: old.stream.locked, + replacementLocked: replacement.stream.locked, + replacementReleasedAfterCleanup: false, + // Keep append-only diagnostics live through the final cleanup below. + reports, + obsoleteBegins, + reached, + } + return observation + }, [ + () => oldHydration.resolve(), + () => old.list.resolve({ records: [] }), + () => replacement.list.resolve({ records: [] }), + () => collection.cleanup(), + async () => { + await turn() + if (observation) + observation.replacementReleasedAfterCleanup = !replacement.stream.locked + }, + () => { + console.error = report + if (nodeEvents) process.off(`unhandledRejection`, detached) + else globalThis.removeEventListener(`unhandledrejection`, browserDetached) + }, + ]) +} diff --git a/packages/trailbase-db-collection/tests/lifecycle-hydration-oracle.ts b/packages/trailbase-db-collection/tests/lifecycle-hydration-oracle.ts new file mode 100644 index 0000000000..c026dcbdf5 --- /dev/null +++ b/packages/trailbase-db-collection/tests/lifecycle-hydration-oracle.ts @@ -0,0 +1,167 @@ +/** + * Cleanup retires one sync run. A late list or persisted-baseline completion + * from that run cannot change a cleaned-up Collection or a replacement run. + * Authority: the lifecycle oracle's cleanup law in ORACLE.md and the glossary's + * cleanup/sync-run contract. Durable accepted rows survive cleanup; a new eager + * run's complete result replaces them (the shared persisted-startup law). + * + * This is a projection of the primary lifecycle oracle, not a second lifecycle + * machine. Input bitsets encode subsets of three string keys. The judgment + * ignores adapter gates and queues: after cleanup public rows are empty; after + * replacement readiness they are exactly the new complete result. Resuming old + * work preserves each observation. A subsequent new-run event still applies. + * The production driver adds real persisted hosts to that same boundary. + */ +import * as fc from 'fast-check' +import { declareOracle } from '../../db/tests/oracle-declaration' + +export type HydrationLifecycleRow = { id: string; value: number } +export type HydrationLifecycleHistory = { + previous: number + old: number + replacement: number + oldList: `complete` | `reject` + resumeAt: `cleaned-up` | `replacement-loading` | `replacement-ready` + initialRender: `default` | `network-first` +} +export const hydrationLifecycleRows = ( + mask: number, + value: number, +): Array => + [0, 1, 2].flatMap((id) => + mask & (1 << id) ? [{ id: String(id), value }] : [], + ) + +export type HydrationLifecycleSnapshot = { + rows: Array + durable: Array + status: string +} +export type HydrationLifecycleObservation = { + retiredResume: HydrationLifecycleSnapshot + ready: HydrationLifecycleSnapshot + afterEvent: HydrationLifecycleSnapshot + oldPreload: string + oldCancelCalls: number + oldLocked: boolean + replacementLocked: boolean + replacementReleasedAfterCleanup: boolean + reports: Array + obsoleteBegins: number + reached: Array +} +export const hydrationLifecycleStates = [ + `hydration-held-cleanup`, + `late-list-rejection`, + `resume-cleaned-up`, + `resume-replacement-loading`, + `resume-replacement-ready`, + `empty-replacement`, + `nonempty-replacement`, + `new-row-outside-old-list`, + `old-row-omitted`, +] + +// The complete replacement decides values without consulting source output. +// Before its list is delivered, only the durable old page and restored rows +// exist. Old rejected lists contribute no page rows. +export function expectedHydrationLifecycle(h: HydrationLifecycleHistory) { + const oldRows = new Map( + hydrationLifecycleRows(h.previous, 1).map((row) => [row.id, row]), + ) + if (h.oldList === `complete`) + for (const row of hydrationLifecycleRows(h.old, 2)) oldRows.set(row.id, row) + const held = [...oldRows.values()].sort((a, b) => a.id.localeCompare(b.id)) + const replacement = hydrationLifecycleRows(h.replacement, 3) + const afterEvent = [...replacement, { id: `3`, value: 4 }] + return { + retiredResume: + h.resumeAt === `replacement-ready` + ? { rows: replacement, durable: replacement, status: `ready` } + : h.resumeAt === `replacement-loading` + ? { + rows: held, + durable: held, + status: `loading`, + } + : { rows: [], durable: held, status: `cleaned-up` }, + ready: { rows: replacement, durable: replacement, status: `ready` }, + afterEvent: { rows: afterEvent, durable: afterEvent, status: `ready` }, + } +} + +export function hydrationLifecycleViolations( + h: HydrationLifecycleHistory, + actual: HydrationLifecycleObservation, +): Array { + const expected = expectedHydrationLifecycle(h) + const violations: Array = [] + for (const cut of [`retiredResume`, `ready`, `afterEvent`] as const) + if (JSON.stringify(actual[cut]) !== JSON.stringify(expected[cut])) + violations.push( + `${cut}: ${JSON.stringify(actual[cut])} != ${JSON.stringify(expected[cut])}`, + ) + // SQLite guide, "Render after SQLite restore": default readiness still + // needs the adapter; network-first has not reached its deadline here. + const oldPreload = `AbortError` + if (actual.oldPreload !== oldPreload) + violations.push(`old preload ${actual.oldPreload} != ${oldPreload}`) + if (actual.oldCancelCalls !== 1 || actual.oldLocked) + violations.push(`old stream was not canceled once and released`) + if (!actual.replacementLocked) + violations.push(`late old work released the replacement stream`) + if (!actual.replacementReleasedAfterCleanup) + violations.push(`final cleanup did not release the replacement stream`) + if (actual.reports.length) + violations.push(`old work reported: ${actual.reports}`) + return violations +} + +// Row subsets vary independently when the list completes. A rejected list has +// no row payload, represented by old=0 rather than a redundant random subset. +// Exclude no legal overlap. Every run cleans up the old source before it resumes; +// list rejection never pretends to have reached the later hydration await. +const hydrationLifecycleCommon = { + previous: fc.integer({ min: 0, max: 7 }), + replacement: fc.integer({ min: 0, max: 7 }), + initialRender: fc.constantFrom(`default` as const, `network-first` as const), + resumeAt: fc.constantFrom( + `cleaned-up` as const, + `replacement-loading` as const, + `replacement-ready` as const, + ), +} +export const hydrationLifecycleHistoryArb = fc.oneof( + fc.record({ + ...hydrationLifecycleCommon, + oldList: fc.constant(`complete` as const), + old: fc.integer({ min: 0, max: 7 }), + }), + fc.record({ + ...hydrationLifecycleCommon, + oldList: fc.constant(`reject` as const), + old: fc.constant(0), + }), +) +export const hydrationLifecycleHistories: Array = ( + [`cleaned-up`, `replacement-loading`, `replacement-ready`] as const +).flatMap((resumeAt) => + ([`complete`, `reject`] as const).flatMap((oldList) => + [0, 6].flatMap((replacement) => + ([`default`, `network-first`] as const).map((initialRender) => ({ + previous: 3, + old: oldList === `complete` ? 1 : 0, + replacement, + oldList, + resumeAt, + initialRender, + })), + ), + ), +) + +declareOracle({ + model: [expectedHydrationLifecycle], + grammar: [hydrationLifecycleHistoryArb, hydrationLifecycleHistories], + check: [hydrationLifecycleViolations], +}) diff --git a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts index 0bfa7e4896..dcee640bfe 100644 --- a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts +++ b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts @@ -10,8 +10,17 @@ import { } from '../../db/tests/oracle-config' import { createOracleReach } from '../../db/tests/oracle-reach' import { declareOracle } from '../../db/tests/oracle-declaration' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import { trailBaseCollectionOptions } from '../src/trailbase' +import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' import { MockRecordApi } from './mock-record-api' +import { observeHydrationLifecycle } from './lifecycle-hydration-driver' +import { + hydrationLifecycleHistories, + hydrationLifecycleHistoryArb, + hydrationLifecycleStates, + hydrationLifecycleViolations, +} from './lifecycle-hydration-oracle' import { deferred, failure, @@ -23,6 +32,7 @@ import { } from './lifecycle-support' import type { Event, ListResponse } from 'trailbase' import type { RaceRow, Row } from './lifecycle-support' +import type { HydrationLifecycleHistory } from './lifecycle-hydration-oracle' /** * # Does a TrailBase sync run publish and retire one valid row history? @@ -1379,7 +1389,7 @@ async function checkPageHistory(history: PageHistory) { const trace = JSON.stringify(history) const checkChangedKeys = (label: string) => { for (const id of changedKeys) { - if (inTransit.some((pending) => pending.id === id)) continue + if (inTransit.some((delivery) => delivery.id === id)) continue expect(value(id), `${trace} ${label}: key ${id}`).toEqual(service.get(id)) } } @@ -1631,6 +1641,82 @@ it(`loads a row in a fresh run that an earlier run's event touched`, () => { return checkFreshRunAfterEvent() }) +/** + * The persisted receiver crosses the same cleanup law with an old completed + * list waiting at hydrateBaseline, or a late rejected list. Fixed histories + * and both generated campaigns use the same judgment on controlled and real + * SQLite hosts. The browser receiver imports the model and driver directly. + */ +async function checkHydrationLifecycle( + history: HydrationLifecycleHistory, + host: `controlled` | `node-sqlite`, + reach?: ReturnType, +) { + reach?.beginHistory() + const storage = createStartupHost(host) + return withOracleCleanup(async () => { + const actual = await observeHydrationLifecycle( + storage.persistence, + history, + `trailbase-hydration-${host}`, + ) + for (const state of actual.reached) reach?.reach(state) + expect(hydrationLifecycleViolations(history, actual)).toEqual([]) + return actual + }, [() => storage.close()]) +} +for (const host of [`controlled`, `node-sqlite`] as const) { + it.each(hydrationLifecycleHistories)( + `retires persisted hydration through ${host}: %j`, + async (history) => { + oracleRoute(`fixed history`) + await checkHydrationLifecycle(history, host) + }, + ) + for (const campaign of [`fixed`, `random`] as const) { + const property = `trailbase.lifecycle.persisted-hydration.${host}` + const settings = + campaign === `fixed` + ? oracleFixedSeed(728_159, { numRuns: oracleRuns(50) }) + : oraclePropertyOptions(50, property) + const reach = createOracleReach( + `${property}.${campaign}`, + hydrationLifecycleStates, + ) + let ran = false + let obsoleteBegins = 0 + afterAll(() => { + if (!ran) return + reach.assertReached() + process.stdout.write( + `${property}.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))} obsoleteBegins=${obsoleteBegins}\n`, + ) + }) + fcTest.prop([hydrationLifecycleHistoryArb], settings)( + `retires generated persisted hydration through ${host} with ${campaign} seed`, + async (history) => { + oracleRoute(oracleRouteOf(settings)) + ran = true + const actual = await checkHydrationLifecycle(history, host, reach) + obsoleteBegins += actual.obsoleteBegins + }, + ) + } + it(`rejects old-work row corruption through ${host}`, async () => { + oracleRoute(`calibration`) + const history = hydrationLifecycleHistories.find( + (h) => h.resumeAt === `replacement-ready` && h.replacement !== 0, + )! + const actual = await checkHydrationLifecycle(history, host) + expect( + hydrationLifecycleViolations(history, { + ...actual, + retiredResume: { ...actual.retiredResume, rows: [] }, + }), + ).not.toEqual([]) + }) +} + // What this oracle owns: the gap report judges these entries, and their // reference trees within the package, by this file's tests. declareOracle({ @@ -1643,6 +1729,7 @@ declareOracle({ checkStreamEnd, checkPageHistory, checkFreshRunAfterEvent, + checkHydrationLifecycle, ], model: [expectedEarlyDemand], }) diff --git a/scripts/bench/persisted-startup-2026-11-10.csv b/scripts/bench/persisted-startup-2026-11-10.csv new file mode 100644 index 0000000000..4d07b6e9ea --- /dev/null +++ b/scripts/bench/persisted-startup-2026-11-10.csv @@ -0,0 +1,177 @@ +experiment,observationOrder,variant,schedule,repetition,warmup,adapter,rows,saved,batch,payloadBytes,render,restoreDelayMs,sourceDelayMs,restoreReadMs,restoreHydratedMs,readyMs,sourceCommitMs,sourceAppliedMs,sourceWrites,sourceCommits,snapshotTransactions,durableTransactions,durableWrites,durableTruncates,heapBefore,heapAfter,sampledHeapMax,rssBefore,rssAfter,sampledRSSMax,externalBefore,sampledExternalMax,rssHighWaterBeforeKiB,rssHighWaterKiB +warm-adapter-natural,0,prior-selection,natural,-1,True,rxdb,1000,False,1000,256,default,0,0,11.747916999999916,45.85537499999987,46.00545899999997,11.554583999999977,45.13616699999989,1000,1,0,1,1000,0,37561296,52724736,52723880,162332672,167821312,167821312,7479543,6919338,158528,163888 +warm-adapter-natural,1,current,natural,-1,True,rxdb,1000,False,1000,256,default,0,0,7.450749999999971,26.72591599999987,26.768374999999878,7.281415999999808,26.618832999999995,1000,1,1,1,1000,1,37928592,49990904,49990048,163905536,168050688,168050688,7479703,6919498,160064,164112 +warm-adapter-natural,2,current,natural,0,False,rxdb,1000,False,1000,256,default,0,0,10.377709000000095,32.012709000000086,32.055834000000004,10.20225000000005,31.898959000000104,1000,1,1,1,1000,1,37566528,49989656,49988800,162299904,167002112,167002112,7479703,7479743,158496,163088 +warm-adapter-natural,3,prior-selection,natural,0,False,rxdb,1000,False,1000,256,default,0,0,13.677666999999929,40.275916999999936,40.323208999999906,13.157333999999992,40.16987500000005,1000,1,0,1,1000,0,37572504,52706112,52705256,163201024,168493056,168493056,7479543,7479583,159376,164544 +warm-adapter-natural,4,prior-selection,natural,1,False,rxdb,1000,False,1000,256,default,0,0,8.908791000000292,31.914874999999938,31.957916000000296,8.564624999999978,31.80574999999999,1000,1,0,1,1000,0,37562800,52708056,52707200,162988032,168525824,168525824,7479543,6919338,159168,164576 +warm-adapter-natural,5,current,natural,1,False,rxdb,1000,False,1000,256,default,0,0,9.796832999999879,32.78037500000005,32.82558300000005,9.626917000000049,32.68166700000006,1000,1,1,1,1000,1,37565520,50373248,49987896,164380672,169066496,169066496,7479703,6919498,160528,165104 +warm-adapter-natural,6,current,natural,2,False,rxdb,1000,False,1000,256,default,0,0,10.514332999999624,30.875125000000025,30.92462499999965,10.342124999999669,30.7785829999998,1000,1,1,1,1000,1,37570848,50254464,50253608,161906688,167002112,167002112,7479703,6919498,158112,163088 +warm-adapter-natural,7,prior-selection,natural,2,False,rxdb,1000,False,1000,256,default,0,0,8.194042000000081,32.29100000000017,32.33804200000009,8.016834000000017,32.20079200000009,1000,1,0,1,1000,0,37565368,52701024,52700168,162463744,167706624,167706624,7479543,6919338,158656,163776 +warm-adapter-natural,8,prior-selection,natural,-1,True,rxdb,1000,True,1000,256,default,0,0,14.336583000000246,55.51629200000025,55.56208300000026,11.16229199999998,55.41283300000032,1000,1,0,1,1000,0,37989952,60081552,59957040,177848320,183025664,183025664,7479543,6919338,173936,178736 +warm-adapter-natural,9,current,natural,-1,True,rxdb,1000,True,1000,256,default,0,0,11.849166999999852,49.82595900000001,49.88533399999983,9.321166999999832,49.53512499999988,1000,1,1,1,1000,1,37998128,55815352,55814496,176734208,178831360,178831360,7479703,6919498,172864,174640 +warm-adapter-natural,10,current,natural,0,False,rxdb,1000,True,1000,256,default,0,0,9.893541000000141,40.23087499999997,40.332833000000164,7.839165999999977,40.028332999999975,1000,1,1,1,1000,1,38003760,55863968,55863112,176128000,178077696,178077696,7479703,6919498,172320,173904 +warm-adapter-natural,11,prior-selection,natural,0,False,rxdb,1000,True,1000,256,default,0,0,12.059208000000126,45.73508300000003,45.784208000000035,9.389290999999957,45.615125000000035,1000,1,0,1,1000,0,37992496,59913504,59912648,178749440,183975936,183975936,7479543,6919338,174864,179664 +warm-adapter-natural,12,prior-selection,natural,1,False,rxdb,1000,True,1000,256,default,0,0,13.704915999999912,48.83895800000005,48.99570799999992,11.240499999999884,48.730458,1000,1,0,1,1000,0,38002352,60307392,60306536,178487296,183828480,183812096,7479543,6919338,174560,179520 +warm-adapter-natural,13,current,natural,1,False,rxdb,1000,True,1000,256,default,0,0,9.518082999999933,37.55508299999997,37.613832999999886,7.620332999999846,37.29595799999993,1000,1,1,1,1000,1,37986128,55854928,55854072,178864128,179748864,179748864,6920284,6920324,175040,175536 +warm-adapter-natural,14,current,natural,2,False,rxdb,1000,True,1000,256,default,0,0,10.567291999999952,45.544958999999835,45.709834,8.264791999999943,45.346667000000025,1000,1,1,1,1000,1,38000264,55856344,55855488,176128000,177881088,177881088,6919458,6919498,172144,173712 +warm-adapter-natural,15,prior-selection,natural,2,False,rxdb,1000,True,1000,256,default,0,0,9.792584000000033,39.15645899999981,39.263708999999835,7.9928339999999025,39.01404200000002,1000,1,0,1,1000,0,38004264,59911784,59910928,176340992,181944320,181944320,7479543,6919338,172448,177680 +warm-adapter-natural,16,prior-selection,natural,-1,True,rxdb,10000,False,1000,256,default,0,0,42.523459,95.50512499999991,364.8539169999999,66.33908399999996,364.73875,10000,1,0,1,10000,0,47804272,92883936,106383968,187695104,304693248,304693248,7478717,6919338,183536,297552 +warm-adapter-natural,17,current,natural,-1,True,rxdb,10000,False,1000,256,default,0,0,52.074416000000156,120.94925000000012,333.19108300000016,77.52966600000013,333.08158300000014,10000,1,1,1,10000,1,48442880,100746824,100745968,179568640,257556480,257556480,6920284,6920324,175600,251520 +warm-adapter-natural,18,current,natural,0,False,rxdb,10000,False,1000,256,default,0,0,45.87220800000023,108.46266600000035,323.88208299999997,72.27195800000027,323.7470410000001,10000,1,1,1,10000,1,48021096,133450928,132934000,186597376,291766272,291766272,7478877,7478917,182448,284928 +warm-adapter-natural,19,prior-selection,natural,0,False,rxdb,10000,False,1000,256,default,0,0,42.66362499999991,106.763375,392.2752079999998,70.6351659999998,392.145125,10000,1,0,1,10000,0,47802568,92930496,106580000,189644800,300417024,300417024,7478717,6919338,185440,293376 +warm-adapter-natural,20,prior-selection,natural,1,False,rxdb,10000,False,1000,256,default,0,0,45.362125000000106,104.80991700000004,417.4734590000003,70.21266700000024,417.35008400000015,10000,1,0,1,10000,0,47799752,93839368,106506472,185712640,297975808,297959424,7478717,6919338,181600,290992 +warm-adapter-natural,21,current,natural,1,False,rxdb,10000,False,1000,256,default,0,0,43.263000000000375,100.3905420000001,303.6570420000003,66.56162500000028,303.5220000000004,10000,1,1,1,10000,1,47804560,133069864,132789192,188694528,295108608,295108608,7478877,6919498,184512,288192 +warm-adapter-natural,22,current,natural,2,False,rxdb,10000,False,1000,256,default,0,0,42.19399999999996,119.53333299999986,328.44562499999984,79.64708299999984,328.3112079999996,10000,1,1,1,10000,1,47800040,133374712,133188144,186466304,288358400,288358400,7478877,7478917,182336,281600 +warm-adapter-natural,23,prior-selection,natural,2,False,rxdb,10000,False,1000,256,default,0,0,45.93729100000019,104.39079100000026,363.49466600000005,70.9896660000004,363.24508300000025,10000,1,0,1,10000,0,47808936,93142864,106708480,185925632,296370176,296370176,7478717,7478757,181808,289424 +warm-adapter-natural,24,prior-selection,natural,-1,True,rxdb,10000,True,1000,256,default,0,0,64.81675000000041,187.20954200000006,408.24129200000016,130.5394170000004,408.1321670000002,10000,1,0,1,10000,0,50205080,152535296,152534440,276414464,354811904,354811904,6919298,6919338,278224,346496 +warm-adapter-natural,25,current,natural,-1,True,rxdb,10000,True,1000,256,default,0,0,65.57295900000008,198.47066700000005,498.66116699999975,138.54254199999968,498.5470419999997,10000,1,1,1,10000,1,50207248,133690680,133689824,279265280,352894976,352894976,6919458,6919498,280192,344624 +warm-adapter-natural,26,current,natural,0,False,rxdb,10000,True,1000,256,default,0,0,63.38075000000026,196.01825000000008,479.1689160000001,127.45904100000007,479.05716600000005,10000,1,1,1,10000,1,50210320,133483480,133384664,288325632,357646336,357646336,6919458,6919498,289120,349264 +warm-adapter-natural,27,prior-selection,natural,0,False,rxdb,10000,True,1000,256,default,0,0,56.41483299999982,181.748916,423.59400000000005,118.15270800000008,423.46691599999986,10000,1,0,1,10000,0,50207616,152430264,152420112,278134784,353058816,353058816,6919298,6919338,279856,344784 +warm-adapter-natural,28,prior-selection,natural,1,False,rxdb,10000,True,1000,256,default,0,0,63.25649999999996,198.16945799999985,442.84791700000005,130.71408299999985,442.7305000000001,10000,1,0,1,10000,0,50204152,151227792,151226936,277757952,354140160,354140160,6919298,6919338,279648,345840 +warm-adapter-natural,29,current,natural,1,False,rxdb,10000,True,1000,256,default,0,0,66.99933400000009,198.17716699999983,468.112709,137.82054199999993,468.0011249999998,10000,1,1,1,10000,1,50205720,131189288,131188432,278740992,364019712,364019712,6919458,6919498,280688,355488 +warm-adapter-natural,30,current,natural,2,False,rxdb,10000,True,1000,256,default,0,0,65.58216700000003,209.48841700000003,530.9003749999997,142.8264589999999,530.6901669999997,10000,1,1,1,10000,1,50205832,132901872,132901016,277659648,365674496,365674496,6919458,6919498,279728,357104 +warm-adapter-natural,31,prior-selection,natural,2,False,rxdb,10000,True,1000,256,default,0,0,68.96254099999987,200.61945800000012,470.8005830000002,139.59454099999994,470.22404099999994,10000,1,0,1,10000,0,50212832,90060368,124234992,248315904,352075776,352075776,6919298,6919338,248448,343824 +warm-adapter-natural,32,prior-selection,natural,-1,True,powersync,1000,False,1000,256,default,0,0,2.3138749999998254,2.501499999999851,37.52341699999988,9.373666999999841,36.73199999999997,1000,2,0,1,1000,0,37225928,49403888,49402984,266616832,272547840,272547840,6901047,6900671,260368,266160 +warm-adapter-natural,33,current,natural,-1,True,powersync,1000,False,1000,256,default,0,0,1.672334000000319,1.7600840000000062,32.413584000000355,11.217667000000347,32.31600000000026,1000,1,1,1,1000,1,37227496,46915560,46733568,275234816,279527424,279527424,7460626,6900831,268784,272976 +warm-adapter-natural,34,current,natural,0,False,powersync,1000,False,1000,256,default,0,0,1.6899590000002718,1.7809590000001663,27.899625000000015,8.738209000000097,27.803792000000158,1000,1,1,1,1000,1,37224504,46630880,46629976,274464768,278249472,278249472,7460626,6900831,268032,271728 +warm-adapter-natural,35,prior-selection,natural,0,False,powersync,1000,False,1000,256,default,0,0,1.6675419999996848,1.7670829999997295,38.78095799999983,7.82654199999979,38.622707999999875,1000,2,0,1,1000,0,37226560,49349320,49348416,267337728,273416192,273416192,6901047,6900671,261072,267008 +warm-adapter-natural,36,prior-selection,natural,1,False,powersync,1000,False,1000,256,default,0,0,2.110124999999698,2.221791999999823,36.71912499999962,6.8436249999999745,36.64066699999967,1000,2,0,1,1000,0,37224064,49375248,49374048,276332544,283492352,283492352,7460466,6900671,269856,276848 +warm-adapter-natural,37,current,natural,1,False,powersync,1000,False,1000,256,default,0,0,2.452541999999994,2.562833999999839,30.996000000000095,9.145375000000058,30.892249999999876,1000,1,1,1,1000,1,37229576,46632192,46631288,275939328,280264704,280264704,7460626,6900831,269472,273696 +warm-adapter-natural,38,current,natural,2,False,powersync,1000,False,1000,256,default,0,0,1.5528749999998581,1.6461669999998776,40.577999999999975,9.00604199999998,40.49108400000023,1000,1,1,1,1000,1,37223792,46632472,46631568,267059200,271777792,271777792,6901207,6901247,260800,265408 +warm-adapter-natural,39,prior-selection,natural,2,False,powersync,1000,False,1000,256,default,0,0,3.973124999999982,4.142583000000286,40.922250000000076,13.591917000000194,40.826292000000194,1000,2,0,1,1000,0,37220280,49384264,49383360,265879552,274169856,274169856,6901047,6901087,259648,267744 +warm-adapter-natural,40,prior-selection,natural,-1,True,powersync,1000,True,1000,256,default,0,0,4.29141699999991,9.932749999999942,40.75337499999978,15.68854199999987,40.43566699999974,1000,2,0,1,1000,0,37661944,56479272,56478072,285376512,287571968,287571968,7460050,6900671,278896,280832 +warm-adapter-natural,41,current,natural,-1,True,powersync,1000,True,1000,256,default,0,0,5.075250000000324,10.740708000000268,51.25412499999993,17.64720800000032,51.14191600000004,1000,1,1,1,1000,1,37659288,52516008,52515104,278036480,279396352,279396352,6901207,6900831,272064,272848 +warm-adapter-natural,42,current,natural,0,False,powersync,1000,True,1000,256,default,0,0,6.0242920000000595,10.90479200000027,50.40162499999997,16.82358299999987,50.29845800000021,1000,1,1,1,1000,1,37664440,52477968,52477064,284065792,285360128,285360128,7460210,7460250,277600,278672 +warm-adapter-natural,43,prior-selection,natural,0,False,powersync,1000,True,1000,256,default,0,0,4.692708000000039,8.583082999999988,45.93120799999997,15.236040999999659,45.82666599999993,1000,2,0,1,1000,0,37661232,56547184,56545984,283754496,285802496,285802496,7460050,6900671,277104,279104 +warm-adapter-natural,44,prior-selection,natural,1,False,powersync,1000,True,1000,256,default,0,0,7.333125000000109,12.721500000000106,54.82604200000014,18.688125000000127,54.73533300000008,1000,2,0,1,1000,0,37662008,56571304,56570400,282722304,285605888,285605888,7460050,7460090,276240,278912 +warm-adapter-natural,45,current,natural,1,False,powersync,1000,True,1000,256,default,0,0,4.810165999999754,8.33220799999981,45.16820800000005,15.14758299999994,45.07679099999996,1000,1,1,1,1000,1,37660984,52534120,52533216,283607040,285032448,285032448,7460210,7460250,277184,278352 +warm-adapter-natural,46,current,natural,2,False,powersync,1000,True,1000,256,default,0,0,5.93729200000007,12.006249999999909,50.515499999999975,17.686167000000296,50.411125000000084,1000,1,1,1,1000,1,37666472,52492424,52491520,282935296,284229632,284229632,7460210,7460250,276416,277568 +warm-adapter-natural,47,prior-selection,natural,2,False,powersync,1000,True,1000,256,default,0,0,5.114916999999878,11.78754200000003,51.08775000000014,22.921957999999904,50.98366699999997,1000,2,0,1,1000,0,37651736,56692416,56475696,281559040,284557312,284557312,6901047,6900671,275568,277888 +warm-adapter-natural,48,prior-selection,natural,-1,True,powersync,10000,False,1000,256,default,0,0,2.5212910000000193,2.6308329999992566,239.54220799999985,9.919457999999395,239.43812499999967,10000,11,0,10,10000,0,37988240,72626048,75988960,298385408,336117760,336117760,6900631,6900671,291536,328240 +warm-adapter-natural,49,current,natural,-1,True,powersync,10000,False,1000,256,default,0,0,2.5020830000003116,2.6119580000004134,261.916792,43.807166999999936,261.8225419999999,10000,1,1,1,10000,1,38093152,73252632,73251728,296714240,350928896,350928896,6900791,6900831,289904,342704 +warm-adapter-natural,50,current,natural,0,False,powersync,10000,False,1000,256,default,0,0,2.2982499999998254,2.426749999999629,269.7201249999998,47.65887500000008,269.627125,10000,1,1,1,10000,1,38100016,72029176,72028272,297795584,354222080,354222080,6901207,6900831,290960,345920 +warm-adapter-natural,51,prior-selection,natural,0,False,powersync,10000,False,1000,256,default,0,0,2.392749999999978,2.579416000000492,222.0574999999999,7.691125000000284,221.95066600000064,10000,11,0,10,10000,0,38103664,72769152,76239552,303874048,336101376,336084992,7460050,6900671,297008,328224 +warm-adapter-natural,52,prior-selection,natural,1,False,powersync,10000,False,1000,256,default,0,0,2.1532500000002983,2.2747920000001614,239.20024999999987,8.955291999999645,239.1098750000001,10000,11,0,10,10000,0,38100496,72791528,76192832,297271296,333103104,333103104,6901047,6901087,290448,325296 +warm-adapter-natural,53,current,natural,1,False,powersync,10000,False,1000,256,default,0,0,1.7942919999995866,1.9057499999998981,247.82616700000017,40.081207999999606,247.56162499999937,10000,1,1,1,10000,1,37821712,93393960,93393056,300269568,364134400,364134400,6900791,6900831,293520,355600 +warm-adapter-natural,54,current,natural,2,False,powersync,10000,False,1000,256,default,0,0,2.0846250000004147,2.2975839999999152,290.6123750000006,44.97962499999994,290.507834,10000,1,1,1,10000,1,38099968,72488856,72487952,298795008,352894976,352894976,6901207,6900831,291984,344624 +warm-adapter-natural,55,prior-selection,natural,2,False,powersync,10000,False,1000,256,default,0,0,1.7345839999998134,1.8377089999994496,224.1939999999995,9.521041999999397,224.09087499999987,10000,11,0,10,10000,0,38100264,72876920,76220152,296615936,330465280,330465280,6901047,6901087,289808,322720 +warm-adapter-natural,56,prior-selection,natural,-1,True,powersync,10000,True,1000,256,default,0,0,26.217375000000175,82.75225000000046,397.5169160000005,100.07670800000051,397.43170800000007,10000,11,0,10,10000,0,39861992,68448664,123968048,383811584,434094080,434094080,6900839,6900879,374832,426016 +warm-adapter-natural,57,current,natural,-1,True,powersync,10000,True,1000,256,default,0,0,19.29966599999989,69.8041659999999,465.41070800000034,109.68104099999982,465.2640000000001,10000,1,1,1,10000,1,39733080,116591832,116590928,382418944,441270272,441270272,6900791,6900831,373600,430928 +warm-adapter-natural,58,current,natural,0,False,powersync,10000,True,1000,256,default,0,0,18.62420800000018,65.14620800000012,448.2021250000007,102.77616600000056,448.1028750000005,10000,1,1,1,10000,1,39715728,113418336,113417432,376815616,433356800,433340416,6900791,6900831,368288,423200 +warm-adapter-natural,59,prior-selection,natural,0,False,powersync,10000,True,1000,256,default,0,0,15.74554199999966,59.14229199999954,395.550209,64.71324999999979,395.45387500000015,10000,11,0,10,10000,0,39710368,67062416,122800472,376078336,437780480,437780480,6900631,6900671,367552,429616 +warm-adapter-natural,60,prior-selection,natural,1,False,powersync,10000,True,1000,256,default,0,0,17.448500000000422,57.650166000000354,334.83608300000014,62.91916600000059,334.7431250000009,10000,11,0,10,10000,0,39863664,65751336,121287152,389922816,433635328,433635328,6900839,6900671,380992,425312 +warm-adapter-natural,61,current,natural,1,False,powersync,10000,True,1000,256,default,0,0,17.162584000000606,53.08287500000006,429.2506250000006,97.08608400000048,429.1605840000002,10000,1,1,1,10000,1,39865712,111648976,111648072,387399680,429326336,429326336,6900999,6901039,378320,419264 +warm-adapter-natural,62,current,natural,2,False,powersync,10000,True,1000,256,default,0,0,16.241915999999947,58.09687500000018,450.87262499999997,103.11204100000032,450.76595799999996,10000,1,1,1,10000,1,39861728,114948072,114947168,373063680,422199296,422182912,6900999,6900831,364512,412304 +warm-adapter-natural,63,prior-selection,natural,2,False,powersync,10000,True,1000,256,default,0,0,21.095916000000216,67.94595800000025,364.1354160000001,73.39375000000018,364.0466660000002,10000,11,0,10,10000,0,39863416,67495464,123128576,385613824,430997504,430997504,6900839,6900671,376576,423024 +final-natural,0,prior-selection,natural,-1,True,rxdb,1000,False,1000,256,default,0,0,8.997624999999971,29.590374999999995,29.6323339999999,8.799291999999923,29.492209000000003,1000,1,0,1,1000,0,37570456,52705552,52704696,161415168,167116800,167116800,7480661,6920456,157632,163200 +final-natural,1,current,natural,-1,True,rxdb,1000,False,1000,256,default,0,0,10.85958299999993,32.53066599999988,32.615749999999935,10.652207999999973,32.42158299999983,1000,1,1,1,1000,1,37571144,50018384,50341256,162889728,167608320,167608320,7480821,7480861,159072,163680 +final-natural,2,current,natural,0,False,rxdb,1000,False,1000,256,default,0,0,15.879915999999866,35.8237909999998,35.92883299999994,15.673624999999902,35.6257499999997,1000,1,1,1,1000,1,37567504,49991000,49990144,162545664,166789120,166789120,7480821,7480861,158736,162880 +final-natural,3,prior-selection,natural,0,False,rxdb,1000,False,1000,256,default,0,0,11.248957999999902,42.00483299999996,42.05099999999993,11.023666999999932,41.88408299999992,1000,1,0,1,1000,0,37562280,52783656,52750848,163201024,168902656,168902656,6921242,6921282,159408,164944 +final-natural,4,prior-selection,natural,1,False,rxdb,1000,False,1000,256,default,0,0,8.478749999999991,37.69858399999998,37.74504200000001,8.27691700000014,37.58845900000006,1000,1,0,1,1000,0,37567056,52745896,52745040,163905536,169000960,169000960,7480661,7480701,160064,165040 +final-natural,5,current,natural,1,False,rxdb,1000,False,1000,256,default,0,0,9.241792000000032,29.609833999999864,29.654125000000022,9.060334000000012,29.517499999999927,1000,1,1,1,1000,1,37557072,49995480,49979680,162136064,167051264,167051264,6921402,6920616,158336,163136 +final-natural,6,current,natural,2,False,rxdb,1000,False,1000,256,default,0,0,10.053750000000036,30.5017499999999,30.557583999999906,8.97845899999993,30.340333999999984,1000,1,1,1,1000,1,37570600,50260808,50259944,164298752,170672128,170672128,7480821,7480861,160448,166672 +final-natural,7,prior-selection,natural,2,False,rxdb,1000,False,1000,256,default,0,0,7.734542000000147,35.93558400000006,35.98595900000009,7.549584000000095,35.74883399999999,1000,1,0,1,1000,0,37542336,52930784,52929928,165150720,170852352,170852352,7480661,7480701,161280,166848 +final-natural,8,prior-selection,natural,-1,True,rxdb,1000,True,1000,256,default,0,0,10.202957999999853,43.02583299999992,43.069499999999834,8.063832999999931,42.911790999999994,1000,1,0,1,1000,0,38010616,59870376,59869520,177192960,182484992,182484992,7480661,6920456,173328,178208 +final-natural,9,current,natural,-1,True,rxdb,1000,True,1000,256,default,0,0,14.701499999999896,63.17308400000002,63.278542000000016,12.14449999999988,63.059499999999844,1000,1,1,1,1000,1,38011008,56238152,56237296,177029120,178896896,178896896,7480821,7480861,173200,174704 +final-natural,10,current,natural,0,False,rxdb,1000,True,1000,256,default,0,0,13.048915999999963,44.6959159999999,44.74920799999995,9.766250000000127,44.58450000000016,1000,1,1,1,1000,1,37989392,56340048,55856792,177946624,178585600,178585600,6921402,6921442,174288,174400 +final-natural,11,prior-selection,natural,0,False,rxdb,1000,True,1000,256,default,0,0,10.005750000000035,40.01970800000004,40.15341699999999,7.927582999999913,39.876207999999906,1000,1,0,1,1000,0,38003192,59878400,59877544,176029696,181059584,181059584,7480661,6920456,172160,176816 +final-natural,12,prior-selection,natural,1,False,rxdb,1000,True,1000,256,default,0,0,10.963959000000159,42.90645900000027,42.96287500000017,7.708875000000262,42.690500000000156,1000,1,0,1,1000,0,38007936,59913472,59912616,175243264,180502528,180502528,7480661,7480701,171376,176272 +final-natural,13,current,natural,1,False,rxdb,1000,True,1000,256,default,0,0,11.570749999999862,42.31204200000002,42.35629199999994,8.930666999999858,42.100749999999834,1000,1,1,1,1000,1,38006048,56040832,55977640,176799744,178454528,178454528,7480821,6920616,172928,174272 +final-natural,14,current,natural,2,False,rxdb,1000,True,1000,256,default,0,0,17.48054100000013,60.90241600000013,61,14.888916000000108,60.7582910000001,1000,1,1,1,1000,1,38012568,55894488,55893632,174817280,176521216,176521216,7480821,7480861,171056,172384 +final-natural,15,prior-selection,natural,2,False,rxdb,1000,True,1000,256,default,0,0,10.997707999999875,41.50887499999999,41.63720799999987,8.567165999999816,41.382082999999966,1000,1,0,1,1000,0,38004528,60141920,60141064,177963008,183042048,183042048,7480661,6920456,174064,178752 +final-natural,16,prior-selection,natural,-1,True,rxdb,10000,False,1000,256,default,0,0,50.175208999999995,111.25929199999996,371.22512499999993,73.41762500000004,371.0646670000001,10000,1,0,1,10000,0,48445784,100894600,100893744,179273728,273137664,273137664,6921242,6920456,175312,266736 +final-natural,17,current,natural,-1,True,rxdb,10000,False,1000,256,default,0,0,37.81658399999992,93.26454199999989,271.21579199999996,64.03395899999987,271.0760839999998,10000,1,1,1,10000,1,47809368,132781728,132780872,188071936,289292288,289292288,7479995,6920616,183904,282512 +final-natural,18,current,natural,0,False,rxdb,10000,False,1000,256,default,0,0,40.02033400000005,97.08437499999991,284.6895420000001,63.183667000000014,284.5697089999999,10000,1,1,1,10000,1,47808608,132961432,132960576,185581568,291880960,291880960,7479995,7480035,181472,285040 +final-natural,19,prior-selection,natural,0,False,rxdb,10000,False,1000,256,default,0,0,36.32112499999994,87.27691600000003,319.0340000000001,59.154624999999896,318.8375000000001,10000,1,0,1,10000,0,47804904,92889600,106408520,186826752,296910848,296894464,7479835,6920456,182688,289952 +final-natural,20,prior-selection,natural,1,False,rxdb,10000,False,1000,256,default,0,0,43.94987500000002,97.27558299999987,323.5017499999999,67.58449999999993,323.376,10000,1,0,1,10000,0,47808376,92639952,106395648,188678144,300662784,300662784,7479835,6920456,184496,293616 +final-natural,21,current,natural,1,False,rxdb,10000,False,1000,256,default,0,0,38.877917000000025,99.05624999999986,292.5224999999998,64.46966700000007,292.31854199999975,10000,1,1,1,10000,1,47823648,133334792,133181168,188497920,294748160,294748160,7479995,6920616,184320,287840 +final-natural,22,current,natural,2,False,rxdb,10000,False,1000,256,default,0,0,45.81491600000004,102.65083299999992,264.46229100000005,71.30995800000005,264.313541,10000,1,1,1,10000,1,47813208,133102368,132938256,188989440,290799616,290799616,7479995,6920616,184800,283984 +final-natural,23,prior-selection,natural,2,False,rxdb,10000,False,1000,256,default,0,0,42.92033399999991,96.13966699999992,317.91008399999987,66.37320899999986,317.7987089999999,10000,1,0,1,10000,0,47800296,92802680,106246344,187252736,300285952,300285952,7479835,7479875,183104,293248 +final-natural,24,prior-selection,natural,-1,True,rxdb,10000,True,1000,256,default,0,0,59.85179199999993,181.0993329999999,425.10479199999986,125.72945800000002,424.9689999999998,10000,1,0,1,10000,0,50212976,152187944,151960056,276627456,350519296,350519296,6920416,6920456,277664,342304 +final-natural,25,current,natural,-1,True,rxdb,10000,True,1000,256,default,0,0,50.30387500000006,228.03420899999992,456.5111670000001,114.87049999999999,456.31570899999997,10000,1,1,1,10000,1,50210832,134036624,133833896,298663936,366837760,366837760,6920576,6920616,300896,358240 +final-natural,26,current,natural,0,False,rxdb,10000,True,1000,256,default,0,0,57.17116699999997,173.4976670000001,428.3977500000001,118.39387500000021,428.27516700000024,10000,1,1,1,10000,1,50207320,133430680,133358272,275562496,356859904,356859904,6920576,6920616,277712,348496 +final-natural,27,prior-selection,natural,0,False,rxdb,10000,True,1000,256,default,0,0,55.16295799999989,173.43899999999985,421.0565409999999,116.58950000000004,420.9075829999997,10000,1,0,1,10000,0,50208096,152231184,151914128,277463040,351027200,351027200,6920416,6920456,278880,342800 +final-natural,28,prior-selection,natural,1,False,rxdb,10000,True,1000,256,default,0,0,63.15491599999996,191.98374999999987,491.571958,130.8750829999999,491.43062499999996,10000,1,0,1,10000,0,50208560,153127080,153126224,251641856,351911936,351911936,6920416,6920456,258288,343664 +final-natural,29,current,natural,1,False,rxdb,10000,True,1000,256,default,0,0,64.36137500000041,180.5895830000004,411.08108300000004,123.7274580000003,410.92483300000004,10000,1,1,1,10000,1,50221656,131150544,131149688,254001152,359694336,359677952,6920576,6920616,259600,351264 +final-natural,30,current,natural,2,False,rxdb,10000,True,1000,256,default,0,0,72.11483300000009,201.2420830000001,453.23020799999995,140.2375830000001,453.1022499999999,10000,1,1,1,10000,1,50213224,133987968,133987112,279003136,361234432,361234432,6920576,6920616,280336,352768 +final-natural,31,prior-selection,natural,2,False,rxdb,10000,True,1000,256,default,0,0,73.79058299999997,209.07158299999992,423.3974579999999,149.56104199999982,423.23099999999977,10000,1,0,1,10000,0,50208976,150842400,151388848,277774336,356515840,356515840,6920416,6920456,279904,348160 +final-natural,32,prior-selection,natural,-1,True,powersync,1000,False,1000,256,default,0,0,3.6840839999999844,3.8366249999999127,33.80054199999995,8.496999999999844,33.647166999999854,1000,2,0,1,1000,0,37238744,49416936,49890872,277610496,283131904,283131904,7461584,6901789,271104,276496 +final-natural,33,current,natural,-1,True,powersync,1000,False,1000,256,default,0,0,2.5019999999999527,2.6175829999997404,26.915958000000046,7.852457999999842,26.766708000000108,1000,1,1,1,1000,1,37235832,46602752,46601848,273825792,277807104,277807104,7461744,6901949,267408,271296 +final-natural,34,current,natural,0,False,powersync,1000,False,1000,256,default,0,0,2.5057079999999132,2.6314579999998386,27.643749999999955,9.248832999999877,27.455917,1000,1,1,1,1000,1,37233856,46601784,46600584,274448384,278102016,278102016,7461744,6901949,268016,271584 +final-natural,35,prior-selection,natural,0,False,powersync,1000,False,1000,256,default,0,0,4.650667000000112,4.790917000000036,48.32012499999996,13.781292000000121,48.204542000000174,1000,2,0,1,1000,0,37236032,49387488,49386584,276283392,281935872,281935872,7461584,7461624,269808,275328 +final-natural,36,prior-selection,natural,1,False,powersync,1000,False,1000,256,default,0,0,4.171000000000049,4.643666000000167,40.470208000000184,10.870458000000099,40.25475000000006,1000,2,0,1,1000,0,37237096,49371328,49370424,276660224,281706496,281706496,7461584,6901789,270176,275104 +final-natural,37,current,natural,1,False,powersync,1000,False,1000,256,default,0,0,2.3391249999999673,2.477957999999944,29.210125000000062,8.227957999999944,28.94762499999979,1000,1,1,1,1000,1,37231168,46645160,46644256,276840448,280756224,280756224,7461744,7461784,270352,274176 +final-natural,38,current,natural,2,False,powersync,1000,False,1000,256,default,0,0,2.843334000000141,2.962541999999985,33.634500000000116,10.536708999999973,33.48908400000005,1000,1,1,1,1000,1,37231240,46641184,46640280,267829248,271777792,271777792,6902325,6902365,261632,265408 +final-natural,39,prior-selection,natural,2,False,powersync,1000,False,1000,256,default,0,0,2.184124999999767,2.331749999999829,36.33241700000008,8.586667000000034,36.21070800000007,1000,2,0,1,1000,0,37235552,49569408,49568504,277135360,282591232,282591232,7461584,7461624,270640,275968 +final-natural,40,prior-selection,natural,-1,True,powersync,1000,True,1000,256,default,0,0,4.265875000000051,7.71287499999994,38.24479199999996,12.838792000000012,37.84570899999994,1000,2,0,1,1000,0,37593016,56434032,56433128,283918336,288784384,288784384,6901749,6901789,277264,282016 +final-natural,41,current,natural,-1,True,powersync,1000,True,1000,256,default,0,0,3.8943340000000717,7.314209000000119,37.053916999999956,12.546084000000064,36.91883400000029,1000,1,1,1,1000,1,37666176,52443792,52442592,285016064,287113216,287113216,7461328,7461368,278336,280384 +final-natural,42,current,natural,0,False,powersync,1000,True,1000,256,default,0,0,4.2533330000001115,7.615792000000056,38.71300000000019,12.853667000000087,38.602125000000115,1000,1,1,1,1000,1,37668912,52499056,52486768,281772032,283934720,283934720,7461328,6901949,275168,277280 +final-natural,43,prior-selection,natural,0,False,powersync,1000,True,1000,256,default,0,0,5.35674999999992,9.151624999999967,37.281791999999996,13.55058299999996,37.17833299999984,1000,2,0,1,1000,0,37672544,56459880,56458680,283000832,286244864,286244864,7461168,6901789,276576,279536 +final-natural,44,prior-selection,natural,1,False,powersync,1000,True,1000,256,default,0,0,4.382000000000062,8.423042000000123,45.65487499999995,13.111625000000004,45.54779200000007,1000,2,0,1,1000,0,37671896,56422192,56420992,284082176,287096832,287096832,7461168,6901789,277424,280368 +final-natural,45,current,natural,1,False,powersync,1000,True,1000,256,default,0,0,4.048000000000229,8.158167000000049,36.999416999999994,13.329334000000017,36.88820900000019,1000,1,1,1,1000,1,37660080,52374160,52373256,281952256,283164672,283164672,6902325,6901949,275712,276528 +final-natural,46,current,natural,2,False,powersync,1000,True,1000,256,default,0,0,4.5623749999999745,8.14058399999999,41.14820899999995,14.548541999999998,40.97404199999983,1000,1,1,1,1000,1,37670952,52446728,52676040,281133056,283557888,283541504,7461328,6901949,274544,276912 +final-natural,47,prior-selection,natural,2,False,powersync,1000,True,1000,256,default,0,0,4.684417000000167,8.052250000000186,38.7013750000001,13.730125000000044,38.530708000000004,1000,2,0,1,1000,0,37675440,56553152,56551952,283574272,286179328,286179328,7461168,6901789,276928,279472 +final-natural,48,prior-selection,natural,-1,True,powersync,10000,False,1000,256,default,0,0,2.532375000000229,2.6542920000001686,183.53304200000002,9.226250000000164,183.35400000000027,10000,11,0,10,10000,0,38110472,72543464,76100256,303022080,338313216,338313216,7461168,6901789,296192,330384 +final-natural,49,current,natural,-1,True,powersync,10000,False,1000,256,default,0,0,1.7920830000002752,1.8897909999996045,187.40070800000012,33.27854099999968,187.21604099999968,10000,1,1,1,10000,1,38110168,93229536,93228632,301350912,361152512,361136128,7461328,6901949,294544,352688 +final-natural,50,current,natural,0,False,powersync,10000,False,1000,256,default,0,0,1.8535420000002887,1.9677920000003724,180.31762500000013,28.747084000000086,180.21329200000037,10000,1,1,1,10000,1,38110056,93380744,93379840,301924352,364150784,364150784,7461328,6901949,295104,355616 +final-natural,51,prior-selection,natural,0,False,powersync,10000,False,1000,256,default,0,0,2.1940420000000813,2.3343340000001263,164.23525000000018,6.8803750000001855,164.1326250000002,10000,11,0,10,10000,0,38110280,72255824,75771288,303071232,337297408,337297408,7461168,6901789,296240,329392 +final-natural,52,prior-selection,natural,1,False,powersync,10000,False,1000,256,default,0,0,2.9472920000002887,3.1235420000002705,182.68716700000004,8.181042000000161,182.54750000000013,10000,11,0,10,10000,0,38110584,72283792,75680064,305512448,336953344,336953344,7461168,6901789,298608,329056 +final-natural,53,current,natural,1,False,powersync,10000,False,1000,256,default,0,0,1.9376250000000255,2.0677090000003773,199.19783400000006,37.161959000000024,198.916334,10000,1,1,1,10000,1,38110792,93083800,93082896,304250880,363855872,363855872,7461328,6901949,297392,355328 +final-natural,54,current,natural,2,False,powersync,10000,False,1000,256,default,0,0,2.366208999999799,2.504124999999476,198.56283399999938,36.13304199999948,198.3925419999996,10000,1,1,1,10000,1,38110120,93338032,93337128,304807936,365232128,365232128,7461328,6901949,297920,356672 +final-natural,55,prior-selection,natural,2,False,powersync,10000,False,1000,256,default,0,0,2.025875000000269,2.1481250000001637,184.69658300000037,9.071458000000348,184.60108300000002,10000,11,0,10,10000,0,38110104,72828480,76181216,303202304,338329600,338329600,7461168,6901789,296352,330400 +final-natural,56,prior-selection,natural,-1,True,powersync,10000,True,1000,256,default,0,0,19.24120799999946,57.86924999999974,316.5879159999995,63.499415999999655,316.49020799999926,10000,11,0,10,10000,0,39873520,68255952,123950728,383729664,436240384,436240384,6901957,6901997,374912,428144 +final-natural,57,current,natural,-1,True,powersync,10000,True,1000,256,default,0,0,16.89141699999982,54.89587500000016,336.4668329999995,102.18341700000019,336.3623749999997,10000,1,1,1,10000,1,39871320,115259640,115258736,385744896,431407104,431407104,6902117,6901949,376896,421296 +final-natural,58,current,natural,0,False,powersync,10000,True,1000,256,default,0,0,15.493000000000393,51.92420800000036,327.06625000000076,94.8041659999999,326.9694159999999,10000,1,1,1,10000,1,39868888,114350080,114349176,384614400,425459712,425459712,6902117,6901949,375760,415488 +final-natural,59,prior-selection,natural,0,False,powersync,10000,True,1000,256,default,0,0,17.585791999999856,54.14599999999973,269.75637499999993,59.68366699999933,269.63737499999934,10000,11,0,10,10000,0,39731536,68145008,123684368,375406592,434307072,434896896,6901749,6901789,366752,426192 +final-natural,60,prior-selection,natural,1,False,powersync,10000,True,1000,256,default,0,0,14.668624999999338,49.83216600000014,283.2072909999997,55.29570799999965,283.0415409999996,10000,11,0,10,10000,0,39867136,67318240,123093296,388087808,433061888,433061888,6901957,6901789,379168,425088 +final-natural,61,current,natural,1,False,powersync,10000,True,1000,256,default,0,0,14.881916999999703,50.49633399999993,320.2602089999996,90.16437499999984,320.1393750000002,10000,1,1,1,10000,1,39869112,114959288,114958384,390660096,427999232,427999232,6902117,6901949,381504,417968 +final-natural,62,current,natural,2,False,powersync,10000,True,1000,256,default,0,0,15.187165999999706,53.14879099999962,319.39500000000044,104.82824999999957,319.27354099999957,10000,1,1,1,10000,1,39870088,114779256,114778352,388366336,427048960,427048960,6902117,6901949,379264,417040 +final-natural,63,prior-selection,natural,2,False,powersync,10000,True,1000,256,default,0,0,15.563292000000274,49.13954200000035,274.7634170000001,54.191625000000386,274.6777500000003,10000,11,0,10,10000,0,39871424,68004800,123769224,387579904,433881088,433881088,6901957,6901789,378688,425872 +final-controls,0,prior-selection,source-first,-1,True,rxdb,10000,True,1000,256,default,300,0,315.3278750000002,599.4081670000003,599.4555000000003,64.7019580000001,599.253125,10000,1,0,1,10000,0,50207672,140121432,140279328,276070400,350797824,350797824,6920416,6920456,278064,342576 +final-controls,1,current,source-first,-1,True,rxdb,10000,True,1000,256,default,300,0,315.11745900000005,527.701875,527.7395839999999,58.97733399999993,527.601625,10000,1,1,1,10000,1,50207392,122748464,122747704,274923520,351010816,351010816,6920576,6920616,277376,342784 +final-controls,2,current,source-first,0,False,rxdb,10000,True,1000,256,default,300,0,313.19337500000006,551.5226660000001,551.574625,58.975415999999996,551.390625,10000,1,1,1,10000,1,50208248,122787496,122786736,274661376,349749248,349749248,6920576,6920616,276976,341552 +final-controls,3,prior-selection,source-first,0,False,rxdb,10000,True,1000,256,default,300,0,313.80324999999993,568.2444170000001,568.314625,59.680417000000034,568.113417,10000,1,0,1,10000,0,50208200,141238152,141237392,276381696,348028928,348028928,6920416,6920456,278608,339872 +final-controls,4,prior-selection,source-first,1,False,rxdb,10000,True,1000,256,default,300,0,316.068583,592.463041,592.5059160000001,58.69875000000002,592.3531250000001,10000,1,0,1,10000,0,50208704,140862680,140861920,274055168,346324992,346324992,6920416,6920456,276240,338208 +final-controls,5,current,source-first,1,False,rxdb,10000,True,1000,256,default,300,0,319.72454100000004,619.307708,619.3519999999999,64.15895799999998,619.189666,10000,1,1,1,10000,1,50208680,121413824,121413064,280002560,357105664,357105664,6920576,6920616,280400,348736 +final-controls,6,current,source-first,2,False,rxdb,10000,True,1000,256,default,300,0,317.278333,597.6594580000001,597.7740829999998,70.290166,597.548875,10000,1,1,1,10000,1,50207128,122638472,122637712,281624576,359415808,359415808,6920576,6920616,281712,350992 +final-controls,7,prior-selection,source-first,2,False,rxdb,10000,True,1000,256,default,300,0,315.95166599999993,627.6436659999999,627.6859999999999,60.27354100000002,627.514083,10000,1,0,1,10000,0,50210184,140127232,140177552,279134208,351977472,351977472,6920416,6920456,279952,343728 +final-controls,8,prior-selection,fallback,-1,True,rxdb,10000,True,1000,256,network-first,0,350,16.274249999999938,54.38499999999999,58.268916999999874,414.841625,648.4728749999999,10000,1,0,1,10000,0,50208672,145975792,145975032,275464192,353075200,353075200,6920416,6920456,277632,344800 +final-controls,9,current,fallback,-1,True,rxdb,10000,True,1000,256,network-first,0,350,15.648374999999987,50.85270800000012,55.43279200000006,411.3445420000003,618.2356669999999,10000,1,1,1,10000,1,50211720,129682576,129681816,277200896,364494848,364494848,6920576,6920616,279200,355952 +final-controls,10,current,fallback,0,False,rxdb,10000,True,1000,256,network-first,0,350,15.031542000000059,53.118999999999915,57.48904199999993,415.0005840000001,615.149834,10000,1,1,1,10000,1,50212896,129604704,129603944,277102592,364150784,364150784,6920576,6920616,279424,355616 +final-controls,11,prior-selection,fallback,0,False,rxdb,10000,True,1000,256,network-first,0,350,15.947292000000061,51.94779200000005,55.85891700000002,411.25424999999996,612.4660000000001,10000,1,0,1,10000,0,50207712,147376296,147375536,273186816,353533952,353533952,6920416,6920456,275552,345248 +final-controls,12,prior-selection,fallback,1,False,rxdb,10000,True,1000,256,network-first,0,350,18.796500000000037,56.376125,60.34904100000006,414.3458750000001,626.241375,10000,1,0,1,10000,0,50217496,146760024,146958072,278429696,360333312,360333312,6920416,6920456,280560,351888 +final-controls,13,current,fallback,1,False,rxdb,10000,True,1000,256,network-first,0,350,14.547333000000094,49.5663330000001,53.397500000000036,404.54266600000005,613.5995,10000,1,1,1,10000,1,50214432,128650136,130809712,279101440,361840640,361840640,6920576,6920616,280048,353360 +final-controls,14,current,fallback,2,False,rxdb,10000,True,1000,256,network-first,0,350,15.138666000000057,52.66416600000002,56.220083000000045,412.79820800000005,624.8136250000001,10000,1,1,1,10000,1,50209752,128891424,128890664,278069248,365019136,365019136,6920576,6920616,279872,356464 +final-controls,15,prior-selection,fallback,2,False,rxdb,10000,True,1000,256,network-first,0,350,13.905082999999877,47.68962499999998,51.251667,412.616583,631.8535420000001,10000,1,0,1,10000,0,50211816,148266048,148608096,276578304,355352576,355336192,6920416,6920456,279120,347024 +final-controls,16,prior-selection,source-first,-1,True,powersync,10000,True,1000,256,default,300,0,314.78475000000026,548.8679999999999,548.9019170000001,7.443416999999954,548.7456670000001,10000,11,0,10,10000,0,39869008,70736632,118949304,389103616,441368576,441368576,6901957,6901997,379984,433440 +final-controls,17,current,source-first,-1,True,powersync,10000,True,1000,256,default,300,0,316.1225830000003,554.0930000000003,554.1384170000001,39.060833,553.872417,10000,1,1,1,10000,1,39865664,113014504,113013600,386547712,427622400,427622400,6902117,6901949,377488,417600 +final-controls,18,current,source-first,0,False,powersync,10000,True,1000,256,default,300,0,312.6551669999999,546.2022919999999,546.2460419999998,34.38708299999962,545.969083,10000,1,1,1,10000,1,39869584,113677352,113676448,386859008,427425792,427425792,6902117,6901949,377792,417408 +final-controls,19,prior-selection,source-first,0,False,powersync,10000,True,1000,256,default,300,0,315.41862500000025,529.655792,529.7153750000002,8.730417000000216,529.472334,10000,11,0,10,10000,0,39871576,72395488,120313880,387563520,434978816,434978816,6901957,6901789,378480,427040 +final-controls,20,prior-selection,source-first,1,False,powersync,10000,True,1000,256,default,300,0,313.7449999999999,533.1108329999997,533.1547499999997,7.216915999999856,532.9582499999997,10000,11,0,10,10000,0,39873688,72387504,120273000,388481024,441352192,441352192,6901957,6901789,379552,433216 +final-controls,21,current,source-first,1,False,powersync,10000,True,1000,256,default,300,0,314.71050000000014,528.2351250000002,528.2724579999999,30.89845800000012,528.130416,10000,1,1,1,10000,1,39870824,113701192,113700288,384385024,427868160,427868160,6902117,6901949,375376,417840 +final-controls,22,current,source-first,2,False,powersync,10000,True,1000,256,default,300,0,314.1998749999998,543.6753330000001,543.7040830000001,31.556958000000122,543.581291,10000,1,1,1,10000,1,39868656,113901304,113900400,388546560,429572096,429572096,6902117,6901949,379440,419504 +final-controls,23,prior-selection,source-first,2,False,powersync,10000,True,1000,256,default,300,0,314.65766699999995,520.5069579999999,520.565333,7.521208000000115,520.358667,10000,11,0,10,10000,0,39868984,71284544,119662400,383729664,435486720,435486720,6901957,6901789,374736,427664 +final-controls,24,prior-selection,fallback,-1,True,powersync,10000,True,1000,256,network-first,0,350,14.856333000000177,48.22658300000012,49.41020800000024,384.949333,587.0154580000003,10000,1,1,1,10000,1,39871128,113204704,113203800,386383872,429211648,429211648,6901957,6901789,377520,419152 +final-controls,25,current,fallback,-1,True,powersync,10000,True,1000,256,network-first,0,350,15.037833000000319,47.86454100000037,49.16983300000038,384.89058300000033,588.5974580000002,10000,1,1,1,10000,1,39872312,113213144,113212240,389283840,430161920,430161920,6902117,6901949,380160,420080 +final-controls,26,current,fallback,0,False,powersync,10000,True,1000,256,network-first,0,350,14.458208000000013,45.26525000000038,46.39866700000039,384.01762499999995,575.3118750000003,10000,1,1,1,10000,1,39862880,112700120,112699216,386842624,434847744,434847744,6902117,6901949,377776,424656 +final-controls,27,prior-selection,fallback,0,False,powersync,10000,True,1000,256,network-first,0,350,13.720832999999857,42.33362500000021,43.27654100000018,377.63695800000005,526.9771660000001,10000,1,1,1,10000,1,39871600,112730832,112729928,387710976,427393024,427393024,6901957,6901789,378624,417376 +final-controls,28,prior-selection,fallback,1,False,powersync,10000,True,1000,256,network-first,0,350,13.143542000000025,41.95120900000006,43.17920900000013,381.5448750000003,542.1043339999999,10000,1,1,1,10000,1,39871344,112434192,112433288,388349952,428982272,428982272,6901957,6901789,379456,418928 +final-controls,29,current,fallback,1,False,powersync,10000,True,1000,256,network-first,0,350,14.468792000000121,47.12912500000016,48.549875000000156,384.2897499999999,541.3684170000001,10000,1,1,1,10000,1,39866144,112978360,112977456,387842048,433307648,433307648,6902117,6901949,378752,423152 +final-controls,30,current,fallback,2,False,powersync,10000,True,1000,256,network-first,0,350,15.231708000000026,44.630957999999964,45.802208000000064,381.6740000000002,533.2740000000001,10000,1,1,1,10000,1,39873224,112543352,112542448,386465792,429588480,429588480,6902117,6901949,377408,419520 +final-controls,31,prior-selection,fallback,2,False,powersync,10000,True,1000,256,network-first,0,350,13.934166000000005,42.462790999999925,43.552666000000045,382.587583,545.49575,10000,1,1,1,10000,1,39866800,112744112,112743208,392314880,431702016,431702016,6901957,6901789,383120,421584 +final-large,0,prior-selection,natural,-1,True,rxdb,50000,True,1000,256,default,0,0,105.04291599999988,316.79179099999965,1745.0369999999998,878.6309159999996,1744.9279159999996,50000,1,0,1,50000,0,98660840,340973000,340972144,539394048,710082560,710082560,6920624,6920456,580320,693440 +final-large,1,current,natural,-1,True,rxdb,50000,True,1000,256,default,0,0,102.55779100000018,306.1466660000001,1764.0737500000002,870.6943750000003,1763.8039580000002,50000,1,1,1,50000,1,98564752,391516152,391515296,541982720,687390720,711147520,6920784,6920616,581968,727008 +final-large,2,current,natural,0,False,rxdb,50000,True,1000,256,default,0,0,108.76525000000015,319.34608300000014,1606.1572079999999,879.7159159999999,1606.0232910000002,50000,1,1,1,50000,1,98567360,391107768,391007576,540196864,747634688,747634688,6920784,6920616,580160,730112 +final-large,3,prior-selection,natural,0,False,rxdb,50000,True,1000,256,default,0,0,106.71833300000003,321.4960000000001,1784.593333,922.9160829999998,1784.479541,50000,1,0,1,50000,0,98568008,348091048,347787616,543539200,715194368,715194368,6920624,6920456,583488,698432 +final-large,4,prior-selection,natural,1,False,rxdb,50000,True,1000,256,default,0,0,106.49175000000014,326.71737499999995,1757.9342500000002,905.24575,1757.799,50000,1,0,1,50000,0,98597344,347256184,346991024,540459008,715964416,715964416,6920624,6920456,580704,699184 +final-large,5,current,natural,1,False,rxdb,50000,True,1000,256,default,0,0,101.96424999999999,301.30441599999995,1769.9249160000002,871.298458,1769.7228750000002,50000,1,1,1,50000,1,98567712,397851896,397851040,538230784,747290624,747290624,6920784,6920616,578240,729776 +final-large,6,current,natural,2,False,rxdb,50000,True,1000,256,default,0,0,101.57050000000004,304.3425,1714.4260000000002,872.9421669999999,1714.275292,50000,1,1,1,50000,1,98569696,393358016,393357160,540049408,753156096,753156096,6920784,6920616,580160,735504 +final-large,7,prior-selection,natural,2,False,rxdb,50000,True,1000,256,default,0,0,102.77479199999993,310.09020799999985,1667.8926249999997,867.8584579999999,1667.7757920000001,50000,1,0,1,50000,0,98666856,341236200,341065816,542883840,713359360,713359360,6920624,6920456,584048,696640 +final-large,8,prior-selection,natural,-1,True,powersync,50000,True,1000,256,default,0,0,65.5517919999993,233.71508399999948,1151.3597089999994,243.55112499999996,1151.2516249999999,50000,51,0,50,50000,0,49234392,218248416,223916560,659832832,708050944,708050944,6901957,6901789,685216,691456 +final-large,9,current,natural,-1,True,powersync,50000,True,1000,256,default,0,0,63.41950000000088,240.02879200000007,1279.851084,453.7997090000008,1279.7130420000012,50000,1,1,1,50000,1,49237288,246849640,258464008,657457152,676806656,739475456,6902117,6901949,683296,730000 +final-large,10,current,natural,0,False,powersync,50000,True,1000,256,default,0,0,62.748875000000226,220.4995829999998,1362.092291,456.8585830000002,1361.9943329999996,50000,1,1,1,50000,1,49240992,307124800,307123896,657293312,755679232,755679232,6902117,6901949,682736,737968 +final-large,11,prior-selection,natural,0,False,powersync,50000,True,1000,256,default,0,0,61.3246250000002,233.64766599999984,1174.8567080000003,241.92500000000018,1174.7210000000005,50000,51,0,50,50000,0,49241512,218053712,223799168,657948672,705347584,705347584,6901957,6901789,683376,688816 +final-large,12,prior-selection,natural,1,False,powersync,50000,True,1000,256,default,0,0,58.05879100000038,225.9402909999999,1122.4466659999998,235.29133300000012,1122.337708,50000,51,0,50,50000,0,49240872,217239656,228084264,653754368,699269120,699580416,6901957,6901789,679280,683184 +final-large,13,current,natural,1,False,powersync,50000,True,1000,256,default,0,0,56.62091700000019,193.9167499999994,1148.2332079999996,376.027333,1148.073625,50000,1,1,1,50000,1,49236288,299601144,299600240,662847488,751009792,751009792,6902117,6901949,688160,733408 +final-large,14,current,natural,2,False,powersync,50000,True,1000,256,default,0,0,52.12916700000005,180.27387499999986,1001.3796249999996,369.4567500000003,1001.2657499999996,50000,1,1,1,50000,1,49241256,298967856,298966952,657424384,745357312,745357312,6902117,6901949,682864,727888 +final-large,15,prior-selection,natural,2,False,powersync,50000,True,1000,256,default,0,0,55.567167000000154,185.98524999999972,853.1055420000002,192.6263339999996,853.0315,50000,51,0,50,50000,0,49238400,218935624,224459640,657539072,706691072,706691072,6901957,6901789,683072,690128 diff --git a/scripts/bench/persisted-startup-worker.ts b/scripts/bench/persisted-startup-worker.ts new file mode 100644 index 0000000000..4ee223af4c --- /dev/null +++ b/scripts/bench/persisted-startup-worker.ts @@ -0,0 +1,382 @@ +/** + * One process measures one persisted eager startup. Source fixtures and the + * saved SQLite baseline are prepared before the clock and forced collection. + * This is a measurement harness, not an oracle or an elapsed-time product law. + */ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { DatabaseSync } from 'node:sqlite' +import { performance } from 'node:perf_hooks' +import { createCollection } from '@tanstack/db' +import { Schema, Table, column } from '@powersync/common' +import { PowerSyncDatabase } from '@powersync/node' +import { createRxDatabase } from 'rxdb/plugins/core' +import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' +import { z } from 'zod' +import { powerSyncCollectionOptions } from '../../packages/powersync-db-collection/src/index' +import { rxdbCollectionOptions } from '../../packages/rxdb-db-collection/src/rxdb' +import { BrowserWASQLiteDriver } from '../../packages/browser-db-sqlite-persistence/src/wa-sqlite-driver' +import { + createSQLiteCorePersistenceAdapter, + persistedCollectionOptions, +} from '../../packages/db-sqlite-persistence-core/src/index' +import type { SQLInputValue } from 'node:sqlite' +import type { SyncConfig } from '@tanstack/db' +import type { PersistenceAdapter } from '../../packages/db-sqlite-persistence-core/src/index' + +type Row = { id: string; value: number; payload: string } +type Config = { + adapter: 'rxdb' | 'powersync' + rows: number + saved: boolean + batch: number + payloadBytes: number + render: 'default' | 'network-first' + restoreDelayMs: number + sourceDelayMs: number +} +const config = JSON.parse(process.argv[2]!) as Config +const directory = await mkdtemp(join(tmpdir(), `persisted-startup-bench-`)) +const id = `startup-benchmark` +const rowAt = (i: number): Row => ({ + id: `row-${String(i).padStart(8, '0')}`, + value: i, + payload: `${i}:`.padEnd(config.payloadBytes, 'x'), +}) +const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)) +let sqlite = new DatabaseSync(join(directory, 'persisted.sqlite')) +sqlite.exec(`PRAGMA journal_mode=WAL`) +sqlite.exec(`PRAGMA synchronous=NORMAL`) +let timing = false +let start = 0 +let restoreReadMs: number | undefined +let restoreHydratedMs: number | undefined +let readyMs: number | undefined +let sourceCommitMs: number | undefined +let sourceAppliedMs: number | undefined +let sourceWrites = 0 +let sourceCommits = 0 +let snapshotTransactions = 0 +let durableTransactions = 0 +let durableWrites = 0 +let durableTruncates = 0 +let maxHeap = 0 +let maxRSS = 0 +let peakExternal = 0 +const sample = () => { + const memory = process.memoryUsage() + maxHeap = Math.max(maxHeap, memory.heapUsed) + maxRSS = Math.max(maxRSS, memory.rss) + peakExternal = Math.max(peakExternal, memory.external) +} +const createAdapter = () => + createSQLiteCorePersistenceAdapter({ + driver: new BrowserWASQLiteDriver({ + database: { + execute: (sql: string, params: ReadonlyArray = []) => { + if (!/^\s*(select|with|pragma)/i.test(sql) && params.length === 0) { + sqlite.exec(sql) + return Promise.resolve([]) + } + return Promise.resolve( + sqlite + .prepare(sql) + .all( + ...(params.map((value) => + value === undefined + ? null + : typeof value === 'boolean' + ? Number(value) + : value, + ) as Array), + ) as Array, + ) + }, + }, + }), + }) +let rawAdapter = createAdapter() +let delayedRestore = false +const instrument = (adapter: PersistenceAdapter): PersistenceAdapter => + new Proxy(adapter, { + get(target, key) { + if (key === 'loadResumeSnapshot') + return async ( + ...args: Parameters + ) => { + if (timing && args[1]?.includeRows !== false && !delayedRestore) { + delayedRestore = true + if (config.restoreDelayMs) await delay(config.restoreDelayMs) + const snapshot = await target.loadResumeSnapshot(...args) + restoreReadMs = performance.now() - start + sample() + return snapshot + } + return target.loadResumeSnapshot(...args) + } + if (key === 'applyCommittedTx') + return async ( + ...args: Parameters + ) => { + if (timing) { + durableTransactions++ + durableWrites += args[1].mutations.length + if (args[1].truncate) durableTruncates++ + sample() + } + return target.applyCommittedTx(...args) + } + if (key === 'runInHydrationScope' && target.runInHydrationScope) + return ( + task: Parameters< + NonNullable + >[0], + ) => target.runInHydrationScope!((scoped) => task(instrument(scoped))) + const value: unknown = Reflect.get(target, key) + return typeof value === 'function' ? value.bind(target) : value + }, + }) +const persistence = { adapter: instrument(rawAdapter) } +const seed = createCollection( + persistedCollectionOptions({ + id, + getKey: (row) => row.id, + persistence, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + if (config.saved) + for (let i = 0; i < config.rows; i++) + write({ type: 'insert', value: rowAt(i) }) + commit() + markReady() + }, + }, + }), +) +await seed.preload() +await seed.cleanup() +// A new wrapper adapter and connection replay an app reopen instead of reusing +// the seeding adapter's in-memory schema and snapshot state. OS file cache is +// still warm; this harness does not attempt to flush the operating system. +sqlite.close() +sqlite = new DatabaseSync(join(directory, 'persisted.sqlite')) +sqlite.exec(`PRAGMA journal_mode=WAL`) +sqlite.exec(`PRAGMA synchronous=NORMAL`) +rawAdapter = createAdapter() +persistence.adapter = instrument(rawAdapter) +let disposeSource: () => Promise +let sourceSync: SyncConfig +const rowSchema = z.object({ + id: z.string(), + value: z.number(), + payload: z.string(), +}) +if (config.adapter === 'rxdb') { + const database = await createRxDatabase({ + name: 'startupbenchmark', + storage: getRxStorageMemory(), + multiInstance: false, + }) + const { rows } = await database.addCollections({ + rows: { + schema: { + version: 0, + type: 'object', + primaryKey: 'id', + properties: { + id: { type: 'string', maxLength: 30 }, + value: { type: 'number' }, + payload: { type: 'string' }, + }, + required: ['id', 'value', 'payload'], + }, + }, + }) + for (let first = 0; first < config.rows; first += 1000) { + const result = await rows.bulkInsert( + Array.from({ length: Math.min(1000, config.rows - first) }, (_, i) => + rowAt(first + i), + ), + ) + if (result.error.length) throw new Error('RxDB source seeding failed') + } + const query = rows.storageInstance.query.bind(rows.storageInstance) + let initialQuery = true + rows.storageInstance.query = async (prepared) => { + if (initialQuery) { + initialQuery = false + if (config.sourceDelayMs) await delay(config.sourceDelayMs) + } + const result = await query(prepared) + if (timing) sample() + return result + } + sourceSync = rxdbCollectionOptions({ + id, + rxCollection: rows, + syncBatchSize: config.batch, + syncMode: 'eager', + }).sync + disposeSource = () => database.remove() +} else { + const schema = new Schema({ + rows: new Table({ value: column.integer, payload: column.text }), + }) + const database = new PowerSyncDatabase({ + database: { + dbFilename: 'source.sqlite', + dbLocation: directory, + implementation: { type: 'node:sqlite' }, + }, + schema, + }) + await database.writeTransaction(async (tx) => { + for (let i = 0; i < config.rows; i++) { + const row = rowAt(i) + await tx.execute( + 'INSERT INTO rows (id, value, payload) VALUES (?, ?, ?)', + [row.id, row.value, row.payload], + ) + } + }) + sourceSync = powerSyncCollectionOptions({ + id, + database, + table: schema.props.rows, + schema: rowSchema, + syncBatchSize: config.batch, + onDeserializationError: (error) => { + throw error + }, + onLoad: () => + config.sourceDelayMs ? delay(config.sourceDelayMs) : undefined, + }).sync + disposeSource = () => database.close() +} +let resolveSource!: () => void +let rejectSource!: (error: unknown) => void +const sourceResult = new Promise((resolve, reject) => { + resolveSource = resolve + rejectSource = reject +}) +const receipts: Array> = [] +const sync: SyncConfig = { + ...sourceSync, + sync: (params) => { + if (params.metadata?.persistence) + void params.metadata.persistence.hydrateBaseline().then(() => { + restoreHydratedMs = performance.now() - start + sample() + }, rejectSource) + return sourceSync.sync({ + ...params, + begin: (options) => { + if (options?.snapshot) snapshotTransactions++ + params.begin(options) + }, + write: (message) => { + sourceWrites++ + params.write(message) + if (sourceWrites % 512 === 0) sample() + }, + commit: (signal) => { + sourceCommits++ + sourceCommitMs ??= performance.now() - start + const receipt = params.commit(signal) + receipts.push(receipt) + sample() + return receipt + }, + fail: (error) => { + rejectSource(error) + params.fail(error) + }, + markReady: () => { + params.markReady() + void Promise.all(receipts).then(() => { + sourceAppliedMs = performance.now() - start + sample() + resolveSource() + }, rejectSource) + }, + }) + }, +} +const collection = createCollection( + persistedCollectionOptions({ + id, + getKey: (row) => row.id, + persistence, + sync, + ...(config.render === 'network-first' + ? { initialRender: { strategy: 'network-first', networkTimeoutMs: 25 } } + : {}), + }), +) +global.gc?.() +const before = process.memoryUsage() +sample() +const rssHighWaterBeforeKiB = process.resourceUsage().maxRSS +timing = true +start = performance.now() +const sampler = setInterval(sample, 2) +try { + const ready = collection.preload().then(() => { + readyMs = performance.now() - start + sample() + }) + await Promise.all([ready, sourceResult]) + sample() + clearInterval(sampler) + const after = process.memoryUsage() + const rssHighWaterKiB = process.resourceUsage().maxRSS + timing = false + if (collection.size !== config.rows) + throw new Error( + `Collection has ${collection.size} rows, expected ${config.rows}`, + ) + const stored = await rawAdapter.loadResumeSnapshot( + { kind: 'eager', collectionId: id }, + { includeRows: true }, + ) + if (stored.rows.length !== config.rows) + throw new Error( + `SQLite has ${stored.rows.length} rows, expected ${config.rows}`, + ) + console.log( + `STARTUP_RESULT ${JSON.stringify({ + ...config, + restoreReadMs, + restoreHydratedMs, + readyMs, + sourceCommitMs, + sourceAppliedMs, + sourceWrites, + sourceCommits, + snapshotTransactions, + durableTransactions, + durableWrites, + durableTruncates, + heapBefore: before.heapUsed, + heapAfter: after.heapUsed, + sampledHeapMax: maxHeap, + rssBefore: before.rss, + rssAfter: after.rss, + sampledRSSMax: maxRSS, + externalBefore: before.external, + sampledExternalMax: peakExternal, + rssHighWaterBeforeKiB, + rssHighWaterKiB, + })}`, + ) +} finally { + clearInterval(sampler) + timing = false + await collection.cleanup() + await disposeSource() + sqlite.close() + await rm(directory, { recursive: true, force: true }) +} diff --git a/scripts/bench/persisted-startup.mjs b/scripts/bench/persisted-startup.mjs new file mode 100644 index 0000000000..0ba02169f4 --- /dev/null +++ b/scripts/bench/persisted-startup.mjs @@ -0,0 +1,191 @@ +// Process-isolated, paired measurements of the PR's persisted snapshot choice. +// Example: node scripts/bench/persisted-startup.mjs --rows=1000,10000 --repetitions=5 --out=.review-2367/startup-perf/results.json +import { spawnSync } from 'node:child_process' +import { readFile, mkdir, writeFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import path from 'node:path' +import os from 'node:os' +import { build } from 'esbuild' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') +const args = Object.fromEntries( + process.argv.slice(2).map((arg) => arg.replace(/^--/, '').split('=')), +) +const rows = (args.rows ?? '1000,10000,100000').split(',').map(Number) +const repetitions = Number(args.repetitions ?? 5) +const warmup = Number(args.warmup ?? 1) +const adapters = (args.adapters ?? 'rxdb,powersync').split(',') +const savedStates = (args.saved ?? 'false,true') + .split(',') + .map((value) => value === 'true') +const schedules = (args.schedules ?? 'natural,source-first,fallback').split(',') +const out = path.resolve( + root, + args.out ?? '.review-2367/startup-perf/results.json', +) +const directory = path.dirname(out) +await mkdir(directory, { recursive: true }) +const variants = ['prior-selection', 'current'] +const workspace = { + '@tanstack/db': path.join(root, 'packages/db/src/index.ts'), + '@tanstack/db-ivm': path.join(root, 'packages/db-ivm/src/index.ts'), +} +for (const variant of variants) { + let changed = 0 + await build({ + entryPoints: [path.join(root, 'scripts/bench/persisted-startup-worker.ts')], + outfile: path.join(directory, `${variant}.mjs`), + bundle: true, + minify: true, + sourcemap: 'inline', + format: 'esm', + platform: 'node', + tsconfigRaw: '{}', + define: { 'process.env.NODE_ENV': '"production"' }, + plugins: [ + { + name: 'startup-source-control', + setup(bundle) { + bundle.onResolve({ filter: /^[^./]/ }, async (request) => { + if (request.pluginData?.resolved) return + if (workspace[request.path]) + return { path: workspace[request.path] } + if (request.path.startsWith('node:')) + return { path: request.path, external: true } + const resolveDir = request.path.startsWith('@powersync/') + ? path.join(root, 'packages/powersync-db-collection') + : request.path.startsWith('rxdb/') + ? path.join(root, 'packages/rxdb-db-collection') + : request.resolveDir + const resolved = await bundle.resolve(request.path, { + resolveDir, + kind: request.kind, + pluginData: { resolved: true }, + }) + if (resolved.errors.length) return { errors: resolved.errors } + return { path: resolved.path, external: true } + }) + if (variant === 'prior-selection') + bundle.onLoad( + { + filter: + /packages\/(rxdb|powersync)-db-collection\/src\/(rxdb|powersync)\.ts$/, + }, + async ({ path: source }) => { + const current = await readFile(source, 'utf8') + const original = current.replace( + /persistence !== null \|\|\s*!collection\._state\.acceptedSyncedEntries\(\)\.next\(\)\.done/, + '!collection._state.acceptedSyncedEntries().next().done', + ) + if (original === current) + throw new Error( + `Snapshot source control did not match ${source}`, + ) + changed++ + return { contents: original, loader: 'ts' } + }, + ) + }, + }, + ], + }) + if (variant === 'prior-selection' && changed !== 2) + throw new Error( + `Expected two controlled snapshot selections, got ${changed}`, + ) +} +const sourceHead = spawnSync('git', ['rev-parse', 'HEAD'], { + cwd: root, + encoding: 'utf8', +}).stdout.trim() +const report = { + sourceHead, + baseline: + 'Current source/dependencies with only the persisted-capability OR operand removed from RxDB/PowerSync eager snapshot selection, matching origin/main selection. Not a full origin/main checkout.', + runtime: { + node: process.version, + platform: process.platform, + arch: process.arch, + cpus: os.cpus()[0]?.model, + logicalCPUs: os.cpus().length, + totalMemoryBytes: os.totalmem(), + }, + configuration: { + rows, + repetitions, + warmup, + adapters, + savedStates, + schedules, + payloadBytes: Number(args.payloadBytes ?? 256), + batch: Number(args.batch ?? 1000), + }, + observations: [], +} +let pair = 0 +for (const adapter of adapters) + for (const count of rows) + for (const saved of savedStates) + for (const schedule of schedules) { + if (schedule === 'fallback' && !saved) continue + const config = { + adapter, + rows: count, + saved, + batch: report.configuration.batch, + payloadBytes: report.configuration.payloadBytes, + render: schedule === 'fallback' ? 'network-first' : 'default', + restoreDelayMs: schedule === 'source-first' ? 300 : 0, + sourceDelayMs: schedule === 'fallback' ? 350 : 0, + } + for (let repetition = -warmup; repetition < repetitions; repetition++) { + const order = pair++ % 2 ? [...variants].reverse() : variants + for (const variant of order) { + const child = spawnSync( + process.execPath, + [ + '--expose-gc', + '--enable-source-maps', + path.join(directory, `${variant}.mjs`), + JSON.stringify(config), + ], + { + cwd: root, + encoding: 'utf8', + timeout: 180000, + maxBuffer: 8 * 1024 * 1024, + }, + ) + if (child.status !== 0) + throw new Error( + `${variant} ${JSON.stringify(config)} failed:\n${child.stdout.slice(-2000)}\n${child.stderr + .split('\n') + .slice(-15) + .map((line) => line.slice(0, 600)) + .join('\n')}`, + ) + const line = child.stdout + .split('\n') + .find((item) => item.startsWith('STARTUP_RESULT ')) + if (!line) + throw new Error( + `No result: ${child.stdout.slice(-2000)}\n${child.stderr + .split('\n') + .slice(-15) + .map((line) => line.slice(0, 600)) + .join('\n')}`, + ) + const result = { + variant, + schedule, + repetition, + warmup: repetition < 0, + ...JSON.parse(line.slice('STARTUP_RESULT '.length)), + } + report.observations.push(result) + await writeFile(out, `${JSON.stringify(report, null, 2)}\n`) + console.log(JSON.stringify(result)) + } + } + } +console.log(`Saved ${report.observations.length} observations to ${out}`) From 76e3a7fbeaf5bc3127a06191a15bd1229412a7b3 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 13:27:49 -0700 Subject: [PATCH 17/24] test: integrate persisted startup campaigns with oracleProperty Preserve the startup, reload, capability, precedence and scope judgments while using randomized constructions and enforcing generated-state obligations. Retain fixed histories, source paths, observation cuts and base run budgets. Validation: 710 startup, 400 Query, 88 reload and 92 capability registrations; 226 real Chromium/OPFS receiving histories; seven selected Query source kills; two historical reload source kills; direct replay, types, format and lint. An independent construction probe checked 12,300 outputs without production. The final same-seed CLI retention run remains in progress. Four survivors and two historical timeouts remain open; no maintainer allowance is assumed. Production source remains unchanged from 5feb266. Older primary owner and browser registration conversions are separate work, not discharged here. --- docs/contributing/oracle-coverage.md | 13 + .../2026-11-10-adapter-reload-gaps.md | 75 +++ ...-11-10-query-oracle-property-conversion.md | 225 +++++++ ...026-11-10-startup-framework-integration.md | 86 +++ .../2026-11-10-startup-property-conversion.md | 166 +++++ .../tests/eager-reload-oracle-suite.ts | 333 +++++++--- .../tests/eager-reload-oracle.ts | 92 +-- .../persisted-capability-oracle-suite.ts | 63 +- .../tests/persisted-capability-oracle.ts | 3 +- .../tests/persisted-startup-oracle-suite.ts | 579 +++++++++++------- .../tests/persisted-startup-oracle.ts | 40 +- packages/db/tests/oracle-config.ts | 4 + packages/db/tests/oracle-replay-manifest.ts | 25 +- .../tests/persisted-precedence-driver.ts | 18 +- .../tests/persisted-precedence-oracle.test.ts | 377 +++++++----- .../tests/persisted-precedence-oracle.ts | 51 +- ...rsisted-startup-conformance-oracle.test.ts | 82 +-- .../tests/retained-scoped-hydration-driver.ts | 40 +- .../retained-scoped-hydration-oracle.test.ts | 495 +++++++++------ .../tests/retained-scoped-hydration-oracle.ts | 14 +- 20 files changed, 1968 insertions(+), 813 deletions(-) create mode 100644 docs/contributing/oracle-reviews/2026-11-10-query-oracle-property-conversion.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-startup-framework-integration.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-startup-property-conversion.md diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index c47663a4cb..fd902f85e9 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -7909,6 +7909,19 @@ Timeouts supply no behavioral rejection evidence. Startup latency/memory are measured separately in the [performance report](oracle-reviews/2026-11-10-persisted-startup-measurements.md). These additions preserve the startup comparisons and their limits above. +The [framework integration record](oracle-reviews/2026-11-10-startup-framework-integration.md) +and the [startup conversion record](oracle-reviews/2026-11-10-startup-property-conversion.md) +track conversion of the five added/shared campaign files to `oracleProperty`, +including randomized constructed histories, per-builder missing-state controls, +unchanged base run budgets and direct replay. The post-conversion Chromium +receiver passes all 226 added histories through the same judgments. The +[Query conversion record](oracle-reviews/2026-11-10-query-oracle-property-conversion.md) +preserves seven exact source-operator rejections and updated replay coordinates. +Under the current ratchet, the four surviving operators above remain open gaps; +the bounded-equivalence evidence grants no allowance. Legacy ownership-lifecycle +and TrailBase owner conversions remain separate obligations before further +production edits. No whole-PR readiness claim follows from these conversions. + ## Adapter conformance: eager live sources (#2118) `packages/db-adapter-conformance/src/eager-live-source-oracle.ts` owns the diff --git a/docs/contributing/oracle-reviews/2026-11-10-adapter-reload-gaps.md b/docs/contributing/oracle-reviews/2026-11-10-adapter-reload-gaps.md index 4a7769fb88..6a5dfe46e6 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-adapter-reload-gaps.md +++ b/docs/contributing/oracle-reviews/2026-11-10-adapter-reload-gaps.md @@ -98,3 +98,78 @@ replication, persisted durability, arbitrary concurrent mutations, every intermediate publication, or on-demand reload. The existing owners retain those contracts. Worker/OPFS startup coverage remains a neighboring persisted path; wrapping this receiver in persistence would mask the operand under judgment. + +## oracleProperty conversion after main merge + +Conversion base: `29de8a92e`, with exact installed `@tanstack/oracles` and ESLint +plugin pin `e1fb504`. No production function changed. The shared reload and +capability registrations now use `oracleProperty`; no legacy allowance was +added. Existing Chromium receivers still import the same raw models and +comparisons, and no observation cut or SDK path changed. + +The pure row computation was extracted as `eagerReloadModel`. The raw checker +uses it by default, and the framework supplies that same model explicitly. +The capability framework similarly uses the existing `capabilityMustAccept` +model and passes its answer into the same raw admission checker. The capability +source remains a complete finite enumeration of 22 histories on each of the +controlled and Node SQLite hosts for each adapter. There is no new random +capability campaign. + +The reload grammar retains the exact three equally weighted lanes, next masks +0..7 and mutation boolean. Its former dependent `chain/map` could not validate +any of the 24 external fixed histories with `canShrinkWithoutContext`; the +flattened full-record grammar validates all 24. This changes generator +representation, not legal histories. All 24 fixed histories stay. Each fixed +seed 2367 and fresh-seed campaign keeps its 80-run budget. + +Eleven randomized constructed builders each require one original declared +state. They constrain only the premise coordinates and retain the remaining +generated membership/mutation coordinates. Each call returns a fresh object. +A local WeakMap records each built history's required state; the same check +requires that state from actual production-driver reach observations on both +the framework's example constructions and its generated construction branch. +This closes the pinned API's gap where generated map outputs lose their +builder association. Each builder also has a missing-state calibration, which +runs the real SDK driver and removes that state's observation before the same +check. Literal fixed histories do not supply required generated reach. + +The first framework run passed 169 of 170 checks, including all row comparisons +and capability cases. PowerSync's determinism check exposed a lawful schedule +difference: on an empty reload, `markReady` after acceptance can fulfill before +or after the external optimistic gate releases. One raw trace included the +pending optimistic row at fulfillment, and the other did not after settlement. +Both satisfy the unchanged accepted-versus-applied and overlay laws. Waiting +unconditionally for `markReady` would instead deadlock RxDB's ordinary commit, +which awaits visibility behind that optimistic transaction. + +The framework bridge therefore applies the unchanged raw law checker to every +actual raw observation before serialization. Successful allowed schedules +have one canonical checked record. Failed judgments retain every raw field, +and the framework check reruns the same raw comparison against the same pure +model before reporting them. This projection discards only differences the +existing law already permits after they have been checked. The real SDK driver +and raw browser comparisons remain unchanged. Five additional controls corrupt +raw before-source rows, fulfillment rows, final rows, a pending optimistic +overlay, and source acceptance before projection; each must still fail the raw +comparison. The exact historical source mutants are also tested after this +projection, so a canonical success cannot hide their stale rows. + +Post-conversion evidence is recorded under `.review-2367/reload-migration/`. +The exact M9/M164 source copies still fail the same retained-row → empty-result +fixed history (`previous: 1`, `next: 0`, no mutation) with stale `a` at +fulfillment and final observation. See `exact-mutants-normalized.log`. The +counterexample, law and captured raw diagnostics survive the conversion. +Final conversion results: + +- `pnpm --config.verifyDepsBeforeRun=false exec vitest run packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts` passes **88 checks** (`green-normalized.log`). Each receiver retains 24 fixed histories and the 80-run fixed/fresh campaigns, and passes determinism plus all 17 calibrations: 11 builder-state omissions, five wrong raw answers before projection, and the original inverted-snapshot source declaration. +- Each campaign runs its 11 seed-derived constructions in addition to 80 generated histories. Both fixed campaigns report the same positive counts: changed overlap 19, empty result 20, fresh 23, fresh-held 16, no mutation 37, omission 25, mutation 54, empty reload 32, empty-reload-held 14, retained reload 36, retained-reload-held 24. Both fresh campaigns also reach all 11 states; every required minimum remains one. +- `pnpm --config.verifyDepsBeforeRun=false exec vitest run packages/rxdb-db-collection/tests/persisted-capability-conformance-oracle.test.ts packages/powersync-db-collection/tests/persisted-capability-conformance-oracle.test.ts` passes **92 checks** (`capability-green.log`): all 22 cases and determinism separately for each adapter and each of the two hosts. +- Both registered reload properties pass guarded replay with seed 42/path 0, one actual property execution and one run. The replay selector now names the framework's `random seed` test. See `replay-rxdb.log` and `replay-powersync.log`. +- The shared conformance package and both receiving packages pass `tsc --noEmit`; targeted ESLint and `pnpm --config.verifyDepsBeforeRun=false lint:changed` pass. See `types-normalized.log`, `receiver-types.log`, `lint-normalized.log`, and `lint-changed.log`. +- Exact source-mutant command after conversion: `pnpm --config.verifyDepsBeforeRun=false exec vitest run --config .review-2367/reload-gaps/mutant.config.ts packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts -t 'fixed history:.*"previous":1,"next":0,"mutation":false'`. Both fail under law `complete eager result`, preserving the same raw stale-row witness as before conversion. This is the same historical operand-negation kill on each adapter, not an exception or equivalent-mutant allowance. + +The parent run retains the existing real Chromium receiving gate and owns the +combined same-seed CLI comparison and final branch lint after the parallel +conversions. Those broader checks are separate from the 180 passing receiver +checks above. There are no pending edits to the models, drivers, suites, or +source for this conversion. diff --git a/docs/contributing/oracle-reviews/2026-11-10-query-oracle-property-conversion.md b/docs/contributing/oracle-reviews/2026-11-10-query-oracle-property-conversion.md new file mode 100644 index 0000000000..11913a37c9 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-query-oracle-property-conversion.md @@ -0,0 +1,225 @@ +# Query receiving properties: campaign API conversion + +This additive record reviews the conversion on merge base +`29de8a92e3aae74dc4c1c2c2d2d47e6edd44dc86`, which merged `origin/main` +`caa936d5a`. It uses the exact `@tanstack/oracles` and lint packages at +`e1fb504`. Query production is unchanged. Its `src/query.ts` SHA256 remains +`6bbf1542e6daa38f33315445c5bb0ae3fbbf90d907cd388cb5d13f41c3b37cc5`. + +This record supersedes campaign counts and replay coordinates in the +[precedence](2026-11-10-persisted-precedence-gaps.md) and +[scope/provenance](2026-11-10-retained-scoped-hydration-gaps.md) records for +these converted runners. Those earlier experiments remain historical evidence. +It does not reclassify the full-package CLI audit or establish PR readiness. + +## Preserved laws and receiving paths + +The conversion retains three independent judgments and their authorities: + +- `query.persisted-write-precedence`: an older result preserves newer accepted + direct writes and removes omitted saved rows; a later physical fetch may + replace those writes. Authority: Query guide, **Direct Writes and Query Sync** + and **Individual Write Operations**. +- `query.retained-scoped-hydration`: on-demand restore exposes demanded saved + rows; complete omission is local to its certified scope. A limited result + cannot certify an ownerless direct row absent. Authority: SQLite guide, + **Persist a collection**, and Query guide, **QueryFn and Predicate Push-Down**. +- `query.cached-peer-claim`: cached first acquisition cannot replace a + continuously owned value from a later physical fetch. A subsequent physical + fetch may replace it. Authority: Query guide, **Direct Writes and Query Sync**. + +The independent computations remain a keyed write overlay, set selection over +two authored scopes, and three authored values in causal order. Extracting +`expectedPersistedPrecedence` and `expectedCachedPeerClaim` exposes the existing +expectations to the framework; it changes no expected rule. The precedence +arbitrary changes from mapped tuples to records with the same language and +branch weights, so fixed and constructed objects have a valid shrink context. + +The real QueryClient, Query Collection, persistence wrapper, controlled storage, +and real Node SQLite paths remain. Optional recorder callbacks capture existing +public, durable and cache observations at their original cuts. Prefix comparisons +run before completion checks, preserving an earlier row mismatch if later work +fails. Existing error-preserving cleanup still closes collections and hosts. + +The first framework run exposed an observation determinism error: the old +precedence snapshot spread retained an unjudged virtual `$collectionId`. +Explicit `{ id, value }` projection fixes that capture error. The previous law +already excluded virtual properties; no judged field was removed. That initial +392/394 result supplies no clean GREEN evidence. + +Chromium continues to use the same shared histories, drivers and comparisons +through actual worker SQLite/OPFS: 104 precedence histories and 50 scope/cache +histories within the parent's 226-history receiving run. The earlier parent run +passed in 50.6 seconds. The parent owns the final post-conversion browser rerun; +this record does not claim that later execution. + +## Campaigns and enforced reach + +Both Node hosts register each property through `oracleProperty`. Each normal +registration has a fixed-seed and random campaign with identical machinery and +run budget. Fixed histories run separately and provide no required reach. + +| Property | Base budget per campaign | Fixed seed | Fixed histories per host | Randomized builders | Histories counted per campaign | +| --- | ---: | ---: | ---: | ---: | ---: | +| persisted-write-precedence | 80 | 2394 | 104 | 15 | 95 | +| retained-scoped-hydration | 60 | 1481 | 48 | 12 | 72 | +| cached-peer-claim | 60 | 1376 | 2 | 4 | 64 | + +Each builder preserves random neighboring dimensions and returns a fresh object. +Each state has one builder and a minimum of one constructed entry per campaign. +Every builder has its own missing-state calibration. Because the pinned framework +does not associate generated construction outputs with their `enters` obligations, +a local WeakMap retains those obligations. The shared check rejects an output +whose production-driver trace omits its required state. This also covers builders +invoked during generated runs, rather than only their seed-derived constructions. + +The final normal run passed **400/400 tests in 25.76 seconds**. These are test +registrations, not 400 histories. `C` means controlled storage; `N` means Node +SQLite. Fixed-seed counts agree between hosts. Random counts are independent. + +| Precedence state | Fixed C/N | Random C | Random N | +| --- | ---: | ---: | ---: | +| after-read | 18 | 16 | 24 | +| before-fetch | 15 | 15 | 6 | +| before-read | 27 | 25 | 27 | +| before-restore | 19 | 22 | 28 | +| before-result | 16 | 17 | 10 | +| cached-result | 30 | 27 | 34 | +| delete | 24 | 12 | 12 | +| derived | 49 | 50 | 55 | +| during-baseline | 45 | 41 | 51 | +| fetched-result | 65 | 68 | 61 | +| insert | 26 | 33 | 30 | +| later-fetch | 55 | 47 | 51 | +| raw | 46 | 45 | 40 | +| update | 13 | 14 | 10 | +| upsert | 32 | 36 | 43 | + +| Scoped hydration state | Fixed C/N | Random C | Random N | +| --- | ---: | ---: | ---: | +| empty-result | 17 | 23 | 14 | +| first-a | 37 | 31 | 38 | +| first-b | 35 | 41 | 34 | +| limited | 37 | 43 | 37 | +| limited-ownerless-restored | 20 | 17 | 16 | +| no-ownerless | 18 | 22 | 20 | +| nonempty-result | 55 | 49 | 58 | +| omitted-owned-row | 37 | 35 | 33 | +| ownerless-first | 30 | 24 | 28 | +| ownerless-other | 24 | 26 | 24 | +| unbounded | 35 | 29 | 35 | +| unrequested-saved-rows | 52 | 56 | 48 | + +| Cached claim state | Fixed C/N | Random C | Random N | +| --- | ---: | ---: | ---: | +| different-cached-value | 61 | 60 | 61 | +| later-value-change | 61 | 62 | 60 | +| manual | 35 | 33 | 33 | +| older-fetch | 29 | 31 | 31 | + +## Preserved mutation sensitivity and guarded replay + +An immutable per-process transformer validates the original operator and source +position, logs the applied SHA, disables filesystem module caching, and assigns +each operator a distinct plugin/cache identity. It never edits production files. +The same seven stable operators rejected before conversion still fail the +intended public or durable law after conversion: + +| Operators | Wrong design | Property | Seed | New shrink path | +| --- | --- | --- | ---: | --- | +| M558/M559 | Delete an ownerless restored row from a limited empty result | retained-scoped-hydration | 1481 | `0:1:1:1:1:2` | +| M1481 | Publish the unrequested saved scope through full baseline hydration | retained-scoped-hydration | 1481 | `2:1:0:1:1` | +| M1376/M1377 | Let stale cached first acquisition replace the later-fetched value | cached-peer-claim | 1376 | `0:0:1:0:1` | +| M1748 | Skip derived-select baseline reconciliation, retaining an omitted saved row | persisted-write-precedence | 2394 | `1:1` | +| M1793 | Let a cached old result overwrite a newer direct write | persisted-write-precedence | 2394 | `3:1:1` | + +Every mutant's guarded replay records the intended `OracleViolation` and two +failed execution witnesses, one per host. Clean production passes those same +five distinct coordinates with two successful execution witnesses each. Tests +selected by `fixed seed|random seed|fixed history` run 320 law registrations and +exclude 80 framework calibration/determinism registrations in the final suite. +Historical pre-calibration logs correctly report their earlier counts. + +The three previous bounded survivors, M563/M1473/M1794, still pass these two +receiving runners. They remain open mutant gaps under the new policy. The prior +bounded-equivalence arguments are explanations, not closure or maintainer +allowances. Prior full-package timeouts M1468/M1469 finish GREEN in these +receivers in 7.9/8.2 seconds; that does not reclassify the earlier 173-second +full-package timeouts, whose ownership suites differ. + +This is a selected stable-operator before/after check, not a newly complete CLI +score. The global audit owns those classifications and remaining gaps. + +## Generated-construction enforcement controls + +Three immutable controls suppress only one reach observation, leaving real row +observations correct. Each chosen history is absent from that campaign's +seed-derived constructions. Therefore rejection specifically demonstrates the +local generated-output obligation check, beyond the framework's seeded check. + +| Property | Generated history distinction | Missing state | Seed | Shrink path | +| --- | --- | --- | ---: | --- | +| persisted-write-precedence | derived/upsert, before fetch, value -8, no later fetch | before-fetch | 2394 | `16` | +| retained-scoped-hydration | first a, unbounded, empty saved masks, next mask 3, ownerless other, value -8 | nonempty-result | 1481 | `13:7` | +| cached-peer-claim | manual cache 9, current -1, later 9 | manual | 1376 | `7:9` | + +All three controls fail **constructed reach** at **constructed states** on both +hosts. Their guarded replays retain that law/checkpoint and two failed witnesses. +Clean replays at all three coordinates record two successful witnesses. The +per-builder missing-state calibrations also pass in the normal 400-test run. +Separate public and durability-only wrong-observation controls prove neither +channel can be ignored. An initial scratch-config escaping error is retained +separately and supplies no rejection evidence. + +## Commands and evidence + +From the worktree root, the normal run is: + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/query-db-collection exec vitest run --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false tests/persisted-precedence-oracle.test.ts tests/retained-scoped-hydration-oracle.test.ts +``` + +From `packages/query-db-collection`, guarded clean replay uses the property ID, +campaign seed and new path from either table: + +```sh +TANSTACK_DB_ORACLE_SEED=2394 TANSTACK_DB_ORACLE_PATH=1:1 TANSTACK_DB_ORACLE_PROPERTY=query.persisted-write-precedence node --import tsx ../db/tests/oracle-replay.ts tests/persisted-precedence-oracle.test.ts tests/retained-scoped-hydration-oracle.test.ts --maxWorkers=2 --coverage.enabled=false --typecheck.enabled=false +``` + +Logs remain in `.review-2367/gap-fill/`: `query-property-green-final.log`, +`query-property-M*.log`, `query-property-M*-replay.log`, +`query-property-*-clean-replay.log`, and +`query-property-generated-omit-*-replay.log`. Exact generated histories live in +`query-property-generated-choices.json`. Typecheck logs are +`query-property-types.log` and `query-property-browser-types.log`. +Final Query/browser TypeScript, scoped ESLint, Prettier, `lint:changed` and +`git diff --check` all pass. Their final logs use the `query-property-*-final` +prefix. No run budget was lowered and no legacy exemption was added. + +## Review scope and remaining obligations + +ORC-001/002/003 retain the documented laws, independent judgments and literate +roles. ORC-004 retains the grammar domains, reconstruction and ablation witnesses +in the prior reports; constructed outputs must also belong to that language. +ORC-005 retains the real receiving paths and original public observation cuts. +ORC-006 preserves seven assertion kills and distinct public/durable calibrations. +ORC-007 has parity campaigns and guarded RED/GREEN replay. ORC-008's stateful +model change trigger does not apply. ORC-009 preserves existing vocabulary and +declared projections. ORC-010 compares captured prefixes before completeness +and retains cleanup diagnostics. No concrete shared semantic fault requiring +an ORC-011 alternate formulation was identified; this is no claim that none +exists. This record supplies ORC-012 evidence. ORC-013 retains nearby scope, +window, causal-order and omission distinctions. ORC-014 retains actual SQLite +and the shared Chromium receiving path, with the final browser rerun assigned +to the parent. ORC-015 now enforces both seeded and generated construction +obligations with missing-state controls. ORC-016 keeps fixed histories and +calibrations within the shared judgments, without standalone expected examples. + +The receiving grammars remain finite: one cold eager reopen and one direct +write, two disjoint on-demand scopes, or one continuously owned overlapping key; +successful I/O and numeric values. Arbitrary overlapping predicates, peer +writers, failed I/O, optimistic transactions in these added receivers, and all +intermediate publications remain outside their evidence. Existing ownership, +startup, recovery and direct-write-window owners retain their separate laws. +Their legacy campaign conversions are not discharged by converting these two +new runners. No production repair or global bug-class closure is claimed here. diff --git a/docs/contributing/oracle-reviews/2026-11-10-startup-framework-integration.md b/docs/contributing/oracle-reviews/2026-11-10-startup-framework-integration.md new file mode 100644 index 0000000000..3074d3daa6 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-startup-framework-integration.md @@ -0,0 +1,86 @@ +# Persisted startup: framework integration + +This append-only record follows the gap-fill commit +`b26629dfe6fb2ff910cc7141fdc0d78e436f5b5a` and merge +`29de8a92e3aae74dc4c1c2c2d2d47e6edd44dc86`. The merge incorporates main's +`caa936d5a` and the exact oracle framework at +`e1fb5048c6d33e958643360d87e168c302e03bd1`. It changes no shipped production +source from the measured `5feb26616a8357853454c2f9ff2ce0b0adb9c908`. + +## Campaign integration + +Five campaign files now use `oracleProperty`: shared persisted startup, +shared eager reload, shared persistence-capability admission, Query persisted +write precedence, and Query retained-scope/cache provenance. Their existing +models, authorities, fixed histories, receiving paths, observation cuts and +base run budgets remain. No entry was added to the frozen legacy list. + +The conversions retain generated variation in constructed histories, fresh +object identity, and a local obligation for each generated construction. Each +state has a missing-state control. Fixed histories supply no required reach. +The records below explain the pinned framework's generated-construction map +limitation and the local checks that prevent a false green. + +- [Startup conversion](2026-11-10-startup-property-conversion.md): 710 checks + across six adapters and two Node hosts; every state reached separately in + all 24 host/campaign runs. The shared independent set judgment still runs + at the original public and durable cuts. +- [Query conversion](2026-11-10-query-oracle-property-conversion.md): 400 + checks across three properties and both Node hosts; all seven previously + rejected selected source operators still fail at their intended cuts. + Eight distinct guarded replay coordinates pass on clean source and fail + their respective source or generated-reach controls. +- [Reload/capability conversion](2026-11-10-adapter-reload-gaps.md#oracleproperty-conversion-after-main-merge): + 88 reload and 92 capability checks; both historical snapshot mutants remain + rejected. A determinism failure exposed permitted SDK/optimistic settlement + schedules. The unchanged raw checker judges each actual schedule before + the framework records its verdict. Five pre-projection corruptions show + that this abstraction preserves the original observation obligations. + +The post-conversion Chromium run passes all 226 receiving histories in 3.1 +minutes. It uses the existing Playwright witness, real Chromium, worker SQLite +and OPFS, the same shared judgments, and six separately checked reach groups: +104 precedence, 48 retained scope, two cached peer, 24 TrailBase hydration, +24 RxDB reload and 24 PowerSync reload. All required counters are positive; +the spec also checks browser errors and fixture cleanup. This is receiving +evidence, not a claim that importing the Vitest constructor converts a +Playwright registration. The log is +`.review-2367/gap-fill/opfs-after-framework.log`. + +A separate read-only agent reviewed these conversions against `b26629dfe` and +found no supported enforcement regression. Its in-memory registration probe +checked 12,300 constructed outputs for grammar membership, fresh identity, +declared-state entry, retained variation and missing-state controls. Each +builder retained 8–100 distinct outputs over 100 generated inputs. This probe +checks instrument construction; it invokes no production and supplies no +replacement for the real-host or mutation evidence. + +## Remaining ratchet work + +Main's new policy requires a kill or explicit maintainer allowance for a gap +in a changed function. Bounded equivalence does not close it. The four prior +survivors therefore remain open: Query `M563`, `M1473`, `M1794`, and TrailBase +`M151`. No allowance has been requested or assumed. Two earlier full-package +Query timeouts also remain separate from assertion kills. + +The five conversions above do not discharge the older ownership-lifecycle or +TrailBase lifecycle/eager-source owners. Before any further production edit, +their affected laws and required real-browser receiving paths must pass the +conversion gate. Removing a condition merely to improve a mutation score +would not supply that gate or establish a sound simplification. + +The final same-seed CLI comparison is a separate experiment. Its stable-key +retention and current classifications will be appended here after completion; +the earlier audit's 49 initial kills plus six additive kills remain historical +executed results, not a substituted final CLI run. + +## Startup cost + +The [paired measurement report](2026-11-10-persisted-startup-measurements.md) +and its raw CSV remain applicable because the production source is unchanged. +They show a PowerSync 10,000-row matching-baseline median increase from 274.7 +to 320.1 ms. At 50,000 rows, sampled heap maxima rise by 44.2 MiB for RxDB +and 71.7 MiB for PowerSync. Network-first fallback still becomes ready before +the held source in that measured scenario. These local measurements establish +neither performance parity nor browser paint times. No performance repair is +implemented by a test-framework conversion. diff --git a/docs/contributing/oracle-reviews/2026-11-10-startup-property-conversion.md b/docs/contributing/oracle-reviews/2026-11-10-startup-property-conversion.md new file mode 100644 index 0000000000..8471a98628 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-startup-property-conversion.md @@ -0,0 +1,166 @@ +# Persisted startup oracleProperty conversion + +This record evaluates `29de8a92e3aae74dc4c1c2c2d2d47e6edd44dc86` plus the +conversion files below, with `@tanstack/oracles` at +`e1fb5048c6d33e958643360d87e168c302e03bd1`. Production is unchanged. The shared +set model, comparisons, adapter drivers, storage hosts and Chromium receiver +remain the accepted product instrument. + +| Converted file | SHA-256 | +| --- | --- | +| `packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts` | `4616fecfbfe811338003197706b3e3839e911a55c6402ce21c63b5893f5a031a` | +| `packages/db-adapter-conformance/tests/persisted-startup-oracle.ts` | `ec7671a2cf02b0d6330a647a19c0b7deec66a103d8787fd9f0451f32fcbb9281` | +| `packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts` | `c53a3cdcbaaea95965ae438fc3a3b248ece57235f6971bce0eb24bd9881b526a` | + +## Preserved law and paths + +A complete eager source result replaces its persisted baseline; a scoped source +retains an omitted peer-owned row. An omitted ownerless direct row is removed. +The first result is durable before a later refresh. Network-first fallback +retains the saved public and durable snapshot until source delivery. Authority: +READY-3/PUB-7, Query's “Direct Writes and Query Sync” and “Initial Data” contracts, +the glossary's network-first initial-render contract, and the maintainer's +shared-owner decision on 2026-11-09. + +The model remains `expectedPersistedStartup` / `expectedStartupFallback` in +`persisted-startup-oracle.ts`. `persistedStartupViolations` and the immediate-cut +checks still invoke that model. The framework adapter's model return adds only +constructed-state obligations; it does not create another product model. + +All six adapters retain controlled and real Node SQLite receiving paths. The +same composition driver compares fallback public/durable rows, the first +source result, its durable state before refresh, rows inside refresh +fulfillment, durable rows afterward, and local-only reopen. Scoped Query also +compares ownership. The one-full-row-read work law and Query's source-origin +premises are unchanged. Source response / restore ordering is proved before +recording reach. + +Each host keeps the 80-history fixed-seed campaign at seed `2367`, the same +80-history fresh campaign, and every fixed input: 116 for Query and 26 for each +other adapter. New mandatory constructions add one seed-derived example per +state in each campaign: 16 for Query, nine elsewhere. The same constructors +also participate in generated and shrunk histories. + +`fc.chain` cannot admit context-free fixed/constructed inputs through +`canShrinkWithoutContext`. The two startup generators therefore use direct +records with the same legal language. Sampling weights and the exact seed +stream change; no mask, schedule, optional action, provenance, origin or fixed +history is removed. The legal bounds remain masks `0..7` over three string +keys. Peers require saved rows; ownerless provenance requires saved rows; +cached/initial Query answers precede restore; an after-deadline source requires +network-first and a nonempty saved baseline. Removing any constraint admits an +invalid history. Removing a named axis loses its corresponding required state +or ownership/omission distinction. + +## Validation and calibration + +All final normal runs pass, including type checking: Query 280 tests; Convex, +Electric, TrailBase, RxDB and PowerSync 86 tests each (710 total). Commands run +`vitest run --coverage.enabled=false` on each package's existing +`persisted-startup-conformance-oracle` runner. The three converted files pass +ESLint. The local filtered install initially lacked unrelated `pg` declarations; +the final clean runs occur after that workspace dependency was linked. + +The existing calibration judgments are retained: a stale first durable result +cannot be repaired by later refresh; losing the saved fallback fails; retaining +fallback as the first source's durable result fails; and dropping source +persistence before restore or after timeout fails at `startupDurable`. +Persistence loss wraps both outer and hydration-scoped adapters. The latter +fault must actually run for the RxDB/PowerSync real SQLite before-restore +witness. A clean candidate that fails cannot certify the fault; the framework +must find a passing clean trace and the named rejection, or fail calibration. + +Each builder returns a fresh object. A WeakMap records its declared state on +every invocation, including fresh generated/shrunk constructions. The shared +check enforces that obligation after product comparisons. One missing-state +control per state proves that the shared check rejects a construction whose +state observation is removed. These controls run through the same model, +driver and comparison; they do not depend on the framework's seed-only +history-to-builder association. Product mismatches retain their original cut +before the driver's structured throw. Missing completion and construction +obligations use unrecorded checkpoints so a prior driver error stays primary. + +This explicit replay ran one history per host, with two tests passed and 278 +skipped; it did not run either normal campaign first: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=query.startup-ownership \ +TANSTACK_DB_ORACLE_SEED=2367 TANSTACK_DB_ORACLE_PATH=0 \ +../../node_modules/.bin/vitest run --coverage.enabled=false \ + --typecheck.enabled=false tests/persisted-startup-conformance-oracle.test.ts +``` + +## Reach + +Every required state has a minimum of one randomized construction per host and +campaign. Fixed literals supply no required-reach credit. Counts below include +the 80 generated histories and the mandatory constructions. `Fixed` is the +same count on both hosts. `Fresh C` and `Fresh SQLite` are separate fresh +campaigns. + +### Query + +| State | Fixed C / SQLite | Fresh C | Fresh SQLite | +| --- | ---: | ---: | ---: | +| `cached-disabled` | 17 | 10 | 7 | +| `cached-pending` | 12 | 8 | 8 | +| `empty-baseline` | 3 | 2 | 1 | +| `empty-result` | 55 | 50 | 51 | +| `fetch` | 50 | 49 | 58 | +| `initial-disabled` | 10 | 16 | 15 | +| `initial-pending` | 7 | 13 | 8 | +| `network-first-fallback` | 19 | 11 | 11 | +| `network-first-source-result` | 70 | 79 | 69 | +| `omitted-row` | 73 | 79 | 76 | +| `overlapping-update` | 34 | 29 | 36 | +| `ownerless-baseline` | 65 | 51 | 58 | +| `peer-owner` | 39 | 60 | 51 | +| `persisting-mutation` | 75 | 79 | 80 | +| `response-before-restore` | 73 | 82 | 77 | +| `restore-before-response` | 23 | 14 | 19 | + +### Collection-wide sources + +Each cell is `fixed both hosts / fresh controlled / fresh SQLite`. + +| State | Convex | Electric | TrailBase | RxDB | PowerSync | +| --- | --- | --- | --- | --- | --- | +| `empty-baseline` | 8 / 10 / 15 | 8 / 13 / 14 | 8 / 18 / 9 | 8 / 15 / 12 | 8 / 18 / 14 | +| `empty-result` | 46 / 46 / 57 | 46 / 52 / 41 | 46 / 50 / 56 | 46 / 43 / 54 | 46 / 49 / 48 | +| `network-first-fallback` | 29 / 21 / 18 | 29 / 25 / 23 | 29 / 18 / 27 | 29 / 21 / 16 | 29 / 16 / 22 | +| `network-first-source-result` | 56 / 60 / 59 | 56 / 55 / 58 | 56 / 64 / 50 | 56 / 55 / 60 | 56 / 61 / 56 | +| `omitted-row` | 69 / 67 / 60 | 69 / 66 / 62 | 69 / 60 / 67 | 69 / 60 / 63 | 69 / 59 / 64 | +| `overlapping-update` | 30 / 27 / 23 | 30 / 23 / 28 | 30 / 28 / 23 | 30 / 31 / 24 | 30 / 25 / 27 | +| `persisting-mutation` | 77 / 74 / 75 | 77 / 74 / 74 | 77 / 78 / 79 | 77 / 76 / 76 | 77 / 73 / 72 | +| `response-before-restore` | 48 / 55 / 55 | 48 / 49 / 52 | 48 / 60 / 48 | 48 / 53 / 51 | 48 / 55 / 54 | +| `restore-before-response` | 41 / 34 / 34 | 41 / 40 / 37 | 41 / 29 / 41 | 41 / 36 / 38 | 41 / 34 / 35 | + +## Rubric and limits + +ORC-001–003 retain the existing authority, independent set model and named +companion boundaries. ORC-004 retains the legal language and all fixed +reconstructions; the record filters state the exclusions above. ORC-005 retains +the six real adapter paths and original public/durable cuts. ORC-006 retains +the wrong-result and scoped dropped-write controls and adds missing-state +controls. ORC-007 uses the constructor's equal-budget fixed/fresh campaigns and +the checked direct replay. ORC-008 changes no product-model state. ORC-009 +preserves canonical state names and identifies construction obligations as +instrument metadata. ORC-010 retains original cut verdicts, partial-trace +failure priority and the existing cleanup. ORC-011 has no new shared semantic +fault in this pure conversion; no second product model is introduced. This +record supplies ORC-012 evidence. ORC-013 retains the existing neighboring +replacement/fallback cuts; it adds no deadline-threshold claim. ORC-014 keeps +both hosts and the unchanged Chromium receiver. ORC-015 has a constructed +builder and missing-state control for every declared state, with fresh-output +obligations and the measured counts above. ORC-016 replaces manual registrations +with fixed inputs and reusable calibration judgments; it adds no example-based +expected answers. + +The conversion does not change or broaden the shared owner's limits: eager +startup, three keys, one source and optional peer, a disjoint persisting +mutation, and local-only reopen. It proves no on-demand, concurrent-writer, +failed-I/O, remote-replication, or every-intermediate-publication law. The +Chromium receiver remains a separate unchanged receiving witness, not a native +Vitest constructor conversion. Gap-retention results belong to the separate +same-seed before/after audit; these green runs alone do not close any surviving +production gap or grant a maintainer allowance. diff --git a/packages/db-adapter-conformance/tests/eager-reload-oracle-suite.ts b/packages/db-adapter-conformance/tests/eager-reload-oracle-suite.ts index 9621390d4f..4183c1f085 100644 --- a/packages/db-adapter-conformance/tests/eager-reload-oracle-suite.ts +++ b/packages/db-adapter-conformance/tests/eager-reload-oracle-suite.ts @@ -1,109 +1,278 @@ /** Registration and campaigns for the unwrapped eager-reload receiving law. */ -import fc from 'fast-check' -import { afterAll, expect, it } from 'vitest' -import { declareOracle } from '../../db/tests/oracle-declaration' -import { createOracleReach } from '../../db/tests/oracle-reach' -import { - oracleFixedSeed, - oraclePropertyOptions, - oracleReplayTest, - oracleRoute, - oracleRouteOf, - oracleRuns, -} from '../../db/tests/oracle-config' +import { oracleProperty, pass, violation } from '../../db/tests/oracle-property' import { eagerReloadHistories, eagerReloadHistoryArb, + eagerReloadModel, eagerReloadReached, eagerReloadStates, eagerReloadViolations, } from './eager-reload-oracle' import { observeEagerReload } from './eager-reload-driver' +import type { Observation, Verdict } from '../../db/tests/oracle-property' import type { EagerReloadAdapter } from './eager-reload-driver' -import type { EagerReloadHistory } from './eager-reload-oracle' +import type { + EagerReloadHistory, + EagerReloadObservation, +} from './eager-reload-oracle' + +type ReloadState = (typeof eagerReloadStates)[number] +type CheckedReloadResult = { + failures: Array + raw?: EagerReloadObservation +} +type CorruptReload = ( + h: EagerReloadHistory, + raw: EagerReloadObservation, +) => EagerReloadObservation -declareOracle({ - check: [eagerReloadViolations], - grammar: [eagerReloadHistoryArb], -}) +// SDK completion and optimistic settlement can race legally. Judge the actual +// fulfillment rows and flags with the unchanged raw checker before projecting +// that scheduling freedom to one successful record. Every failed judgment +// keeps its complete raw observation and is judged again by the framework +// check. No SDK observation cut or browser comparison changes. Calibrations +// below inject wrong raw rows before this projection, rather than corrupting a +// summary after the relevant information has already been discarded. +function checkedReloadResult( + h: EagerReloadHistory, + raw: EagerReloadObservation, +): CheckedReloadResult { + const failures = eagerReloadViolations(h, raw) + return failures.length ? { failures, raw } : { failures } +} export function registerEagerReloadConformance( property: string, adapter: EagerReloadAdapter, ) { let sequence = 0 - const fixedReach = createOracleReach(`${property}.fixed-histories`, [ - ...eagerReloadStates, - ]) - afterAll(() => fixedReach.assertReached()) - const check = async (h: EagerReloadHistory, reach = fixedReach) => { - reach.beginHistory() - const observed = await observeEagerReload( - adapter, - h, - `${property}-${sequence++}`, - () => { - for (const state of eagerReloadReached(h)) reach.reach(state) - }, - ) - expect( - eagerReloadViolations(h, observed), - JSON.stringify({ h, observed }), - ).toEqual([]) + // Each construction keeps the generated coordinates that its premise does + // not constrain. Even a fresh-held construction varies the next source set. + // Fresh identities prevent the pinned framework's builder map collisions. + const retained = (h: EagerReloadHistory) => h.previous || (h.next % 7) + 1 + const key = (h: EagerReloadHistory) => { + const previous = retained(h) + return previous & -previous } - oracleReplayTest(it).each(eagerReloadHistories)( - `replaces the complete result: %j`, - async (h) => { - oracleRoute(`fixed history`) - await check(h) + const builds: Array<{ + state: ReloadState + build: (h: EagerReloadHistory) => EagerReloadHistory + }> = [ + { state: `fresh`, build: (h) => ({ ...h, start: `fresh`, previous: 0 }) }, + { + state: `reload-empty`, + build: (h) => ({ ...h, start: `reload`, previous: 0 }), + }, + { + state: `reload-retained`, + build: (h) => ({ ...h, start: `reload`, previous: retained(h) }), + }, + { state: `empty-result`, build: (h) => ({ ...h, next: 0 }) }, + { + state: `omitted-row`, + build: (h) => ({ + ...h, + start: `reload`, + previous: retained(h), + next: h.next & ~key(h), + }), + }, + { + state: `changed-overlap`, + build: (h) => ({ + ...h, + start: `reload`, + previous: retained(h), + next: h.next | key(h), + }), + }, + { state: `persisting-mutation`, build: (h) => ({ ...h, mutation: true }) }, + { state: `no-mutation`, build: (h) => ({ ...h, mutation: false }) }, + { + state: `fresh-held`, + build: (h) => ({ ...h, start: `fresh`, previous: 0, mutation: true }), + }, + { + state: `reload-empty-held`, + build: (h) => ({ ...h, start: `reload`, previous: 0, mutation: true }), }, - ) - oracleReplayTest(it)( - `rejects an inverted snapshot decision on retained rows`, - async () => { - oracleRoute(`calibration`) - const h = eagerReloadHistories.find( - (candidate) => - candidate.previous === 1 && - candidate.next === 2 && - !candidate.mutation, - )! + { + state: `reload-retained-held`, + build: (h) => ({ + ...h, + start: `reload`, + previous: retained(h), + mutation: true, + }), + }, + ] + // The pinned framework checks named example constructions, but its generated + // map branch does not retain their enters obligations. Attach each fresh + // built history's obligation locally, so the same check enforces it when the + // builder runs in either path. This map carries test metadata, not row truth. + const required = new WeakMap>() + const constructed = builds.map(({ state, build }) => ({ + name: `generated ${state}`, + enters: [state], + build: (generated: EagerReloadHistory) => { + const history = { ...build(generated) } + required.set(history, [state]) + return history + }, + })) + const drive = + (invertSnapshot = false, corrupt: CorruptReload = (_h, raw) => raw) => + async ( + h: EagerReloadHistory, + observe: (checkpoint: string, value: unknown) => void, + ) => { const observed = await observeEagerReload( adapter, h, - `${property}-inverted`, - undefined, - true, + `${property}-${sequence++}`, + () => { + for (const state of eagerReloadReached(h)) observe(`reach`, state) + }, + invertSnapshot, ) - expect(eagerReloadViolations(h, observed)).not.toEqual([]) - }, - ) - for (const campaign of [`fixed`, `random`] as const) { - const register = - campaign === `fixed` - ? oracleReplayTest(it) - : oracleReplayTest(it, property) - register( - `reconciles generated reload histories (${campaign})`, - async () => { - const parameters = - campaign === `fixed` - ? oracleFixedSeed(2367, { numRuns: oracleRuns(80) }) - : oraclePropertyOptions(80, property) - oracleRoute(oracleRouteOf(parameters)) - const reach = createOracleReach(`${property}.${campaign}`, [ - ...eagerReloadStates, - ]) - await fc.assert( - fc.asyncProperty(eagerReloadHistoryArb, (h) => check(h, reach)), - parameters, + observe(`result`, checkedReloadResult(h, corrupt(h, observed))) + } + const check = ( + h: EagerReloadHistory, + trace: ReadonlyArray, + expected: ReturnType, + ): Verdict => { + const result = trace.find((entry) => entry.checkpoint === `result`) + if (result) { + const projection = result.value as CheckedReloadResult + if (projection.failures.length) { + const failures = projection.raw + ? eagerReloadViolations(h, projection.raw, expected) + : projection.failures + return violation( + `result`, + failures.join(`; `), + `complete eager result`, + { actual: projection.raw ?? projection, expected }, ) - reach.assertReached() - process.stdout.write( - `${property}.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, - ) - }, - 120_000, + } + } + const reached = new Set( + trace + .filter((entry) => entry.checkpoint === `reach`) + .map((entry) => entry.value), + ) + const missing = (required.get(h) ?? []).filter( + (state) => !reached.has(state), ) + if (missing.length) + return violation( + `reach`, + `generated construction omitted ${missing.join(`, `)}`, + `constructed builder reach`, + ) + return result + ? pass + : violation( + `result`, + `source result was not observed`, + `complete eager result`, + ) } + oracleProperty({ + id: property, + law: { + statement: `A complete eager source result replaces retained source membership and values; before source entry the previous public rows remain, and a persisting optimistic overlay follows the accepted-versus-applied sync contract until settlement.`, + authority: `Glossary reload, snapshot transaction and accepted sync transaction; eager-source READY-3/PUB-7; optimistic-history-oracle.ts.`, + limits: `Three string keys and numeric values, one fresh load or eager reload, one disjoint optimistic insert, real local SDK storage. No remote replication, persisted durability, arbitrary mutations, on-demand reload, or every intermediate publication.`, + owner: `Adapter conformance: complete eager reload`, + }, + histories: { + arbitrary: eagerReloadHistoryArb, + states: eagerReloadStates, + runs: 80, + fixedSeeds: [2367], + constructed, + fixed: eagerReloadHistories.map((history) => ({ + name: JSON.stringify(history), + history, + })), + }, + model: eagerReloadModel, + driver: drive(), + check, + calibrations: [ + { + name: `inverted snapshot declaration on retained rows`, + law: `complete eager result`, + driver: drive(true), + }, + ...( + [ + { + name: `wrong before-source rows`, + corrupt: (_h, raw) => ({ + ...raw, + before: [...raw.before, { id: `unexpected`, value: -1 }], + }), + }, + { + name: `wrong fulfillment rows`, + corrupt: (_h, raw) => ({ + ...raw, + atFulfillment: [ + ...raw.atFulfillment, + { id: `unexpected`, value: -1 }, + ], + }), + }, + { + name: `wrong final rows`, + corrupt: (_h, raw) => ({ + ...raw, + final: [...raw.final, { id: `unexpected`, value: -1 }], + }), + }, + { + name: `lost pending optimistic overlay`, + corrupt: (h, raw) => + h.mutation + ? { + ...raw, + mutationReleasedBeforeFulfillment: false, + mutationSettledBeforeFulfillment: false, + atFulfillment: raw.atFulfillment.filter( + (row) => row.id !== `optimistic`, + ), + } + : raw, + }, + { + name: `missing source acceptance`, + corrupt: (_h, raw) => ({ + ...raw, + submittedCommits: 0, + acceptedCommits: 0, + }), + }, + ] satisfies Array<{ name: string; corrupt: CorruptReload }> + ).map(({ name, corrupt }) => ({ + name: `raw judgment rejects ${name}`, + law: `complete eager result`, + driver: drive(false, corrupt), + })), + ...builds.map(({ state }) => ({ + name: `missing generated builder state ${state}`, + law: `constructed builder reach`, + corrupt: (trace: ReadonlyArray, h: EagerReloadHistory) => + required.get(h)?.includes(state) + ? trace.filter( + (entry) => + !(entry.checkpoint === `reach` && entry.value === state), + ) + : undefined, + })), + ], + timeout: 120_000, + }) } diff --git a/packages/db-adapter-conformance/tests/eager-reload-oracle.ts b/packages/db-adapter-conformance/tests/eager-reload-oracle.ts index b2248cdb62..7e1376ab20 100644 --- a/packages/db-adapter-conformance/tests/eager-reload-oracle.ts +++ b/packages/db-adapter-conformance/tests/eager-reload-oracle.ts @@ -41,29 +41,29 @@ export type EagerReloadHistory = { // nonempty baseline; every next subset is legal, including empty and overlap. // Removing reload loses omissions from retained rows. Removing mutation loses // the ordinary-commit versus snapshot scheduling boundary on an empty baseline. -export const eagerReloadHistoryArb: fc.Arbitrary = fc - .oneof( - fc.record({ - start: fc.constant(`fresh` as const), - previous: fc.constant(0), - }), - fc.record({ - start: fc.constant(`reload` as const), - previous: fc.constant(0), - }), - fc.record({ - start: fc.constant(`reload` as const), - previous: fc.integer({ min: 1, max: 7 }), - }), - ) - .chain((prefix) => - fc - .record({ - next: fc.integer({ min: 0, max: 7 }), - mutation: fc.boolean(), - }) - .map((suffix) => ({ ...prefix, ...suffix })), - ) +// Full records preserve the three equally weighted lanes and permit the +// framework to validate/shrink fixed and constructed histories without context. +const reloadSuffix = { + next: fc.integer({ min: 0, max: 7 }), + mutation: fc.boolean(), +} +export const eagerReloadHistoryArb: fc.Arbitrary = fc.oneof( + fc.record({ + start: fc.constant(`fresh` as const), + previous: fc.constant(0), + ...reloadSuffix, + }), + fc.record({ + start: fc.constant(`reload` as const), + previous: fc.constant(0), + ...reloadSuffix, + }), + fc.record({ + start: fc.constant(`reload` as const), + previous: fc.integer({ min: 1, max: 7 }), + ...reloadSuffix, + }), +) export const eagerReloadHistories: Array = [ { start: `fresh` as const, previous: 0 }, @@ -124,26 +124,38 @@ export type EagerReloadObservation = { final: Array error?: string } +// The same pure row judgment supplies the browser checker and the framework. +// It predicts only from authored source membership and the optimistic lane; +// no commit count or observed production schedule enters this computation. +export function eagerReloadModel(h: EagerReloadHistory) { + return { + initial: sortedStartupRows([ + ...startupRows(h.previous, 1), + ...(h.mutation ? [{ id: `optimistic`, value: 99 }] : []), + ]), + source: expectedPersistedStartup( + { + previous: h.previous, + next: h.next, + peer: false, + response: `after-restore`, + }, + `collection`, + ).rows, + retained: startupRows(h.previous, 1), + states: eagerReloadReached(h), + } +} export function eagerReloadViolations( h: EagerReloadHistory, observed: EagerReloadObservation, + expected = eagerReloadModel(h), ): Array { const violations: Array = [] - const initial = sortedStartupRows([ - ...startupRows(h.previous, 1), - ...(h.mutation ? [{ id: `optimistic`, value: 99 }] : []), - ]) - const expected = expectedPersistedStartup( - { - previous: h.previous, - next: h.next, - peer: false, - response: `after-restore`, - }, - `collection`, - ).rows + const { initial, source: expectedRows, retained } = expected const equal = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b) + if (!equal(observed.before, initial)) violations.push(`before-source rows`) if (observed.beforeStatus !== (h.start === `reload` ? `ready` : `loading`)) violations.push(`before-source status`) @@ -152,7 +164,7 @@ export function eagerReloadViolations( if (observed.error !== undefined) violations.push(`source failed: ${observed.error}`) if ( - observed.submittedRows < expected.length || + observed.submittedRows < expectedRows.length || observed.submittedCommits === 0 ) violations.push(`complete source submission not reached`) @@ -171,9 +183,8 @@ export function eagerReloadViolations( const overlay = observed.atFulfillment.filter( (row) => row.id === `optimistic`, ) - const retained = startupRows(h.previous, 1) if ( - !equal(fulfillmentSource, expected) && + !equal(fulfillmentSource, expectedRows) && !( h.mutation && !observed.mutationSettledBeforeFulfillment && @@ -192,10 +203,11 @@ export function eagerReloadViolations( overlay.length !== 1 ) violations.push(`pending optimistic overlay disappeared`) - if (!equal(observed.final, expected)) violations.push(`final rows`) + if (!equal(observed.final, expectedRows)) violations.push(`final rows`) return violations } declareOracle({ grammar: [eagerReloadHistoryArb, eagerReloadHistories], + model: [eagerReloadModel], check: [eagerReloadViolations], }) diff --git a/packages/db-adapter-conformance/tests/persisted-capability-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-capability-oracle-suite.ts index 3fc2dd4989..8c82e910ef 100644 --- a/packages/db-adapter-conformance/tests/persisted-capability-oracle-suite.ts +++ b/packages/db-adapter-conformance/tests/persisted-capability-oracle-suite.ts @@ -1,47 +1,58 @@ /** Shared receiving registration for the finite capability owner. */ -import { afterAll, describe, expect, it } from 'vitest' -import { createOracleReach } from '../../db/tests/oracle-reach' -import { declareOracle } from '../../db/tests/oracle-declaration' -import { oracleRoute } from '../../db/tests/oracle-config' +import { oracleProperty, pass, violation } from '../../db/tests/oracle-property' import { createStartupHost } from './persisted-startup-host' import { capabilityHistories, + capabilityMustAccept, capabilityViolations, observeCapability, } from './persisted-capability-oracle' import type { PersistedStartupAdapter } from './persisted-startup-driver' import type { PersistedStartupHistory } from './persisted-startup-oracle' -declareOracle({ check: [capabilityViolations], grammar: [capabilityHistories] }) export function registerCapabilityConformance( name: string, adapter: PersistedStartupAdapter, ) { for (const host of [`controlled`, `node-sqlite`] as const) - describe(`${name} capability through ${host}`, () => { - const reach = createOracleReach(`${name}.capability.${host}`, [ - ...new Set(capabilityHistories.map((h) => h.kind)), - ]) - afterAll(() => reach.assertReached()) - it.each(capabilityHistories)( - `validates persistence protocol: %j`, - async (h) => { - oracleRoute(`fixed history`) - reach.beginHistory() - const storage = createStartupHost(host) - try { - const error = await observeCapability( + oracleProperty({ + id: `adapter.${name}.capability.${host}`, + law: { + statement: `An advertised persistence capability must satisfy its structural protocol before source delivery; absent metadata and an explicit null capability remain legal.`, + authority: `SyncMetadataApi.persistence and sync-persistence.ts's cross-package protocol contract.`, + limits: `The finite 22-history required-member grammar, through the real SDK and named local persistence host; no optional extension faults, on-demand results, or arbitrary malformed values.`, + owner: `Adapter conformance: persistence capability admission`, + }, + // This table is the whole finite grammar. Enumeration preserves all 22 + // cases on each host without inventing a meaningless random campaign. + histories: capabilityHistories, + model: capabilityMustAccept, + driver: async (h, observe) => { + const storage = createStartupHost(host) + try { + observe( + `admission`, + await observeCapability( adapter, storage.persistence, h, `${name}-${host}-${JSON.stringify(h)}`, - ) - reach.reach(h.kind) - expect(capabilityViolations(h, error)).toEqual([]) - } finally { - storage.close() - } - }, - ) + ), + ) + } finally { + storage.close() + } + }, + check: (h, trace, mustAccept) => { + const admission = trace.find( + (entry) => entry.checkpoint === `admission`, + ) + if (!admission) + return violation(`admission`, `sync admission was not observed`) + const failures = capabilityViolations(h, admission.value, mustAccept) + return failures.length + ? violation(`admission`, failures.join(`; `), `capability protocol`) + : pass + }, }) } diff --git a/packages/db-adapter-conformance/tests/persisted-capability-oracle.ts b/packages/db-adapter-conformance/tests/persisted-capability-oracle.ts index fbec6571b2..079c89d74a 100644 --- a/packages/db-adapter-conformance/tests/persisted-capability-oracle.ts +++ b/packages/db-adapter-conformance/tests/persisted-capability-oracle.ts @@ -73,9 +73,10 @@ function transformCapability(metadata: unknown, h: CapabilityHistory): unknown { export function capabilityViolations( h: CapabilityHistory, error: unknown, + mustAccept = capabilityMustAccept(h), ): Array { const name = error instanceof Error ? error.name : undefined - if (capabilityMustAccept(h)) + if (mustAccept) return error === undefined ? [] : [`legal capability rejected: ${String(error)}`] diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts index 6cfe458c8b..537296361b 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts @@ -1,27 +1,26 @@ /** - * Receiving registration for persisted-startup-oracle.ts. Every adapter runs - * its legal grammar through both storage hosts, at fallback (when enabled), - * the first source result, durability, refresh and - * reopen. Fixed histories and generated histories share driver and judgment. - * Each host/campaign enforces its own reach; one cannot cover another's gap. + * A complete source result replaces its persisted baseline, with a surviving + * peer retaining only its own rows. Network-first fallback retains the saved + * snapshot until that result arrives. The independent set model and cut checks + * live in persisted-startup-oracle.ts; this registration gives each adapter + * the same controlled and real SQLite receiving paths. + * + * Each host runs every fixed history and the same 80-history fixed/fresh + * campaigns. Constructed histories vary generated membership, mutation and + * render choices while preserving the state they promise to enter. State + * observations occur only after the composition driver proves its source / + * restore order. The first-result, durable, refresh and reopen comparisons keep + * their original cuts; a later result cannot repair an earlier verdict. + * + * Limits: the shared owner's three-key eager grammar, one source scope and an + * optional peer, a disjoint persisting mutation, and local-only reopen. The + * unchanged Chromium receiver supplies its separate real OPFS witness. */ -import fc from 'fast-check' -import { afterAll, describe, expect, it } from 'vitest' -import { declareOracle } from '../../db/tests/oracle-declaration' -import { createOracleReach } from '../../db/tests/oracle-reach' -import { - oracleFixedSeed, - oraclePropertyOptions, - oracleReplayTest, - oracleRoute, - oracleRouteOf, - oracleRuns, -} from '../../db/tests/oracle-config' +import { describe } from 'vitest' +import { oracleProperty, pass, violation } from '../../db/tests/oracle-config' import { expectedPersistedStartup, expectedStartupFallback, - persistedStartupCutViolations, - persistedStartupFallbackViolations, persistedStartupViolations, startupReachedStates, startupRequiredStates, @@ -29,18 +28,18 @@ import { import { observePersistedStartup } from './persisted-startup-driver' import { createStartupHost } from './persisted-startup-host' import type { Arbitrary } from 'fast-check' +import type { + Observation, + OraclePropertyOptions, + Verdict, +} from '../../db/tests/oracle-config' import type { PersistenceAdapter } from '../../db-sqlite-persistence-core/src' -import type { PersistedStartupHistory } from './persisted-startup-oracle' +import type { + PersistedStartupHistory, + PersistedStartupObservation, +} from './persisted-startup-oracle' import type { PersistedStartupAdapter } from './persisted-startup-driver' -declareOracle({ - check: [ - persistedStartupCutViolations, - persistedStartupFallbackViolations, - persistedStartupViolations, - ], -}) - export function registerPersistedStartupConformance< H extends PersistedStartupHistory, >({ @@ -60,57 +59,105 @@ export function registerPersistedStartupConformance< origins?: Array origin?: (history: H) => string | undefined }) { - // A later refresh must not make a stale startup durable result acceptable. - oracleReplayTest(it)( - `rejects stale durability before a later refresh`, - () => { - oracleRoute(`calibration`) - const history = histories[0]! - const stale = expectedPersistedStartup( - { ...history, next: history.previous }, - adapter.scope, - ) - expect( - persistedStartupCutViolations( - history, - adapter.scope, - `startupDurable`, - stale, - ), - ).not.toEqual([]) - }, - ) - oracleReplayTest(it)(`rejects a lost persisted fallback`, () => { - oracleRoute(`calibration`) - const h = { - ...histories.find((history) => history.previous !== 0)!, - initialRender: `network-first` as const, - response: `after-deadline` as const, + const states = [...startupRequiredStates(adapter.scope), ...origins] + // The pinned framework remembers required states only for seed examples. + // Keep the same obligations on every fresh generated or shrunk construction. + // They describe its inputs, never production's expected rows. + const constructionStates = new WeakMap>() + const constructed = states.map((state) => { + const witness = histories.find( + (h) => startupReachedStates(h).includes(state) || origin(h) === state, + ) + if (!witness) + throw new Error(`${property}: no legal state witness for ${state}`) + return { + name: `generated ${state}`, + enters: [state], + build: (generated: H): H => { + // Preserve generated membership and optional actions. Borrow only a + // legal receiver origin / response schedule, then splice the minimum + // premise for this state into the generated three-key history. + const { + previous: _previous, + next: _next, + peer: _peer, + previousOwner: _owner, + mutation: _mutation, + initialRender: _render, + ...receiver + } = witness + const history = { ...generated, ...receiver } as H + const key = 1 << ((generated.previous + generated.next) % 3) + switch (state) { + case `empty-baseline`: + history.previous = 0 + break + case `empty-result`: + history.next = 0 + break + case `overlapping-update`: + history.previous |= key + history.next |= key + break + case `omitted-row`: + history.previous |= key + history.next &= ~key + break + case `peer-owner`: + history.previous |= key + history.peer = true + break + case `ownerless-baseline`: + history.previous |= key + history.peer = false + history.previousOwner = `none` + break + case `persisting-mutation`: + history.mutation = `persisting` + break + } + if ( + state.startsWith(`network-first-`) || + history.response === `after-deadline` + ) + history.initialRender = `network-first` + if (history.response === `after-deadline`) history.previous |= key + if (history.previous === 0) { + history.peer = false + delete history.previousOwner + } + constructionStates.set(history, [state]) + return history + }, } - const saved = expectedStartupFallback(h, adapter.scope) - expect( - persistedStartupFallbackViolations(h, adapter.scope, `fallback`, { - ...saved, - rows: [], - }), - ).not.toEqual([]) - expect( - persistedStartupCutViolations(h, adapter.scope, `startupDurable`, saved), - ).not.toEqual([]) }) - const states = [...startupRequiredStates(adapter.scope), ...origins] + for (const host of [`controlled`, `node-sqlite`] as const) { describe(`${property} through ${host}`, () => { - const reach = createOracleReach(`${property}.${host}`, states) - afterAll(() => { - reach.assertReached() - process.stdout.write( - `${property}.${host} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, - ) - }) let sequence = 0 - async function check(history: H, historyReach = reach) { - historyReach.beginHistory() + // This adapter's model return adds only construction obligations. The + // product model remains expectedPersistedStartup / expectedStartupFallback + // in the shared comparison module, also used by the Chromium receiver. + type Expected = { + requiredStates: ReadonlyArray + } + type Driver = OraclePropertyOptions[`driver`] + // The composition driver performs immediate cut comparisons. Record any + // mismatch before its structured throw so the framework preserves that + // law ahead of later cleanup/setup errors. The final observation uses + // exactly the same independent comparison as the previous registrar. + const recordCutFailure = ( + observe: Parameters[1], + violations: Array, + ) => { + const first = violations[0]! + const colon = first.indexOf(`:`) + observe( + colon < 0 ? `startup premise` : first.slice(0, colon), + violations, + ) + } + const drive: Driver = async (history, observe) => { const storage = createStartupHost(host) try { const observation = await observePersistedStartup( @@ -122,154 +169,266 @@ export function registerPersistedStartupConformance< storage.readRowCount, () => { for (const state of startupReachedStates(history)) - historyReach.reach(state) + observe(`reach`, state) const responseOrigin = origin(history) - if (responseOrigin) historyReach.reach(responseOrigin) + if (responseOrigin) observe(`reach`, responseOrigin) }, - (violations) => expect(violations).toEqual([]), + (violations) => recordCutFailure(observe, violations), ) - expect( - persistedStartupViolations(history, adapter.scope, observation), - ).toEqual([]) + observe(`completed`, observation) } finally { storage.close() } } + const check = ( + history: H, + trace: ReadonlyArray, + expected: Expected, + ): Verdict => { + const immediate = trace.find( + ({ checkpoint }) => + checkpoint !== `completed` && checkpoint !== `reach`, + ) + if (immediate) { + const violations = immediate.value as Array + return violation( + immediate.checkpoint, + violations.join(`\n`), + violations[0]?.startsWith(`startupDurable:`) + ? `startup durability` + : `persisted startup`, + ) + } + const completed = trace.find( + ({ checkpoint }) => checkpoint === `completed`, + ) + if (!completed) + return violation( + `completed`, + `the persisted startup driver did not complete`, + `persisted startup`, + ) + const violations = persistedStartupViolations( + history, + adapter.scope, + completed.value as PersistedStartupObservation, + ) + if (violations.length) + return violation( + `completed`, + violations.join(`\n`), + violations[0]?.startsWith(`startupDurable:`) + ? `startup durability` + : `persisted startup`, + ) + const reached = new Set( + trace + .filter(({ checkpoint }) => checkpoint === `reach`) + .map(({ value }) => value), + ) + const missing = expected.requiredStates.filter( + (state) => !reached.has(state), + ) + // Completeness is judged only after production's own cuts. Keep this + // checkpoint unrecorded so an interrupted driver keeps its real error. + return missing.length + ? violation( + `constructed states`, + `constructed history did not enter ${missing.join(`, `)}`, + `constructed reach`, + ) + : pass + } + // A source that publishes after fallback but silently loses its durable - // writes must fail before refresh can repair it. Seed and restore use the - // real host; only the receiving source's persistence commits are dropped. - for (const response of [`before-restore`, `after-deadline`] as const) - oracleReplayTest(it)( - `rejects lost source persistence with ${response}`, - async () => { - oracleRoute(`calibration`) - const history = histories.find((h) => h.response === response)! - // Controlled storage models one Collection; use a fresh host for - // each candidate so the clean run cannot seed the corrupted run. - const clean = createStartupHost(host) - try { - await observePersistedStartup( - adapter, - clean.persistence, - clean.holdBaseline, - history, - `clean-persistence-${host}-${response}`, - clean.readRowCount, - ) - } finally { - clean.close() - } - const storage = createStartupHost(host) - let dropped = 0 - let scopedDrops = 0 - const corrupt = ( - subject: PersistenceAdapter, - inScope = false, - ): PersistenceAdapter => - new Proxy(subject, { - get(target, key) { - if (key === `applyCommittedTx`) - return () => { - dropped++ - if (inScope) scopedDrops++ - return Promise.resolve() - } - if ( - key === `runInHydrationScope` && - target.runInHydrationScope + // writes fails before refresh can repair it. Wrap both the outer adapter + // and the hydration-scope adapter; the scoped fault must actually run. + const losePersistence = + (response: H[`response`]): Driver => + async (history, observe) => { + if (history.response !== response) return drive(history, observe) + let dropped = 0 + let scopedDrops = 0 + const corrupt = ( + subject: PersistenceAdapter, + inScope = false, + ): PersistenceAdapter => + new Proxy(subject, { + get(target, key) { + if (key === `applyCommittedTx`) + return () => { + dropped++ + if (inScope) scopedDrops++ + return Promise.resolve() + } + if (key === `runInHydrationScope` && target.runInHydrationScope) + return ( + task: Parameters< + NonNullable + >[0], + ) => + target.runInHydrationScope!((scoped) => + task(corrupt(scoped, true)), + ) + const value: unknown = Reflect.get(target, key) + return typeof value === `function` ? value.bind(target) : value + }, + }) + const storage = createStartupHost(host) + try { + const observation = await observePersistedStartup( + { + ...adapter, + create: (persistence, h, id) => + adapter.create( + { ...persistence, adapter: corrupt(persistence.adapter) }, + h, + id, + ), + }, + storage.persistence, + storage.holdBaseline, + history, + `lost-persistence-${host}-${sequence++}`, + storage.readRowCount, + () => { + for (const state of startupReachedStates(history)) + observe(`reach`, state) + const responseOrigin = origin(history) + if (responseOrigin) observe(`reach`, responseOrigin) + }, + (violations) => { + if ( + violations[0]?.startsWith(`startupDurable:`) && + dropped > 0 && + (!( + host === `node-sqlite` && + response === `before-restore` && + scopedStartupCommit + ) || + scopedDrops > 0) + ) + recordCutFailure(observe, violations) + }, + ) + observe(`completed`, observation) + } finally { + storage.close() + } + } + + oracleProperty({ + id: property, + law: { + statement: `A complete eager source result replaces its persisted baseline and is durable before later refresh; network-first fallback preserves the saved snapshot.`, + authority: `READY-3/PUB-7; docs/collections/query-collection.md, Direct Writes and Query Sync and Initial Data; glossary, network-first initial render; maintainer shared-owner decision 2026-11-09.`, + owner: `Adapter conformance: persisted startup (#2367)`, + limits: `Three string keys, eager startup, one source plus an optional peer, a disjoint persisting mutation, local-only reopen; no concurrent writers, failed I/O, on-demand windows, or intermediate-publication claim.`, + }, + histories: { + arbitrary, + runs: 80, + fixedSeeds: [2367], + states, + constructed, + fixed: histories.map((history) => ({ + name: JSON.stringify(history), + history, + })), + }, + model: (history) => ({ + requiredStates: constructionStates.get(history) ?? [], + }), + driver: drive, + check, + calibrations: [ + ...states.map((state) => ({ + name: `omits constructed ${state}`, + law: `constructed reach`, + corrupt: (trace: ReadonlyArray, history: H) => + constructionStates.get(history)?.includes(state) + ? trace.filter( + ({ checkpoint, value }) => + checkpoint !== `reach` || value !== state, ) - return ( - task: Parameters< - NonNullable - >[0], - ) => - target.runInHydrationScope!((scoped) => - task(corrupt(scoped, true)), - ) - const value: unknown = Reflect.get(target, key) - return typeof value === `function` - ? value.bind(target) - : value - }, - }) - try { - await expect( - observePersistedStartup( - { - ...adapter, - create: (persistence, h, id) => - adapter.create( - { - ...persistence, - adapter: corrupt(persistence.adapter), - }, - h, - id, - ), - }, - storage.persistence, - storage.holdBaseline, - history, - `lost-persistence-${host}-${response}`, - storage.readRowCount, - ), - ).rejects.toThrow(/startupDurable:/) - expect( - dropped, - `fault must reach a source persistence commit`, - ).toBeGreaterThan(0) - if ( - host === `node-sqlite` && - response === `before-restore` && - scopedStartupCommit + : undefined, + })), + { + name: `keeps stale durability until later refresh`, + law: `startup durability`, + corrupt: (trace, history) => { + if (history.next === history.previous) return undefined + return trace.map((entry) => + entry.checkpoint === `completed` + ? { + ...entry, + value: { + ...(entry.value as PersistedStartupObservation), + startupDurable: expectedPersistedStartup( + { ...history, next: history.previous }, + adapter.scope, + ), + }, + } + : entry, ) - expect( - scopedDrops, - `fault must reach the hydration-scoped adapter`, - ).toBeGreaterThan(0) - } finally { - storage.close() - } + }, }, - ) - oracleReplayTest(it).each(histories)( - `reconciles persisted startup: %j`, - async (history) => { - oracleRoute(`fixed history`) - await check(history) - }, - ) - for (const campaign of [`fixed`, `random`] as const) { - const register = - campaign === `fixed` - ? oracleReplayTest(it) - : oracleReplayTest(it, property) - register( - `reconciles generated startup histories (${campaign})`, - async () => { - const parameters = - campaign === `fixed` - ? oracleFixedSeed(2367, { numRuns: oracleRuns(80) }) - : oraclePropertyOptions(80, property) - oracleRoute(oracleRouteOf(parameters)) - const generatedReach = createOracleReach( - `${property}.${host}.${campaign}`, - states, - ) - await fc.assert( - fc.asyncProperty(arbitrary, (history) => - check(history, generatedReach), - ), - parameters, - ) - generatedReach.assertReached() - process.stdout.write( - `${property}.${host}.${campaign} ${JSON.stringify(Object.fromEntries(generatedReach.counts()))}\n`, - ) + { + name: `loses the saved fallback`, + law: `persisted startup`, + corrupt: (trace, history) => + history.response === `after-deadline` && history.previous !== 0 + ? trace.map((entry) => + entry.checkpoint === `completed` + ? { + ...entry, + value: { + ...(entry.value as PersistedStartupObservation), + fallback: { + ...expectedStartupFallback( + history, + adapter.scope, + ), + rows: [], + }, + }, + } + : entry, + ) + : undefined, }, - 60_000, - ) - } + { + name: `keeps the saved snapshot at first durability`, + law: `startup durability`, + corrupt: (trace, history) => + history.response === `after-deadline` && history.previous !== 0 + ? trace.map((entry) => + entry.checkpoint === `completed` + ? { + ...entry, + value: { + ...(entry.value as PersistedStartupObservation), + startupDurable: expectedStartupFallback( + history, + adapter.scope, + ), + }, + } + : entry, + ) + : undefined, + }, + ...([`before-restore`, `after-deadline`] as const).map( + (response) => ({ + name: `loses source persistence with ${response}`, + law: `startup durability`, + driver: losePersistence(response), + }), + ), + ], + timeout: 60_000, + }) }) } } diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts index 75d892dfa2..a3e30dffdf 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts @@ -142,29 +142,33 @@ export function withStartupRender( : h.response, } } +// A direct record preserves the same finite language as the former chained +// render grammar and also admits context-free fixed and constructed histories. +// Cache-origin constraints remain in the receiving Query grammar. export const persistedStartupHistoryArb = ( scope: StartupScope, ): fc.Arbitrary => fc - .tuple( - persistedStartupMembershipArb(scope), - fc.boolean(), - fc.constantFrom(`before-restore` as const, `after-restore` as const), - ) - .chain(([membership, mutation, response]) => - fc - .tuple(persistedStartupRenderArb, fc.boolean()) - .map(([network, fallback]) => - withStartupRender( - { - ...membership, - response, - ...(mutation ? { mutation: `persisting` as const } : {}), - }, - network, - fallback, - ), + .record( + { + previous: fc.integer({ min: 0, max: 7 }), + next: fc.oneof(fc.constant(0), fc.integer({ min: 0, max: 7 })), + peer: scope === `scoped` ? fc.boolean() : fc.constant(false), + response: fc.constantFrom( + `before-restore` as const, + `after-restore` as const, + `after-deadline` as const, ), + mutation: fc.constant(`persisting` as const), + initialRender: fc.constant(`network-first` as const), + }, + { requiredKeys: [`previous`, `next`, `peer`, `response`] }, + ) + .filter( + (h) => + (!h.peer || h.previous !== 0) && + (h.response !== `after-deadline` || + (h.initialRender === `network-first` && h.previous !== 0)), ) // These are inputs to the same judgment as generated histories. Receivers may diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 13182b5226..de1db34b29 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -337,6 +337,10 @@ const staticOracleProperties = [ `query.cached-peer-claim`, `adapter.rxdb.eager-reload`, `adapter.powersync.eager-reload`, + `adapter.rxdb.capability.controlled`, + `adapter.rxdb.capability.node-sqlite`, + `adapter.powersync.capability.controlled`, + `adapter.powersync.capability.node-sqlite`, `trailbase.lifecycle.persisted-hydration.controlled`, `trailbase.lifecycle.persisted-hydration.node-sqlite`, `adapter.convex.persisted-startup`, diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index 9c8586d86d..eae5880735 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -276,6 +276,15 @@ const ownerGroups: ReadonlyArray< `db-adapter-conformance/tests/eager-reload-oracle-suite.ts`, ] as const, ), + ...[`rxdb`, `powersync`].map( + (adapter) => + [ + `${adapter}-db-collection/tests/persisted-capability-conformance-oracle.test.ts`, + `adapter.${adapter}.capability`, + `controlled node-sqlite`, + `db-adapter-conformance/tests/persisted-capability-oracle-suite.ts`, + ] as const, + ), [ `electric-db-collection/tests/electric-descriptor-isolation-oracle.test.ts`, `electric`, @@ -1000,16 +1009,13 @@ export type OracleReplayManifestEntry = { const replayTestNames = new Map([ [ `query.persisted-write-precedence`, - `orders generated fetch/write histories \\(random\\)`, + `query\\.persisted-write-precedence.*random seed`, ], [ `query.retained-scoped-hydration`, - `revalidates generated retained scopes \\(random\\)`, - ], - [ - `query.cached-peer-claim`, - `orders generated cached peer claims \\(random\\)`, + `query\\.retained-scoped-hydration.*random seed`, ], + [`query.cached-peer-claim`, `query\\.cached-peer-claim.*random seed`], ...[`controlled`, `node-sqlite`].map( (host) => [ @@ -1021,7 +1027,7 @@ const replayTestNames = new Map([ (adapter) => [ `adapter.${adapter}.eager-reload`, - `reconciles generated reload histories \\(random\\)`, + `adapter\\.${adapter}\\.eager-reload.*random seed`, ] as const, ), ...[ @@ -1031,10 +1037,7 @@ const replayTestNames = new Map([ ), ].map( (property) => - [ - property, - `reconciles generated startup histories \\(random\\)`, - ] as const, + [property, `${property.replaceAll(`.`, `\\.`)}.*random seed`] as const, ), [ `scripts.legacy-campaign-baseline`, diff --git a/packages/query-db-collection/tests/persisted-precedence-driver.ts b/packages/query-db-collection/tests/persisted-precedence-driver.ts index 8e38e6f631..99f6b53047 100644 --- a/packages/query-db-collection/tests/persisted-precedence-driver.ts +++ b/packages/query-db-collection/tests/persisted-precedence-driver.ts @@ -36,7 +36,7 @@ function gate() { return { promise, release } } const snapshot = (rows: Iterable) => - Array.from(rows, (row) => ({ ...row })).sort((a, b) => + Array.from(rows, ({ id, value }) => ({ id, value })).sort((a, b) => a.id.localeCompare(b.id), ) export async function observePersistedPrecedence( @@ -44,6 +44,7 @@ export async function observePersistedPrecedence( h: PrecedenceHistory, id: string, holdBaseline?: (id: string) => HeldCall, + record?: (checkpoint: string, value: unknown) => void, ): Promise { const queryHash = hashKey([id]) const seed = createCollection( @@ -233,6 +234,7 @@ export async function observePersistedPrecedence( await written const afterWrite = h.phase === `before-restore` ? undefined : snapshot(collection.values()) + if (afterWrite) record?.(`afterWrite`, afterWrite) const firstFetch = h.phase === `before-fetch` ? collection.utils.refetch({ throwOnError: true }) @@ -244,17 +246,25 @@ export async function observePersistedPrecedence( await firstFetch await preload const first = snapshot(collection.values()) + record?.(`first`, first) const firstDurable = await durable() + record?.(`firstDurable`, firstDurable) const firstCache = snapshot(decode(client.getQueryData([id])!)) + record?.(`firstCache`, firstCache) let later: Array | undefined let laterDurable: Array | undefined if (h.laterFetch) { later = await collection.utils .refetch({ throwOnError: true }) - .then(() => snapshot(collection.values())) + .then(() => { + const rows = snapshot(collection.values()) + record?.(`later`, rows) + return rows + }) laterDurable = await durable() + record?.(`laterDurable`, laterDurable) } - return { + const observed = { afterWrite, first, firstDurable, @@ -266,6 +276,8 @@ export async function observePersistedPrecedence( heldRestore, fetches, } + record?.(`completed`, observed) + return observed }, [ resultGate.release, scanGate.release, diff --git a/packages/query-db-collection/tests/persisted-precedence-oracle.test.ts b/packages/query-db-collection/tests/persisted-precedence-oracle.test.ts index 3aa6c81dfb..643b6b48bc 100644 --- a/packages/query-db-collection/tests/persisted-precedence-oracle.test.ts +++ b/packages/query-db-collection/tests/persisted-precedence-oracle.test.ts @@ -1,170 +1,261 @@ /** - * The ownership model receives real Query and storage on both Node hosts. - * Fixed and fresh generated histories share the same driver and comparison. - * Each host/campaign independently requires each shape, held boundary, operation, - * and the later-fetch neighbor. Browser receivers import these same parts. + * Real Query and both Node storage hosts receive the ownership oracle's same + * independent fetch/write judgment. Framework constructions keep random values + * and neighboring actions while ensuring every required order is exercised. + * Fixed regressions remain in the generated language and supply no reach. */ import fc from 'fast-check' -import { describe, expect, it } from 'vitest' +import { describe } from 'vitest' import { declareOracle } from '../../db/tests/oracle-declaration' -import { createOracleReach } from '../../db/tests/oracle-reach' import { - oracleFixedSeed, - oraclePropertyOptions, - oracleReplayTest, - oracleRoute, - oracleRouteOf, - oracleRuns, -} from '../../db/tests/oracle-config' + differs, + oracleProperty, + pass, + violation, +} from '../../db/tests/oracle-property' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' import { observePersistedPrecedence } from './persisted-precedence-driver' import { + expectedPersistedPrecedence, precedenceHistories, precedenceReached, precedenceStates, + precedenceValues, precedenceViolations, } from './persisted-precedence-oracle' -import type { PrecedenceHistory } from './persisted-precedence-oracle' +import type { + PrecedenceHistory, + PrecedenceObservation, +} from './persisted-precedence-oracle' +import type { + ConstructedHistory, + Observation, +} from '../../db/tests/oracle-property' +import type { MergeRow } from './direct-write-precedence-oracle' +const writable = { + shape: fc.constantFrom('raw' as const, 'derived' as const), + op: fc.constantFrom( + 'insert' as const, + 'update' as const, + 'delete' as const, + 'upsert' as const, + ), + laterFetch: fc.boolean(), + value: fc.integer({ min: -10, max: 10 }), +} +// Records preserve the previous union and branch weights, while accepting +// fixed/constructed objects without a fast-check map's private shrink context. const historyArb: fc.Arbitrary = fc.oneof( - fc - .tuple( - fc.record({ - shape: fc.constantFrom(`raw`, `derived`), - op: fc.constantFrom(`insert`, `update`, `delete`, `upsert`), - laterFetch: fc.boolean(), - value: fc.integer({ min: -10, max: 10 }), - }), - fc.oneof( - fc.constant({ phase: `before-result` } as const), - fc.constant({ phase: `before-fetch` } as const), - fc.record({ - phase: fc.constant(`during-baseline` as const), - scanCut: fc.constantFrom( - `before-read` as const, - `after-read` as const, - ), - }), - ), - ) - .map(([history, cut]) => ({ ...history, ...cut })), + fc.oneof( + fc.record({ ...writable, phase: fc.constant('before-result' as const) }), + fc.record({ ...writable, phase: fc.constant('before-fetch' as const) }), + fc.record({ + ...writable, + phase: fc.constant('during-baseline' as const), + scanCut: fc.constantFrom('before-read' as const, 'after-read' as const), + }), + ), fc.record({ - shape: fc.constantFrom(`raw` as const, `derived` as const), - phase: fc.constant(`before-restore` as const), - op: fc.constantFrom(`insert` as const, `upsert` as const), - laterFetch: fc.boolean(), - value: fc.integer({ min: -10, max: 10 }), + ...writable, + phase: fc.constant('before-restore' as const), + op: fc.constantFrom('insert' as const, 'upsert' as const), }), fc.record({ - shape: fc.constantFrom(`raw` as const, `derived` as const), - origin: fc.constant(`cache` as const), - phase: fc.constant(`during-baseline` as const), - scanCut: fc.constantFrom(`before-read` as const, `after-read` as const), - op: fc.constantFrom( - `insert` as const, - `update` as const, - `delete` as const, - `upsert` as const, - ), - laterFetch: fc.boolean(), - value: fc.integer({ min: -10, max: 10 }), + ...writable, + origin: fc.constant('cache' as const), + phase: fc.constant('during-baseline' as const), + scanCut: fc.constantFrom('before-read' as const, 'after-read' as const), }), ) -declareOracle({ - owns: [`src/query.ts#queryCollectionOptions`], - grammar: [historyArb, precedenceHistories], - check: [precedenceViolations], +const constructedStates = new WeakMap< + PrecedenceHistory, + ReadonlyArray +>() +const base = (h: PrecedenceHistory) => ({ + shape: h.shape, + op: h.op, + laterFetch: h.laterFetch, + value: h.value, }) -for (const host of [`controlled`, `node-sqlite`] as const) - describe(`persisted precedence through ${host}`, () => { +function constructState( + h: PrecedenceHistory, + state: string, +): PrecedenceHistory { + if (state === 'raw' || state === 'derived') return { ...h, shape: state } + if (state === 'fetched-result') { + const { origin: _origin, ...fetched } = h + return fetched + } + if (state === 'cached-result') + return { + ...base(h), + origin: 'cache', + phase: 'during-baseline', + scanCut: 'before-read', + } + if (state === 'before-result' || state === 'before-fetch') + return { ...base(h), phase: state } + if (state === 'before-restore') + return { + ...base(h), + phase: state, + op: h.op === 'upsert' ? 'upsert' : 'insert', + } + if ( + state === 'during-baseline' || + state === 'before-read' || + state === 'after-read' + ) + return { + ...base(h), + ...(h.origin === 'cache' ? { origin: 'cache' as const } : {}), + phase: 'during-baseline', + scanCut: state === 'after-read' ? 'after-read' : 'before-read', + } + if (state === 'later-fetch') return { ...h, laterFetch: true } + if ( + state === 'insert' || + state === 'update' || + state === 'delete' || + state === 'upsert' + ) + return h.phase === 'before-restore' && + (state === 'update' || state === 'delete') + ? { ...base(h), phase: 'before-result', op: state } + : ({ ...h, op: state } as PrecedenceHistory) + throw new Error('Unknown precedence construction state: ' + state) +} +const constructed: Array> = + precedenceStates.map((state) => ({ + name: state, + enters: [state], + build: (h) => { + const built = constructState(h, state) + // A fresh identity per invocation avoids the pinned framework's builder + // map collision; this ledger also guards its generated constructions. + constructedStates.set(built, [state]) + return built + }, + })) + +declareOracle({ owns: ['src/query.ts#queryCollectionOptions'] }) +for (const host of ['controlled', 'node-sqlite'] as const) + describe('persisted precedence through ' + host, () => { let sequence = 0 - const check = async ( - h: PrecedenceHistory, - reach: ReturnType, - ) => { - reach.beginHistory() - const storage = createStartupHost(host) - try { - const observed = await observePersistedPrecedence( - storage.persistence, + oracleProperty({ + id: 'query.persisted-write-precedence', + law: { + statement: + 'An older source result preserves accepted newer direct writes while removing omitted saved rows; a later fetch can replace them.', + authority: + 'Query collection guide: Direct Writes and Query Sync; Individual Write Operations.', + owner: 'Query ownership lifecycle; persisted precedence receiving path', + limits: + 'One eager run, three saved keys, one direct write, raw/derived response, optional later fetch; no peer writer or failed I/O.', + }, + histories: { + arbitrary: historyArb, + runs: 80, + fixedSeeds: [2394], + states: precedenceStates, + constructed, + fixed: precedenceHistories.map((history, i) => ({ + name: String(i), + history, + })), + }, + model: expectedPersistedPrecedence, + driver: async (h, observe) => { + const storage = createStartupHost(host) + await withOracleCleanup(async () => { + await observePersistedPrecedence( + storage.persistence, + h, + 'precedence-' + host + '-' + sequence++, + undefined, + observe, + ) + for (const state of precedenceReached(h)) observe('reach', state) + }, [() => storage.close()]) + }, + check: (h, trace, expected) => { + // Judge recorded cuts first. A later driver/cleanup failure cannot hide + // an already recorded wrong public, durable or cache observation. + for (const observation of trace) { + if (!(observation.checkpoint in expected)) continue + const cut = observation.checkpoint as keyof typeof expected + const owed = expected[cut] + if (owed === undefined) continue + const mismatch = differs( + cut, + 'write precedence', + precedenceValues(observation.value as Array>), + owed, + ) + if (mismatch) return mismatch + } + const completed = trace.find((o) => o.checkpoint === 'completed') + if (!completed) + return violation( + 'completed', + 'the receiving history did not complete', + 'write precedence', + ) + const failures = precedenceViolations( h, - `precedence-${host}-${sequence++}`, + completed.value as PrecedenceObservation, ) - for (const state of precedenceReached(h)) reach.reach(state) - expect(precedenceViolations(h, observed)).toEqual([]) - } finally { - storage.close() - } - } - oracleReplayTest(it)(`orders fixed fetch/write histories`, async () => { - oracleRoute(`fixed history`) - const reach = createOracleReach( - `query.precedence.${host}.corpus`, - precedenceStates, - ) - for (const h of precedenceHistories) await check(h, reach) - reach.assertReached() - console.info( - `query.precedence.${host}.corpus`, - Object.fromEntries(reach.counts()), - ) - }) - it(`rejects a first-result observation that lost the newer inserted row`, async () => { - oracleRoute(`calibration`) - const h: PrecedenceHistory = { - shape: `derived`, - phase: `before-result`, - op: `insert`, - laterFetch: false, - value: 9, - } - const storage = createStartupHost(host) - try { - const observed = await observePersistedPrecedence( - storage.persistence, - h, - `precedence-calibration-${host}`, + if (failures.length) + return violation('completed', failures.join('; '), 'write precedence') + const reached = new Set( + trace.filter((o) => o.checkpoint === 'reach').map((o) => o.value), + ) + const missing = (constructedStates.get(h) ?? []).filter( + (state) => !reached.has(state), ) - expect(precedenceViolations(h, observed)).toEqual([]) - expect( - precedenceViolations(h, { - ...observed, - first: observed.first.filter((row) => row.id !== `new`), - }).some((v) => v.startsWith(`first:`)), - ).toBe(true) - } finally { - storage.close() - } + return missing.length + ? violation( + 'constructed states', + 'Missing generated construction states: ' + missing.join(', '), + 'constructed reach', + ) + : pass + }, + calibrations: [ + ...(['first', 'firstDurable'] as const).map((cut) => ({ + name: 'loses a newer inserted row at ' + cut, + law: 'write precedence', + corrupt: ( + trace: ReadonlyArray, + h: PrecedenceHistory, + ) => { + if (h.op !== 'insert' || h.phase === 'before-fetch') + return undefined + return trace.map((o) => + o.checkpoint === cut + ? { + ...o, + value: (o.value as Array>).filter( + (row) => row.id !== 'new', + ), + } + : o, + ) + }, + })), + ...precedenceStates.map((state) => ({ + name: 'omits constructed state ' + state, + law: 'constructed reach', + corrupt: (trace: ReadonlyArray, h: PrecedenceHistory) => + constructedStates.get(h)?.includes(state) + ? trace.filter( + (o) => o.checkpoint !== 'reach' || o.value !== state, + ) + : undefined, + })), + ], + timeout: 60_000, }) - for (const campaign of [`fixed`, `random`] as const) { - const property = `query.persisted-write-precedence` - const register = - campaign === `fixed` - ? oracleReplayTest(it) - : oracleReplayTest(it, property) - register( - `orders generated fetch/write histories (${campaign})`, - async () => { - const parameters = - campaign === `fixed` - ? oracleFixedSeed(2394, { numRuns: oracleRuns(80) }) - : oraclePropertyOptions(80, property) - oracleRoute(oracleRouteOf(parameters)) - const reach = createOracleReach( - `query.precedence.${host}.${campaign}`, - precedenceStates, - ) - await fc.assert( - fc.asyncProperty(historyArb, (h) => check(h, reach)), - parameters, - ) - reach.assertReached() - console.info( - `query.precedence.${host}.${campaign}`, - Object.fromEntries(reach.counts()), - ) - }, - 60_000, - ) - } }) diff --git a/packages/query-db-collection/tests/persisted-precedence-oracle.ts b/packages/query-db-collection/tests/persisted-precedence-oracle.ts index 3bc5f8b7c4..50bed8a6a6 100644 --- a/packages/query-db-collection/tests/persisted-precedence-oracle.ts +++ b/packages/query-db-collection/tests/persisted-precedence-oracle.ts @@ -114,8 +114,27 @@ export type PrecedenceObservation = { heldRestore: boolean fetches: number } -const values = (rows: Array>) => +export const precedenceValues = (rows: Array>) => Object.fromEntries(rows.map((r) => [r.id, r.value])) +/** The same independent overlay predicts each captured public/cache/durable cut. */ +export function expectedPersistedPrecedence(h: PrecedenceHistory) { + const writes = precedenceWrites(h) + const first = mergeDirectWritesOverFetch( + precedenceFetched, + h.phase === `before-fetch` ? [] : writes, + ) + return { + afterWrite: + h.phase === `before-restore` + ? undefined + : mergeDirectWritesOverFetch(precedenceSaved, writes), + first, + firstDurable: first, + firstCache: h.shape === `raw` ? first : precedenceValues(precedenceFetched), + later: h.laterFetch ? precedenceValues(precedenceLater) : undefined, + laterDurable: h.laterFetch ? precedenceValues(precedenceLater) : undefined, + } +} export function precedenceViolations( h: PrecedenceHistory, o: PrecedenceObservation, @@ -128,37 +147,25 @@ export function precedenceViolations( ) => { if ( !actual || - JSON.stringify(Object.entries(values(actual)).sort()) !== + JSON.stringify(Object.entries(precedenceValues(actual)).sort()) !== JSON.stringify(Object.entries(expected).sort()) ) failures.push( `${cut}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, ) } - const writes = precedenceWrites(h) + const expected = expectedPersistedPrecedence(h) // With restore held, saved rows are not yet promised as a public base. Those // histories compare the first complete applied result and its durable state; // the other histories also compare the accepted write while the source waits. if (h.phase !== `before-restore`) - same( - `afterWrite`, - o.afterWrite, - mergeDirectWritesOverFetch(precedenceSaved, writes), - ) - const first = mergeDirectWritesOverFetch( - precedenceFetched, - h.phase === `before-fetch` ? [] : writes, - ) - same(`first`, o.first, first) - same(`firstDurable`, o.firstDurable, first) - same( - `firstCache`, - o.firstCache, - h.shape === `raw` ? first : values(precedenceFetched), - ) + same(`afterWrite`, o.afterWrite, expected.afterWrite!) + same(`first`, o.first, expected.first) + same(`firstDurable`, o.firstDurable, expected.firstDurable) + same(`firstCache`, o.firstCache, expected.firstCache) if (h.laterFetch) { - same(`later`, o.later, values(precedenceLater)) - same(`laterDurable`, o.laterDurable, values(precedenceLater)) + same(`later`, o.later, expected.later!) + same(`laterDurable`, o.laterDurable, expected.laterDurable!) } if (h.phase === `during-baseline` && !o.heldScan) failures.push(`missing held baseline scan`) @@ -194,7 +201,7 @@ export const precedenceReached = (h: PrecedenceHistory) => [ ...(h.laterFetch ? [`later-fetch`] : []), ] declareOracle({ - model: [mergeDirectWritesOverFetch], + model: [expectedPersistedPrecedence], grammar: [precedenceHistories], check: [precedenceViolations], }) diff --git a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts index 374a148e9c..14d58478da 100644 --- a/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts +++ b/packages/query-db-collection/tests/persisted-startup-conformance-oracle.test.ts @@ -6,11 +6,6 @@ import { describe } from 'vitest' import fc from 'fast-check' import { declareOracle } from '../../db/tests/oracle-declaration' -import { - persistedStartupMembershipArb, - persistedStartupRenderArb, - withStartupRender, -} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { queryStartupAdapter, @@ -18,56 +13,41 @@ import { } from './persisted-startup-driver' import type { QueryStartupHistory } from './persisted-startup-driver' -const startupMembershipArb = persistedStartupMembershipArb(`scoped`) +// The record states the same legal language as the former chained render +// grammar. A cached/initial answer is already available before restore, while +// only a held fetch can cross restore or reach network-first fallback. Optional +// fields are absent when that action was not authored, matching fixed replays. const queryStartupHistoryArb: fc.Arbitrary = fc - .oneof( - { - weight: 3, - arbitrary: fc.record({ - membership: startupMembershipArb, - origin: fc.constant(`fetch` as const), - response: fc.constantFrom( - `before-restore` as const, - `after-restore` as const, - ), - }), - }, + .record( { - weight: 4, - arbitrary: fc.record({ - membership: startupMembershipArb, - origin: fc.constantFrom( - `initial-disabled` as const, - `initial-pending` as const, - `cached-disabled` as const, - `cached-pending` as const, - ), - response: fc.constant(`before-restore` as const), - }), + previous: fc.integer({ min: 0, max: 7 }), + next: fc.oneof(fc.constant(0), fc.integer({ min: 0, max: 7 })), + peer: fc.boolean(), + origin: fc.constantFrom( + `fetch` as const, + `initial-disabled` as const, + `initial-pending` as const, + `cached-disabled` as const, + `cached-pending` as const, + ), + response: fc.constantFrom( + `before-restore` as const, + `after-restore` as const, + `after-deadline` as const, + ), + mutation: fc.constant(`persisting` as const), + previousOwner: fc.constant(`none` as const), + initialRender: fc.constant(`network-first` as const), }, + { requiredKeys: [`previous`, `next`, `peer`, `origin`, `response`] }, ) - .map(({ membership, ...schedule }) => ({ ...membership, ...schedule })) - .chain((h) => - fc - .tuple( - persistedStartupRenderArb, - fc.boolean(), - fc.boolean(), - fc.boolean(), - ) - .map(([network, fallback, mutation, ownerless]) => - withStartupRender( - { - ...h, - ...(mutation ? { mutation: `persisting` as const } : {}), - ...(ownerless && h.previous - ? { previousOwner: `none` as const } - : {}), - }, - network, - fallback && h.origin === `fetch`, - ), - ), + .filter( + (h) => + (!h.peer || h.previous !== 0) && + (h.previousOwner === undefined || h.previous !== 0) && + (h.origin === `fetch` || h.response === `before-restore`) && + (h.response !== `after-deadline` || + (h.initialRender === `network-first` && h.previous !== 0)), ) // The receiving path judges persisted startup through this adapter entry. diff --git a/packages/query-db-collection/tests/retained-scoped-hydration-driver.ts b/packages/query-db-collection/tests/retained-scoped-hydration-driver.ts index 43427f6cdd..1a0c6a34da 100644 --- a/packages/query-db-collection/tests/retained-scoped-hydration-driver.ts +++ b/packages/query-db-collection/tests/retained-scoped-hydration-driver.ts @@ -55,6 +55,7 @@ export async function observeCachedPeerClaim( h: CachedPeerClaimHistory, id: string, sourceFactory = queryCollectionOptions, + record?: (checkpoint: string, value: unknown) => void, ): Promise { const client = new QueryClient({ defaultOptions: { @@ -108,17 +109,28 @@ export async function observeCachedPeerClaim( }) await collection._sync.loadSubset(broad) const before = collection.get(`shared`)?.value + record?.(`before`, before) if (h.cache === `manual`) client.setQueryData(narrowKey, [row(h.cached)]) const claimed = await Promise.resolve( collection._sync.loadSubset(narrow), - ).then(() => collection.get(`shared`)?.value) + ).then(() => { + const value = collection.get(`shared`)?.value + record?.(`claimed`, value) + return value + }) const claimedDurable = await durableValue() + record?.(`claimedDurable`, claimedDurable) const callsBeforeRefetch = calls const refetched = await Promise.resolve( collection._sync.loadSubset({ ...narrow, refetch: true }), - ).then(() => collection.get(`shared`)?.value) + ).then(() => { + const value = collection.get(`shared`)?.value + record?.(`refetched`, value) + return value + }) const refetchedDurable = await durableValue() - return { + record?.(`refetchedDurable`, refetchedDurable) + const observed = { before, claimed, claimedDurable, @@ -127,6 +139,8 @@ export async function observeCachedPeerClaim( callsBeforeRefetch, calls, } + record?.(`completed`, observed) + return observed }, [() => collection.cleanup(), () => client.clear()]) } export async function observeRetainedScopedHydration( @@ -134,6 +148,7 @@ export async function observeRetainedScopedHydration( h: ScopedHydrationHistory, id: string, sourceFactory = queryCollectionOptions, + record?: (checkpoint: string, value: unknown) => void, ): Promise { const makeClient = () => new QueryClient({ @@ -293,18 +308,29 @@ export async function observeRetainedScopedHydration( return withOracleCleanup(async () => { const firstLoad = Promise.resolve( collection._sync.loadSubset(firstDemand), - ).then(() => sortScopedRows(collection.values())) + ).then(() => { + const rows = sortScopedRows(collection.values()) + record?.(`first`, rows) + return rows + }) void firstLoad.catch(() => {}) await untilStartup(() => fetches === 1) const pending = sortScopedRows(collection.values()) + record?.(`pending`, pending) release() const first = await firstLoad const firstDurable = await durableRows!() + record?.(`firstDurable`, firstDurable) const second = await Promise.resolve( collection._sync.loadSubset(otherDemand), - ).then(() => sortScopedRows(collection.values())) + ).then(() => { + const rows = sortScopedRows(collection.values()) + record?.(`second`, rows) + return rows + }) const secondDurable = await durableRows!() - return { + record?.(`secondDurable`, secondDurable) + const observed = { pending, first, firstDurable, @@ -315,5 +341,7 @@ export async function observeRetainedScopedHydration( hydrateCalls, scanCalls, } + record?.(`completed`, observed) + return observed }, [release, () => collection.cleanup(), () => client.clear()]) } diff --git a/packages/query-db-collection/tests/retained-scoped-hydration-oracle.test.ts b/packages/query-db-collection/tests/retained-scoped-hydration-oracle.test.ts index b90f241188..ad954e4992 100644 --- a/packages/query-db-collection/tests/retained-scoped-hydration-oracle.test.ts +++ b/packages/query-db-collection/tests/retained-scoped-hydration-oracle.test.ts @@ -1,17 +1,17 @@ -/** The scope law receives controlled storage and the real SQLite demand path. */ -import fc from 'fast-check' -import { describe, expect, it } from 'vitest' +/** + * The scope and cached-first-claim laws receive controlled storage and real + * SQLite through their unchanged shared drivers and judgments. Randomized + * constructions enforce required states without crediting fixed histories. + */ +import { describe } from 'vitest' import { declareOracle } from '../../db/tests/oracle-declaration' -import { createOracleReach } from '../../db/tests/oracle-reach' -import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import { - oracleFixedSeed, - oraclePropertyOptions, - oracleReplayTest, - oracleRoute, - oracleRouteOf, - oracleRuns, -} from '../../db/tests/oracle-config' + differs, + oracleProperty, + pass, + violation, +} from '../../db/tests/oracle-property' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' import { observeCachedPeerClaim, @@ -24,6 +24,8 @@ import { cachedPeerClaimReached, cachedPeerClaimStates, cachedPeerClaimViolations, + expectedCachedPeerClaim, + expectedScopedHydration, scopedHydrationHistories, scopedHydrationHistoryArb, scopedHydrationReached, @@ -32,199 +34,302 @@ import { } from './retained-scoped-hydration-oracle' import type { CachedPeerClaimHistory, + CachedPeerClaimObservation, ScopedHydrationHistory, + ScopedHydrationObservation, } from './retained-scoped-hydration-oracle' import type { PersistenceAdapter } from '../../db-sqlite-persistence-core/src' +import type { + ConstructedHistory, + Observation, + Verdict, +} from '../../db/tests/oracle-property' -declareOracle({ - owns: [`src/query.ts#queryCollectionOptions`], - grammar: [ - scopedHydrationHistoryArb, - scopedHydrationHistories, - cachedPeerClaimHistoryArb, - cachedPeerClaimHistories, - ], - check: [scopedHydrationViolations, cachedPeerClaimViolations], -}) -for (const host of [`controlled`, `node-sqlite`] as const) - describe(`retained scoped hydration through ${host}`, () => { +const scopedConstructedStates = new WeakMap< + ScopedHydrationHistory, + ReadonlyArray +>() +const cachedConstructedStates = new WeakMap< + CachedPeerClaimHistory, + ReadonlyArray +>() +function constructScope( + h: ScopedHydrationHistory, + state: string, +): ScopedHydrationHistory { + const built = { ...h } + if (state === 'first-a') built.first = 'a' + else if (state === 'first-b') built.first = 'b' + else if (state === 'empty-result') built.next = 0 + else if (state === 'nonempty-result') built.next ||= 1 + else if (state === 'unrequested-saved-rows') built.otherPrevious ||= 1 + else if (state === 'omitted-owned-row') { + built.previous = 3 + built.next &= ~1 + } else if (state === 'ownerless-first') built.ownerless = 'first' + else if (state === 'ownerless-other') built.ownerless = 'other' + else if (state === 'no-ownerless') built.ownerless = 'none' + else if (state === 'unbounded') built.window = 'all' + else if (state === 'limited') built.window = 'one' + else if (state === 'limited-ownerless-restored') { + built.window = 'one' + built.ownerless = 'first' + } else throw new Error('Unknown scope construction state: ' + state) + return built +} +const scopeConstructions: Array> = + scopedHydrationStates.map((state) => ({ + name: state, + enters: [state], + build: (h) => { + const built = constructScope(h, state) + scopedConstructedStates.set(built, [state]) + return built + }, + })) +const differentValue = (value: number) => (value === 10 ? -10 : value + 1) +const cacheConstructions: Array> = + cachedPeerClaimStates.map((state) => ({ + name: state, + enters: [state], + build: (h) => { + const built = { ...h } + if (state === 'manual' || state === 'older-fetch') built.cache = state + else if ( + state === 'different-cached-value' && + built.cached === built.current + ) + built.cached = differentValue(built.current) + else if (state === 'later-value-change' && built.later === built.current) + built.later = differentValue(built.current) + // Each invocation returns a fresh identity. The local ledger enforces + // generated outputs too, which the pinned framework does not associate. + cachedConstructedStates.set(built, [state]) + return built + }, + })) + +/** Compare captured prefixes before completeness, preserving earlier failures. */ +function checkReceivingTrace( + trace: ReadonlyArray, + expected: Record, + completedCheck: (actual: unknown) => Array, + required: ReadonlyArray, + law: string, +): Verdict { + for (const { checkpoint, value } of trace) { + if (!(checkpoint in expected)) continue + const mismatch = differs(checkpoint, law, value, expected[checkpoint]) + if (mismatch) return mismatch + } + const completed = trace.find((o) => o.checkpoint === 'completed') + if (!completed) + return violation('completed', 'the receiving history did not complete', law) + const failures = completedCheck(completed.value) + if (failures.length) return violation('completed', failures.join('; '), law) + const reached = new Set( + trace.filter((o) => o.checkpoint === 'reach').map((o) => o.value), + ) + const missing = required.filter((state) => !reached.has(state)) + return missing.length + ? violation( + 'constructed states', + 'Missing generated construction states: ' + missing.join(', '), + 'constructed reach', + ) + : pass +} +function omitConstructedState( + trace: ReadonlyArray, + required: ReadonlyArray, +): ReadonlyArray | undefined { + const [state] = required + return state === undefined + ? undefined + : trace.filter((o) => o.checkpoint !== 'reach' || o.value !== state) +} + +declareOracle({ owns: ['src/query.ts#queryCollectionOptions'] }) +for (const host of ['controlled', 'node-sqlite'] as const) + describe('retained scoped hydration through ' + host, () => { let sequence = 0 - async function check( - h: ScopedHydrationHistory, - reach?: ReturnType, - ) { - const storage = createStartupHost(host) - if (host === `controlled`) { - // The controlled persistence adapter supplies the same scope predicate - // that real SQLite evaluates. It returns stored facts, not model rows. - const adapter: PersistenceAdapter = storage.persistence.adapter - const loadSubset = adapter.loadSubset - adapter.loadSubset = async (target, options, context) => { - const entries = await loadSubset(target, options, context) - if (!options.where) return entries - const scope = requestedRetainedScope(options) - const matching = entries.filter((row) => row.value.scope === scope) - if (options.limit === undefined) return matching - return matching - .sort((a, b) => String(b.key).localeCompare(String(a.key))) - .slice(0, options.limit) - } - } - return withOracleCleanup(async () => { - reach?.beginHistory() - const actual = await observeRetainedScopedHydration( - storage.persistence, - h, - `scoped-${host}-${sequence++}`, - ) - for (const state of scopedHydrationReached(h)) reach?.reach(state) - expect( - scopedHydrationViolations(h, actual), - JSON.stringify({ h, actual }), - ).toEqual([]) - return actual - }, [() => storage.close()]) - } - oracleReplayTest(it)( - `revalidates only the acquired retained scope`, - async () => { - oracleRoute(`fixed history`) - const reach = createOracleReach( - `query.scoped-hydration.${host}.histories`, - [...scopedHydrationStates], - ) - for (const h of scopedHydrationHistories) await check(h, reach) - reach.assertReached() - process.stdout.write( - `query.scoped-hydration.${host}.histories ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, - ) + oracleProperty({ + id: 'query.retained-scoped-hydration', + law: { + statement: + 'An on-demand persisted Collection restores only acquired demand; limited results cannot prove ownerless-row omission or retire a retained broader owner.', + authority: + 'SQLite persistence guide: Persist a collection; Query guide: QueryFn and Predicate Push-Down, Direct Writes and Query Sync.', + owner: + 'Query ownership lifecycle; retained scoped hydration receiving path', + limits: + 'Two disjoint scopes, one cold reopen, all/one-row demands, numeric values and successful I/O; no peer writer or rotation.', }, - 60_000, - ) - oracleReplayTest(it)( - `rejects a full-store restore that publishes unrequested saved rows`, - async () => { - oracleRoute(`calibration`) - const h = scopedHydrationHistories[0]! - const actual = await check(h) - expect( - scopedHydrationViolations(h, { - ...actual, - first: actual.firstDurable, - }).some((v) => v.startsWith(`first:`)), - ).toBe(true) + histories: { + arbitrary: scopedHydrationHistoryArb, + runs: 60, + fixedSeeds: [1481], + states: scopedHydrationStates, + constructed: scopeConstructions, + fixed: scopedHydrationHistories.map((history, i) => ({ + name: String(i), + history, + })), }, - ) - for (const campaign of [`fixed`, `random`] as const) { - const property = `query.retained-scoped-hydration` - const register = - campaign === `fixed` - ? oracleReplayTest(it) - : oracleReplayTest(it, property) - register( - `revalidates generated retained scopes (${campaign})`, - async () => { - const parameters = - campaign === `fixed` - ? oracleFixedSeed(1481, { numRuns: oracleRuns(60) }) - : oraclePropertyOptions(60, property) - oracleRoute(oracleRouteOf(parameters)) - const reach = createOracleReach( - `query.scoped-hydration.${host}.${campaign}`, - [...scopedHydrationStates], - ) - await fc.assert( - fc.asyncProperty(scopedHydrationHistoryArb, async (h) => { - await check(h, reach) - }), - parameters, - ) - reach.assertReached() - process.stdout.write( - `query.scoped-hydration.${host}.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + model: expectedScopedHydration, + driver: async (h, observe) => { + const storage = createStartupHost(host) + if (host === 'controlled') { + // The fake host supplies only demand evaluation over stored facts; + // real SQLite evaluates the same predicate and limit itself. + const adapter: PersistenceAdapter = storage.persistence.adapter + const loadSubset = adapter.loadSubset + adapter.loadSubset = async (target, options, context) => { + const entries = await loadSubset(target, options, context) + if (!options.where) return entries + const scope = requestedRetainedScope(options) + const matching = entries.filter((row) => row.value.scope === scope) + if (options.limit === undefined) return matching + return matching + .sort((a, b) => String(b.key).localeCompare(String(a.key))) + .slice(0, options.limit) + } + } + await withOracleCleanup(async () => { + await observeRetainedScopedHydration( + storage.persistence, + h, + 'scoped-' + host + '-' + sequence++, + undefined, + observe, ) + for (const state of scopedHydrationReached(h)) observe('reach', state) + }, [() => storage.close()]) + }, + check: (h, trace, expected) => + checkReceivingTrace( + trace, + expected, + (actual) => + scopedHydrationViolations(h, actual as ScopedHydrationObservation), + scopedConstructedStates.get(h) ?? [], + 'scope restoration', + ), + calibrations: [ + { + name: 'publishes unrequested saved rows from a full-store restore', + law: 'scope restoration', + corrupt: (trace) => { + const first = trace.find((o) => o.checkpoint === 'first') + const durable = trace.find((o) => o.checkpoint === 'firstDurable') + if ( + !first || + !durable || + JSON.stringify(first.value) === JSON.stringify(durable.value) + ) + return undefined + return trace.map((o) => + o.checkpoint === 'first' ? { ...o, value: durable.value } : o, + ) + }, }, - 120_000, - ) - } - async function checkCached( - h: CachedPeerClaimHistory, - reach?: ReturnType, - ) { - const storage = createStartupHost(host) - return withOracleCleanup(async () => { - reach?.beginHistory() - const actual = await observeCachedPeerClaim( - storage.persistence, - h, - `cached-claim-${host}-${sequence++}`, - ) - for (const state of cachedPeerClaimReached(h)) reach?.reach(state) - expect( - cachedPeerClaimViolations(h, actual), - JSON.stringify({ h, actual }), - ).toEqual([]) - return actual - }, [() => storage.close()]) - } - oracleReplayTest(it)( - `keeps a peer value when a cached Query first claims it`, - async () => { - oracleRoute(`fixed history`) - const reach = createOracleReach( - `query.cached-claim.${host}.histories`, - [...cachedPeerClaimStates], - ) - for (const h of cachedPeerClaimHistories) await checkCached(h, reach) - reach.assertReached() - process.stdout.write( - `query.cached-claim.${host}.histories ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, - ) + ...scopedHydrationStates.map((state) => ({ + name: 'omits constructed state ' + state, + law: 'constructed reach', + corrupt: ( + trace: ReadonlyArray, + h: ScopedHydrationHistory, + ) => + scopedConstructedStates.get(h)?.includes(state) + ? omitConstructedState(trace, [state]) + : undefined, + })), + { + name: 'loses durable rows while public rows remain correct', + law: 'scope restoration', + corrupt: (trace) => { + const durable = trace.find((o) => o.checkpoint === 'firstDurable') + if (!Array.isArray(durable?.value) || durable.value.length === 0) + return undefined + return trace.map((o) => + o.checkpoint === 'firstDurable' ? { ...o, value: [] } : o, + ) + }, + }, + ], + }) + oracleProperty({ + id: 'query.cached-peer-claim', + law: { + statement: + 'A cached Query first claim cannot replace a continuously owned later-fetched value; a later physical refetch can.', + authority: + 'Query collection guide: Direct Writes and Query Sync, per-key fetch order and cache patches without successful-fetch authority.', + owner: + 'Query ownership lifecycle; persisted cached peer claim receiving path', + limits: + 'One overlapping key and QueryClient, manual/older fetched cache, later refetch, numeric values and successful I/O.', }, - ) - oracleReplayTest(it)( - `rejects stale cache overwrite at first claim`, - async () => { - oracleRoute(`calibration`) - const h = cachedPeerClaimHistories[0]! - const actual = await checkCached(h) - expect( - cachedPeerClaimViolations(h, { ...actual, claimed: h.cached }).some( - (v) => v.startsWith(`claimed:`), - ), - ).toBe(true) + histories: { + arbitrary: cachedPeerClaimHistoryArb, + runs: 60, + fixedSeeds: [1376], + states: cachedPeerClaimStates, + constructed: cacheConstructions, + fixed: cachedPeerClaimHistories.map((history, i) => ({ + name: String(i), + history, + })), }, - ) - for (const campaign of [`fixed`, `random`] as const) { - const property = `query.cached-peer-claim` - const register = - campaign === `fixed` - ? oracleReplayTest(it) - : oracleReplayTest(it, property) - register( - `orders generated cached peer claims (${campaign})`, - async () => { - const parameters = - campaign === `fixed` - ? oracleFixedSeed(1376, { numRuns: oracleRuns(60) }) - : oraclePropertyOptions(60, property) - oracleRoute(oracleRouteOf(parameters)) - const reach = createOracleReach( - `query.cached-claim.${host}.${campaign}`, - [...cachedPeerClaimStates], + model: expectedCachedPeerClaim, + driver: async (h, observe) => { + const storage = createStartupHost(host) + await withOracleCleanup(async () => { + await observeCachedPeerClaim( + storage.persistence, + h, + 'cached-claim-' + host + '-' + sequence++, + undefined, + observe, ) - await fc.assert( - fc.asyncProperty(cachedPeerClaimHistoryArb, async (h) => { - await checkCached(h, reach) - }), - parameters, - ) - reach.assertReached() - process.stdout.write( - `query.cached-claim.${host}.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, - ) - }, - 120_000, - ) - } + for (const state of cachedPeerClaimReached(h)) observe('reach', state) + }, [() => storage.close()]) + }, + check: (h, trace, expected) => + checkReceivingTrace( + trace, + expected, + (actual) => + cachedPeerClaimViolations(h, actual as CachedPeerClaimObservation), + cachedConstructedStates.get(h) ?? [], + 'cached claim authority', + ), + calibrations: [ + ...(['claimed', 'claimedDurable'] as const).map((cut) => ({ + name: + 'overwrites a continuously owned value from stale cache at ' + cut, + law: 'cached claim authority', + corrupt: ( + trace: ReadonlyArray, + h: CachedPeerClaimHistory, + ) => + h.cached === h.current + ? undefined + : trace.map((o) => + o.checkpoint === cut ? { ...o, value: h.cached } : o, + ), + })), + ...cachedPeerClaimStates.map((state) => ({ + name: 'omits constructed state ' + state, + law: 'constructed reach', + corrupt: ( + trace: ReadonlyArray, + h: CachedPeerClaimHistory, + ) => + cachedConstructedStates.get(h)?.includes(state) + ? omitConstructedState(trace, [state]) + : undefined, + })), + ], + }) }) diff --git a/packages/query-db-collection/tests/retained-scoped-hydration-oracle.ts b/packages/query-db-collection/tests/retained-scoped-hydration-oracle.ts index 4ffadd762b..1a761e4839 100644 --- a/packages/query-db-collection/tests/retained-scoped-hydration-oracle.ts +++ b/packages/query-db-collection/tests/retained-scoped-hydration-oracle.ts @@ -260,17 +260,20 @@ export const cachedPeerClaimReached = (h: CachedPeerClaimHistory) => [ ...(h.cached !== h.current ? [`different-cached-value`] : []), ...(h.current !== h.later ? [`later-value-change`] : []), ] -export function cachedPeerClaimViolations( - h: CachedPeerClaimHistory, - actual: CachedPeerClaimObservation, -): Array { - const expected = { +export function expectedCachedPeerClaim(h: CachedPeerClaimHistory) { + return { before: h.current, claimed: h.current, claimedDurable: h.current, refetched: h.later, refetchedDurable: h.later, } +} +export function cachedPeerClaimViolations( + h: CachedPeerClaimHistory, + actual: CachedPeerClaimObservation, +): Array { + const expected = expectedCachedPeerClaim(h) const violations: Array = [] for (const cut of [ `before`, @@ -286,6 +289,7 @@ export function cachedPeerClaimViolations( return violations } declareOracle({ + model: [expectedCachedPeerClaim], grammar: [cachedPeerClaimHistoryArb, cachedPeerClaimHistories], check: [cachedPeerClaimViolations], }) From af50717a7a0a6502e0e15cb62aa52d3c9ca807e0 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 16:33:40 -0700 Subject: [PATCH 18/24] Preserve persisted startup batching and extend oracle receiving coverage Law: complete eager results replace saved rows within their scope (READY-3, PUB-7 and durable acceptance), while preserving ordinary startup commits. Reach: six adapter receivers; PowerSync B=1/B=2 and transformed keys; both restore orders, network-first fallback and held optimistic publication. RED: expanded startup/work comparisons fail on unchanged sources on controlled storage, real Node SQLite and actual Chromium OPFS before the repairs. GREEN: Query 1663 tests; PowerSync/RxDB startup 254/104; retained reload 44 each; actual browser receiving, types, lint and builds pass. See versioned records. Mutant: original omission/value defects, lost persistence, transformed-key and work-bound controls fail at their intended comparisons; prior kills retained. Limits: bounded local-source histories; no remote replication or universal performance claim. Anchored mutation outcomes and unresolved allowances remain explicit in the final-validation and gap audit records. --- .changeset/fix-persisted-query-startup.md | 2 +- .github/workflows/e2e-tests.yml | 9 +- docs/contributing/oracle-coverage.md | 115 +- ...-11-10-eager-source-primary-conversions.md | 169 + ...-final-adapter-changed-code-gap-audit.json | 24141 ++++++++++++++++ ...10-final-adapter-changed-code-gap-audit.md | 97 + ...2026-11-10-persisted-baseline-lifecycle.md | 242 + .../2026-11-10-persisted-startup-batching.md | 257 + ...26-11-10-persisted-startup-measurements.md | 122 + .../2026-11-10-pr-2394-external-review.md | 12 + .../2026-11-10-pr-2394-final-validation.md | 108 + .../2026-11-10-pr-2394-rereview.md | 9 + ...10-primary-eager-conversion-retention.json | 576 + ...1-10-query-changed-callback-gap-audit.json | 1314 + ...-11-10-query-changed-callback-gap-audit.md | 96 + ...6-11-10-query-rejected-write-cutoff-gap.md | 222 + .../2026-11-10-query-scope-omission-gap.md | 207 + ...2026-11-10-query-startup-receiving-gaps.md | 262 + .../2026-11-10-query-sync-write-protocol.json | 377 + .../2026-11-10-query-sync-write-protocol.md | 140 + ...26-11-10-startup-property-gap-retention.md | 303 + ...-trailbase-browser-property-integration.md | 288 + ...railbase-lifecycle-framework-conversion.md | 164 + .../e2e/persisted-baseline-lifecycle.html | 13 + .../e2e/persisted-baseline-lifecycle.ts | 38 + ...ed-startup-conformance-oracle.opfs.spec.ts | 20 +- .../e2e/persisted-startup-conformance.opfs.ts | 21 +- ...ery-immediate-cache-oracle.browser.test.ts | 47 + .../e2e/query-ownership-browser-support.ts | 59 + .../e2e/query-ownership.opfs.html | 9 + .../e2e/query-ownership.opfs.ts | 52 + ...ed-direct-ownership-oracle.browser.test.ts | 47 + ...rejected-write-cutoff-bootstrap-error.html | 9 + .../query-rejected-write-cutoff-cleanup.ts | 29 + ...jected-write-cutoff-oracle.browser.test.ts | 117 + .../e2e/query-rejected-write-cutoff.opfs.html | 6 + .../e2e/query-rejected-write-cutoff.opfs.ts | 84 + ...uery-scope-omission-oracle.browser.test.ts | 60 + .../e2e/query-scope-omission.opfs.html | 6 + .../e2e/query-scope-omission.opfs.ts | 44 + .../e2e/trailbase-bootstrap-error.html | 11 + .../e2e/trailbase-eager.ts | 14 + .../e2e/trailbase-lifecycle.html | 9 + .../e2e/trailbase-lifecycle.ts | 37 + .../package.json | 1 + .../tests/adapter-chromium-host.ts | 131 + ...ersisted-baseline-lifecycle-oracle.test.ts | 66 + ...cted-write-cutoff-browser-fidelity.test.ts | 173 + .../tests/trailbase-browser-fidelity.test.ts | 205 + ...trailbase-eager-live-source-oracle.test.ts | 42 + .../tests/trailbase-hydration-oracle.test.ts | 36 + .../tsconfig.json | 1 + .../vite.config.ts | 12 +- .../vitest.adapter-oracles.config.ts | 41 + packages/db-adapter-conformance/MIGRATION.md | 22 +- packages/db-adapter-conformance/README.md | 31 +- .../src/eager-live-source-oracle.ts | 184 +- .../eager-live-source-framework-oracle.ts | 128 + .../tests/eager-live-source-oracle-suite.ts | 87 + .../tests/eager-reload-driver.ts | 16 +- .../tests/eager-reload-projection.test.ts | 78 + .../persisted-baseline-candidate-plugins.ts | 45 + .../persisted-baseline-lifecycle-driver.ts | 387 + ...rsisted-baseline-lifecycle-oracle-suite.ts | 236 + .../persisted-baseline-lifecycle-oracle.ts | 275 + .../tests/persisted-startup-driver.ts | 31 + .../tests/persisted-startup-oracle-suite.ts | 89 +- .../tests/persisted-startup-oracle.ts | 160 +- packages/db/tests/oracle-config.ts | 44 + packages/db/tests/oracle-replay-manifest.ts | 191 +- packages/db/tests/oracle-replay.test.ts | 30 + .../powersync-db-collection/src/powersync.ts | 84 +- ...source-conformance-oracle.property.test.ts | 112 +- .../tests/eager-live-source-history-oracle.ts | 49 + .../persisted-baseline-lifecycle-browser.ts | 19 + .../persisted-baseline-lifecycle-common.ts | 106 + .../persisted-baseline-lifecycle-driver.ts | 35 + ...ersisted-baseline-lifecycle-oracle.test.ts | 47 + .../tests/persisted-startup-common.ts | 30 +- ...tartup-conformance-oracle.property.test.ts | 6 +- packages/query-db-collection/package.json | 2 +- packages/query-db-collection/src/query.ts | 5 +- .../immediate-eager-materialization-driver.ts | 96 + ...diate-eager-materialization-oracle.test.ts | 15 + .../immediate-eager-materialization-oracle.ts | 331 + .../tests/ownership-bottom.oracle.test.ts | 716 + .../ownership-confirmation.oracle.test.ts | 1078 + .../tests/ownership-lifecycle.oracle.test.ts | 6277 +--- .../tests/ownership-model-oracle.ts | 868 + .../tests/ownership-models.test.ts | 571 + .../tests/ownership-settlement.oracle.test.ts | 1754 ++ .../tests/ownership-upper.oracle.test.ts | 2026 ++ .../persisted-direct-ownership-driver.ts | 253 + .../persisted-direct-ownership-oracle.test.ts | 29 + .../persisted-direct-ownership-oracle.ts | 521 + ...isted-rejected-write-cutoff-oracle.test.ts | 105 + .../persisted-rejected-write-cutoff-oracle.ts | 518 + .../tests/query-receiving-fidelity.test.ts | 130 + ...cope-omission-reacquisition-oracle.test.ts | 37 + .../scope-omission-reacquisition-oracle.ts | 503 + .../tests/upper-model-checks.test.ts | 61 + packages/query-db-collection/vite.config.ts | 14 + packages/query-db-collection/vitest.config.ts | 15 +- packages/rxdb-db-collection/src/rxdb.ts | 49 +- ...source-conformance-oracle.property.test.ts | 113 +- .../tests/eager-live-source-history-oracle.ts | 78 + .../persisted-baseline-lifecycle-driver.ts | 102 + ...ersisted-baseline-lifecycle-oracle.test.ts | 47 + ...tartup-conformance-oracle.property.test.ts | 6 +- .../tests/persisted-startup-driver.ts | 1 + packages/trailbase-db-collection/package.json | 2 +- .../trailbase-db-collection/tests/ORACLE.md | 46 +- .../tests/conformance-driver.ts | 62 +- ...source-conformance-oracle.property.test.ts | 64 +- .../eager-live-source-framework-oracle.ts | 202 + .../tests/eager-live-source-oracle-suite.ts | 98 + .../tests/lifecycle-hydration-oracle-suite.ts | 222 + .../tests/lifecycle-judgment-oracle.ts | 237 + .../tests/lifecycle-oracle.property.test.ts | 1226 +- .../bench/persisted-startup-2026-11-10.csv | 64 + scripts/bench/persisted-startup.mjs | 93 +- scripts/oracle-lint-settings.mjs | 20 - 122 files changed, 45503 insertions(+), 6348 deletions(-) create mode 100644 docs/contributing/oracle-reviews/2026-11-10-eager-source-primary-conversions.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-final-adapter-changed-code-gap-audit.json create mode 100644 docs/contributing/oracle-reviews/2026-11-10-final-adapter-changed-code-gap-audit.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-persisted-baseline-lifecycle.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-persisted-startup-batching.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-primary-eager-conversion-retention.json create mode 100644 docs/contributing/oracle-reviews/2026-11-10-query-changed-callback-gap-audit.json create mode 100644 docs/contributing/oracle-reviews/2026-11-10-query-changed-callback-gap-audit.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-query-rejected-write-cutoff-gap.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-query-scope-omission-gap.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-query-startup-receiving-gaps.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-query-sync-write-protocol.json create mode 100644 docs/contributing/oracle-reviews/2026-11-10-query-sync-write-protocol.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-startup-property-gap-retention.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-trailbase-browser-property-integration.md create mode 100644 docs/contributing/oracle-reviews/2026-11-10-trailbase-lifecycle-framework-conversion.md create mode 100644 packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-immediate-cache-oracle.browser.test.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-ownership-browser-support.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-persisted-direct-ownership-oracle.browser.test.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-bootstrap-error.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-cleanup.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-oracle.browser.test.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-scope-omission-oracle.browser.test.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-scope-omission.opfs.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/query-scope-omission.opfs.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/trailbase-bootstrap-error.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/trailbase-eager.ts create mode 100644 packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.html create mode 100644 packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.ts create mode 100644 packages/browser-db-sqlite-persistence/tests/adapter-chromium-host.ts create mode 100644 packages/browser-db-sqlite-persistence/tests/persisted-baseline-lifecycle-oracle.test.ts create mode 100644 packages/browser-db-sqlite-persistence/tests/query-rejected-write-cutoff-browser-fidelity.test.ts create mode 100644 packages/browser-db-sqlite-persistence/tests/trailbase-browser-fidelity.test.ts create mode 100644 packages/browser-db-sqlite-persistence/tests/trailbase-eager-live-source-oracle.test.ts create mode 100644 packages/browser-db-sqlite-persistence/tests/trailbase-hydration-oracle.test.ts create mode 100644 packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts create mode 100644 packages/db-adapter-conformance/tests/eager-live-source-framework-oracle.ts create mode 100644 packages/db-adapter-conformance/tests/eager-live-source-oracle-suite.ts create mode 100644 packages/db-adapter-conformance/tests/eager-reload-projection.test.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-baseline-candidate-plugins.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-driver.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle-suite.ts create mode 100644 packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle.ts create mode 100644 packages/powersync-db-collection/tests/eager-live-source-history-oracle.ts create mode 100644 packages/powersync-db-collection/tests/persisted-baseline-lifecycle-browser.ts create mode 100644 packages/powersync-db-collection/tests/persisted-baseline-lifecycle-common.ts create mode 100644 packages/powersync-db-collection/tests/persisted-baseline-lifecycle-driver.ts create mode 100644 packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts create mode 100644 packages/query-db-collection/tests/immediate-eager-materialization-driver.ts create mode 100644 packages/query-db-collection/tests/immediate-eager-materialization-oracle.test.ts create mode 100644 packages/query-db-collection/tests/immediate-eager-materialization-oracle.ts create mode 100644 packages/query-db-collection/tests/ownership-bottom.oracle.test.ts create mode 100644 packages/query-db-collection/tests/ownership-confirmation.oracle.test.ts create mode 100644 packages/query-db-collection/tests/ownership-model-oracle.ts create mode 100644 packages/query-db-collection/tests/ownership-models.test.ts create mode 100644 packages/query-db-collection/tests/ownership-settlement.oracle.test.ts create mode 100644 packages/query-db-collection/tests/ownership-upper.oracle.test.ts create mode 100644 packages/query-db-collection/tests/persisted-direct-ownership-driver.ts create mode 100644 packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts create mode 100644 packages/query-db-collection/tests/persisted-direct-ownership-oracle.ts create mode 100644 packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.test.ts create mode 100644 packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.ts create mode 100644 packages/query-db-collection/tests/query-receiving-fidelity.test.ts create mode 100644 packages/query-db-collection/tests/scope-omission-reacquisition-oracle.test.ts create mode 100644 packages/query-db-collection/tests/scope-omission-reacquisition-oracle.ts create mode 100644 packages/query-db-collection/tests/upper-model-checks.test.ts create mode 100644 packages/rxdb-db-collection/tests/eager-live-source-history-oracle.ts create mode 100644 packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-driver.ts create mode 100644 packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts create mode 100644 packages/trailbase-db-collection/tests/eager-live-source-framework-oracle.ts create mode 100644 packages/trailbase-db-collection/tests/eager-live-source-oracle-suite.ts create mode 100644 packages/trailbase-db-collection/tests/lifecycle-hydration-oracle-suite.ts create mode 100644 packages/trailbase-db-collection/tests/lifecycle-judgment-oracle.ts diff --git a/.changeset/fix-persisted-query-startup.md b/.changeset/fix-persisted-query-startup.md index c39175f6e1..facc2a51c0 100644 --- a/.changeset/fix-persisted-query-startup.md +++ b/.changeset/fix-persisted-query-startup.md @@ -5,4 +5,4 @@ '@tanstack/trailbase-db-collection': patch --- -Remove obsolete persisted rows when an eager source's first complete result overlaps SQLite restore. Query Collection also reconciles ownerless saved rows and returned values from initial data and cached results while preserving peer ownership, including when an optimistic transaction holds publication. RxDB and PowerSync reject incomplete persistence capabilities. TrailBase accepts a sync boundary without the optional metadata API. +Remove obsolete persisted rows when an eager source's first complete result overlaps SQLite restore. Query Collection also reconciles ownerless saved rows and returned values from initial data, cached results and direct writes while preserving peer ownership, including when an optimistic transaction holds publication. PowerSync preserves its startup batch commits; RxDB preserves its existing source commit. Both delete only restored omissions and retain atomic reload snapshots. RxDB and PowerSync reject incomplete persistence capabilities. TrailBase accepts a sync boundary without the optional metadata API. diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 268a8a8962..6d63823083 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - group: [browser, desktop, cloud-mobile] + group: [browser, adapter-browser, desktop, cloud-mobile] runs-on: ubuntu-latest timeout-minutes: 35 steps: @@ -146,6 +146,13 @@ jobs: cd packages/browser-db-sqlite-persistence pnpm test:opfs-fairness + - name: Run adapter oracle browser receivers + if: matrix.group == 'adapter-browser' + working-directory: packages/browser-db-sqlite-persistence + run: | + pnpm exec playwright install --with-deps chromium + pnpm test:oracles:adapters-browser + - name: Run Browser SQLite packaged consumer E2E tests if: matrix.group == 'browser' # Build browser dependencies already built this package's workspace diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 50ddf58ad7..7337fafefe 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -7829,8 +7829,9 @@ snapshots also check exact ownership at all four snapshot cuts. Startup checks the reload owner's one-full-row-read law before its own durable observation. Query's receiving driver preserves exact fetch-count and pending-fetch premises. The [Chromium OPFS receiver](../../packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts) -runs 116 Query histories and 26 complete-result histories for each of the other -five adapters through the same composition and check on real worker/OPFS. It +runs 116 Query histories, 104 PowerSync histories, 32 RxDB histories and 26 +complete-result histories for each of Convex, Electric and TrailBase through +the same composition and check on real worker/OPFS. It repeats in the same browser profile, with file cleanup and separately enforced reach for network-first result/fallback, empty result/baseline, overlap, omission, mutation, and Query peers/ownerless baselines. Identical normalized histories are @@ -7861,12 +7862,16 @@ Node hosts and OPFS. Per-adapter/host mutants that drop source persistence after fallback fail at `startupDurable`, before a refresh. Fallback checker controls also reject lost saved rows. The one-full-row-read law excludes only the observer's own durable reads. -The new receiving campaigns expose startup violations in RxDB, PowerSync and +The receiving campaigns expose startup violations in RxDB, PowerSync and TrailBase before production edits, on both Node hosts and real OPFS. RxDB and -PowerSync select snapshots for pending persisted baselines; TrailBase hydrates -before its complete eager-list omission sweep. Unwrapped snapshot selection -and TrailBase's event fence remain. The original Query baseline-await repair -stays at its adapter boundary. The old Query-only startup model/runner +PowerSync now keep ordinary first-startup source commits, await hydration, then +delete only restored omissions. PowerSync commits source rows and omission +deletes in batches bounded by `syncBatchSize`; RxDB keeps its existing single +source commit. Both preserve atomic snapshots for retained nonempty reloads. +TrailBase hydrates before its complete eager-list omission sweep and retains +its event fence. Query's reconciliation stays at its adapter boundary while +already available cache results retain immediate materialization. +The old Query-only startup model/runner and OPFS files are retired after receiving parity; Query-specific ownership laws and the unrelated direct-write/failure-fidelity repairs remain. @@ -7887,6 +7892,28 @@ and PowerSync sources supply no remote-replication claim. See [network-first evidence](oracle-reviews/2026-11-10-network-first-startup.md), and [external-review amendment](oracle-reviews/2026-11-10-pr-2394-external-review.md). +The additive startup work law bounds source writes by input rows and deletes +by saved omissions. It rejects snapshot replacement and truncate at first +startup. PowerSync crosses B=1/B=2, source sizes below/at/above B, omissions +above B, and provider IDs transformed by deserialization. RxDB crosses source +pagination and omissions above its read batch. The work comparison follows +the existing public/durable/reopen judgments; correct rows cannot mask excess +work. Both Node campaigns enforce randomized constructed reach. The actual +Chromium receiver uses the same 104/32 histories and comparison. This is no +latency or total-memory threshold. See the +[batching evidence](oracle-reviews/2026-11-10-persisted-startup-batching.md). + +The independent +[hydration lifecycle owner](../../packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle.ts) +holds the adapter's hydration continuation across cleanup, reload and terminal +source failure. RxDB and PowerSync receive its public/durable, error-identity +and resource-release judgments on both Node hosts and actual Chromium OPFS. +An acknowledgment rejected after real application does not erase accepted +work. See the [receiving evidence](oracle-reviews/2026-11-10-persisted-baseline-lifecycle.md). +The primary RxDB, PowerSync and TrailBase eager campaigns now use +`oracleProperty`, preserving their models, states, budgets and prior mutant +kills; see the [conversion record](oracle-reviews/2026-11-10-eager-source-primary-conversions.md). + The [original diff-focused gap CLI audit](oracle-reviews/2026-11-10-startup-gap-cli.md) is historical; the [current audit](oracle-reviews/2026-11-10-startup-gap-current-head.md) records subsequent classification separately from startup conformance. @@ -7968,10 +7995,76 @@ unchanged base run budgets and direct replay. The post-conversion Chromium receiver passes all 226 added histories through the same judgments. The [Query conversion record](oracle-reviews/2026-11-10-query-oracle-property-conversion.md) preserves seven exact source-operator rejections and updated replay coordinates. -Under the current ratchet, the four surviving operators above remain open gaps; -the bounded-equivalence evidence grants no allowance. Legacy ownership-lifecycle -and TrailBase owner conversions remain separate obligations before further -production edits. No whole-PR readiness claim follows from these conversions. +The later [retention supplement](oracle-reviews/2026-11-10-startup-property-gap-retention.md) +keeps the original stable mutant keys and instrument failures separate from +conversion results. It records the primary Query ownership and TrailBase +lifecycle/eager conversions, unchanged campaign budgets, and the no-mutation +reload observation projection. That projection removes only a meaningless +release-order flag when no mutation exists; every mutation-present observation +and raw judgment remains unchanged. + +The [Query startup receivers](oracle-reviews/2026-11-10-query-startup-receiving-gaps.md) +add independent first-result value/ownership and synchronous creation-return +comparisons. Controlled storage, native Node SQLite and actual Chromium/OPFS +receive the same direct-write grammar. It crosses distinct and overlapping +saved/direct keys, independent saved/direct/source values, insert/upsert, and +writes before versus after the first physical fetch starts. A retained distinct +saved baseline plus a direct insert before a later changed source response +exposed a stale public/durable value on all three hosts before the source repair. +The repaired diff uses accepted current rows when that saved baseline has no +entry. The original value defect and the historical ownership mutant M563 fail +at their intended first-result comparisons on every host. Immediate nonpersisted +initialData/cache receivers reject M1468/M1469 at synchronous creation return; +the persisted-startup receivers do not supply that different checkpoint. + +The same direct receiver also validates each raw Query sync-write operation +against the public `SyncConfig.write` input contract before forwarding it. +Its `fetched-row-write` reach observes a row-bearing write after fetch return, +independently of the operation chosen. The exact M1334 field deletion fails +`sync-write` / `declared sync-write operation` on all three hosts; clean same- +history replays pass. The [protocol evidence](oracle-reviews/2026-11-10-query-sync-write-protocol.md) +records the source span, fixed/fresh reach, full clean suites and exact replay. +This added check requires no write count/order or own property and does not +claim validation of all other sync-message fields. + +The [rejected-write cutoff receiver](oracle-reviews/2026-11-10-query-rejected-write-cutoff-gap.md) +closes M1794 through an actual Query stale callback after a rejected missing-key +write, followed by a new Infinity-stale peer acquisition. The independent model +requires the fetched value/owners and one physical request. The exact mutant +makes two requests and fails that comparison on controlled storage, native Node +SQLite and actual Chromium/OPFS. The client timer premise uses jsdom for the +Node wrapper; native persistence and browser host tasks remain real. + +The direct receiver runs 40 histories per fixed/fresh campaign plus twelve +randomized constructions, with fourteen states enforced per receiving path. +The immediate receiver runs 40 plus eight constructions; the cutoff receiver +runs 20 plus nine. Fixed regressions do not receive generated reach credit. +Their reports preserve per-path counts, generated-only missing-state controls, +primary-versus-cleanup diagnostics, source hashes and assertion-mutant evidence. +The full normal Query package command passes 1,623 tests with no type errors. +These finite receivers do not establish arbitrary concurrent writers, failed +I/O, every intermediate publication or a whole-PR readiness verdict. + +Query M1473 and TrailBase M151 remain open anchored optimization-guard gaps. +The former adds redundant cached hydration work; the latter allows obsolete +post-cancellation scanning and begin attempts that downstream fences reject. +Keep both useful guards. Neither a bounded public/durable equivalence result +nor this coverage entry supplies the maintainer allowance required by AGENTS.md. + +The [scope omission and reacquisition receiver](../../packages/query-db-collection/tests/scope-omission-reacquisition-oracle.ts) +checks the ownership owner's next-use law. A complete scoped result can remove +an ownerless direct-write row. A later acquisition can then project another +scope's retained successful cache response under the usual cache rules. +Generated histories cross raw and derived caches, cache completion before or +after the direct write, insert/upsert, omission versus inclusion, source values, +and empty/nonempty scoped results. Controlled storage, Node SQLite and actual +Chromium/OPFS use the same model and public/durable comparisons at deletion and +reacquisition. The stale direct-position mutations M1313/M1315/M1316/M1317 fail +that later public-row comparison on both Node hosts; M1313 also fails on +Chromium. This is additive evidence after the +[changed-callback inventory](oracle-reviews/2026-11-10-query-changed-callback-gap-audit.md), +not a change to that immutable run's 24-kill/11-survivor result. It does not +establish precedence among multiple cache results from before a row's lifetime. ## Adapter conformance: eager live sources (#2118) diff --git a/docs/contributing/oracle-reviews/2026-11-10-eager-source-primary-conversions.md b/docs/contributing/oracle-reviews/2026-11-10-eager-source-primary-conversions.md new file mode 100644 index 0000000000..1ad8a9ca50 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-eager-source-primary-conversions.md @@ -0,0 +1,169 @@ +# PowerSync and RxDB primary eager-source conversions + +Validated immutable head: `777935fd68daa497fdd3c9db533845b710be68a1`, parent +`c4c00b20b6c91549a2b9fe502096d0841520f3fe`. The seven-file conversion was then +integrated into the live PR worktree. Shortened imports received formatting only. +This record covers the conversion with unchanged adapter production, before the +separate startup batching and performance repair. + +## Preserved law and integration + +The shared eager-source owner keeps its existing authority, independent row +Map and sync run phase model. It preserves status, health, rows at readiness, +field values, event folds and every intermediate publication comparison. The new bridge transports +the original violated law, step and expected/actual observations. It does not +derive expected rows from production. + +The integrated files are: + +- `packages/db-adapter-conformance/src/eager-live-source-oracle.ts` +- `packages/db-adapter-conformance/tests/eager-live-source-framework-oracle.ts` +- `packages/db-adapter-conformance/tests/eager-live-source-oracle-suite.ts` +- `packages/powersync-db-collection/tests/eager-live-source-history-oracle.ts` +- `packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts` +- `packages/rxdb-db-collection/tests/eager-live-source-history-oracle.ts` +- `packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts` + +Both existing IDs remain: +`adapter-conformance.eager-live-source.powersync` and +`adapter-conformance.eager-live-source.rxdb`. Each wrapper supplies literal +`runs: 400` and `fixedSeeds: [2118]`: 400 histories per fixed and fresh campaign. +The shared grammar only gains inverse maps to admit constructed RxDB startup histories. +Across both domains and seeds 2118/2367/20261110, all 2400 sampled forward draws +match before and after conversion. + +Five randomized builders enforce the original minimum reach of five histories. A local +WeakMap also binds fresh generated outputs to their required states, because the +pinned framework binds only its seed-derived constructions. All five builders +return fresh arrays. PowerSync retains its twelve-step bound: four required +steps and up to eight generated steps. RxDB retains three startup batches plus +twelve steps: its construction uses nine required steps and up to three generated +steps after the startup batches. Keys, titles, notes, value examples, kind +rotations and suffixes vary with generated inputs. Unsupported wait steps remain +outside both grammars. + +PowerSync retains real local SQLite through its Node SDK, its diff-trigger +propagation cut, per-batch publications, five typed field kinds and existing +15-significant-digit storage exclusion. RxDB retains its controlled RxCollection, +real query matcher/comparator, per-change publications, seven JSON value kinds, +initial buffering and post-ready stream failure. Its existing memory-storage +receiving witness remains. No receiving path or checkpoint is removed. + +## Executed evidence + +- **GREEN:** RxDB 129/129 in its original jsdom host. PowerSync 89/89 in both + Node 24.22.1/forks clean captures, within full 265-test baselines of 181.089s and + 169.686s. Both captures have no setup/unhandled errors or nondeterminism. +- **Fresh campaign:** a separate PowerSync unseeded 400 passes in 62.94s, all 17 + declared states at 213–334 histories. PROPERTY/SEED/PATH/CALIBRATION were removed + from its environment. The retention audit's random campaign was explicitly + reseeded 2367; it is not substituted for this fresh campaign. +- **Reach calibration:** all 5×17 PowerSync and 5×25 RxDB named builder/state + omissions fail `constructed builder reach`. Six grammar controls also pass + reconstruction, fresh identity, variation, actual receiving reach and + unsupported-step exclusion. Removing a required prefix loses its corresponding + failure/restart, buffering, optional-field or typed-path state. +- **Generated-only RED:** each chosen construction is absent from all five + seed-derived histories. Suppressing only its `field-removed` observation, + after the unchanged shared product comparison passes, fails at `reach` with + `constructed builder reach`. Exact clean/omitted replays preserve that + distinction: RxDB seed 2118/path 6; PowerSync seed 2118/path + `6:12:10:18:30:29:38:46:46:46:56:64:64:72:81`. + These are assertion failures, not setup errors. +- **Mutant retention:** all 12/12 original stable operator kills remain, six per + adapter, with zero exclusions/nondeterminism. PowerSync run + `96e1e23c1c70`; RxDB run `5f5866b66af7`. The + [versioned retention record](2026-11-10-primary-eager-conversion-retention.json) + preserves stable keys, commands, source hashes, clean captures and integrity. + The separate adapted semantic control remains covered by Audit A's unchanged + input proof; no duplicate control is credited. +- **Failure fidelity:** wrong public values fail the original `FID-1 (values)` + comparison. Three additional actual-RxDB controls preserve that law/step 0 + ahead of a secondary dispose failure; cleanup-only and setup-only failures + escape instead of becoming product violations. +- **Static checks:** strict TypeScript, scoped typed ESLint and diff checks pass + on the integrated seven files. The required whole-branch command + `pnpm --config.verifyDepsBeforeRun=false lint:changed` also exits 0 without + reports after central removal of the two stale legacy entries. + +Earlier default-runtime attempts are retained as failed runtime evidence: +a combined PowerSync fresh campaign lost its worker to SIGKILL, and a single +clean replay aborted in better-sqlite3's environment-cleanup hook with SIGABRT. +Neither is a product verdict or mutant kill. The same full replay then passes +under the validated Node 24.22.1/forks runtime; its omitted-state replay fails the +intended assertion there. + +## Normal-campaign reach + +Each listed state has enforced minimum 5. Counts below are complete original-host +fixed and fresh campaigns of 400 histories each; explicit one-history replays do +not replace them. + +### PowerSync + +| State | Fixed2118 | Unseeded fresh | +| --- | ---: | ---: | +| `change-batch` | 296 | 294 | +| `failure-before-ready` | 255 | 273 | +| `field-removed` | 196 | 213 | +| `field:boolean:change` | 274 | 271 | +| `field:boolean:load` | 309 | 315 | +| `field:date:change` | 274 | 271 | +| `field:date:load` | 309 | 315 | +| `field:float:change` | 274 | 271 | +| `field:float:load` | 309 | 315 | +| `field:integer:change` | 274 | 271 | +| `field:integer:load` | 309 | 315 | +| `field:string:change` | 274 | 271 | +| `field:string:load` | 309 | 315 | +| `first-ready` | 333 | 334 | +| `multi-change-batch` | 254 | 249 | +| `restart` | 264 | 281 | +| `restart-after-failure` | 213 | 232 | + +### RxDB + +| State | Fixed2118 | Unseeded fresh | +| --- | ---: | ---: | +| `change-batch` | 280 | 291 | +| `change-before-ready` | 373 | 381 | +| `failure-after-ready` | 258 | 272 | +| `failure-before-ready` | 255 | 282 | +| `field-removed` | 198 | 222 | +| `first-ready` | 333 | 338 | +| `multi-change-batch` | 330 | 348 | +| `read-missed-change` | 263 | 266 | +| `restart` | 265 | 298 | +| `restart-after-failure` | 226 | 251 | +| `seen-change-superseded` | 203 | 233 | +| `value:array:change` | 246 | 272 | +| `value:array:load` | 235 | 250 | +| `value:boolean:change` | 253 | 267 | +| `value:boolean:load` | 233 | 255 | +| `value:float:change` | 234 | 265 | +| `value:float:load` | 212 | 252 | +| `value:integer:change` | 270 | 288 | +| `value:integer:load` | 241 | 268 | +| `value:null:change` | 254 | 272 | +| `value:null:load` | 232 | 251 | +| `value:object:change` | 246 | 265 | +| `value:object:load` | 233 | 249 | +| `value:string:change` | 248 | 271 | +| `value:string:load` | 236 | 249 | + +## Bounds and review outcome + +The product model and its vocabulary are unchanged (ORC001–003/008–009). +Grammar controls, real receiving execution, value/reach calibrations and direct +replay cover ORC004–007. Failure fidelity covers ORC010. No new shared semantic +fault or product threshold is proposed, so ORC011/013 add no new obligation. +This versioned record and the immutable retention record supply ORC012 evidence. +Original receiving boundaries remain (ORC014); constructed and generated-output +checks enforce ORC015; campaigns and calibrations share the original judgment +without separate expected row constants (ORC016). + +These are eager, unwrapped source receivers. They do not cover persistence +hydration-await lifecycle windows, remote replication, mutation handlers, +on-demand loading or elapsed/heap performance. The separate persisted-startup, +lifecycle and measurement owners supply those claims. This conversion does not +claim closure of later production changes. diff --git a/docs/contributing/oracle-reviews/2026-11-10-final-adapter-changed-code-gap-audit.json b/docs/contributing/oracle-reviews/2026-11-10-final-adapter-changed-code-gap-audit.json new file mode 100644 index 0000000000..a8905df6d3 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-final-adapter-changed-code-gap-audit.json @@ -0,0 +1,24141 @@ +{ + "scope": "Bounded supplementary final PowerSync/RxDB changed-code discovery; intentionally stopped, not universal closure.", + "frozenSnapshot": "/private/tmp/tanstack-db-2394-final-adapter-gap", + "head": "0e86c1fdae85520aff142f141f71ccf46a8ee8bd", + "instrument": "Isolated e1fb CLI with expected-failure, capture-completeness and full-title identity repairs; installed CLI unchanged.", + "diagnosticBudgets": "Frozen transform: runs2,seed2367,fixed histories0,calibrations0; canonical constructed histories/models/drivers/checks unchanged; repository budgets unchanged.", + "counts": { + "automaticSelected": 110, + "automaticCompleted": 52, + "automaticKills": 22, + "automaticSurvivors": 18, + "automaticCrash": 1, + "automaticTimeouts": 10, + "automaticUnreached": 1, + "automaticUnrunOrInterrupted": 58, + "authoredPilotKills": 8, + "separateLiveIteratorSurvivors": 2 + }, + "groups": [ + { + "ids": [ + "powersync.ts:M41", + "rxdb.ts:M6", + "rxdb.ts:M11", + "rxdb.ts:M12" + ], + "effect": "Change the retained-reload snapshot predicate or option.", + "risk": "Atomic publication and retained replacement may differ before the final cut. M12 changes captured observations; the other three show no captured difference. No production defect is established.", + "needed": "Generate and observe the intermediate retained-reload publication boundary, or obtain an exact maintainer allowance." + }, + { + "ids": [ + "powersync.ts:M79" + ], + "effect": "Replace the required sync-write operation type with undefined.", + "risk": "The public write protocol is not enforced for this PowerSync boundary by these two selected owners; rows alone do not distinguish it.", + "needed": "Add shared receiving validation of required operation type, or obtain an exact maintainer allowance." + }, + { + "ids": [ + "rxdb.ts:M8", + "rxdb.ts:M65", + "rxdb.ts:M67" + ], + "effect": "Broaden source-key collection or the baseline-reconciliation guard.", + "risk": "Extra collection/reconciliation can occur in retained or unwrapped runs; this persisted-startup campaign observes no difference. It does not prove equivalent behavior.", + "needed": "Distinguish retained/unwrapped source work and reconcile boundaries, or obtain an exact maintainer allowance." + }, + { + "ids": [ + "rxdb.ts:M44", + "rxdb.ts:M45" + ], + "effect": "Remove the stop fence after an awaited source read.", + "risk": "The true stopped branch of this specific fence is not reached by the selected histories. No evidence establishes what happens on source-read completion after retirement.", + "needed": "Generate retirement while the initial RxDB storage read is held, then release it, or obtain an exact maintainer allowance." + }, + { + "ids": [ + "rxdb.ts:M60", + "rxdb.ts:M63", + "rxdb.ts:M86", + "rxdb.ts:M89" + ], + "effect": "Always attach the catch handler, including a hypothetical synchronous true receipt.", + "risk": "The differentiating synchronous-receipt branch is not reached. A true receipt would not have catch; this campaign does not establish whether that receipt is legal at this boundary.", + "needed": "Exercise the legal synchronous receipt path or document its inapplicability with an exact maintainer allowance." + }, + { + "ids": [ + "rxdb.ts:M87", + "rxdb.ts:M88", + "rxdb.ts:M90" + ], + "effect": "Omit the immediate catch handler for an omission-commit receipt.", + "risk": "The selected histories do not distinguish rejected omission receipts and their reporting timing. This is a generator gap, not a demonstrated production error.", + "needed": "Exercise omission-receipt rejection at the relevant await/reporting boundary, or obtain an exact maintainer allowance." + }, + { + "ids": [ + "rxdb.ts:M79" + ], + "effect": "Run one additional empty omission transaction at the end of the loop.", + "risk": "Captured work differs but existing comparisons permit it. Whether this extra empty transaction violates an accepted work law requires an explicit authority decision.", + "needed": "Add a justified work comparison, or obtain an exact maintainer allowance." + } + ], + "adapters": { + "powersync": { + "sourceSha256": "c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068", + "fullStaticInventoryCount": 569, + "selectedCount": 51, + "completedCount": 4, + "counts": { + "crashed": 1, + "survived": 2, + "timeout": 1 + }, + "unrunOrInterruptedCount": 47, + "selectionBasis": "Every automatic operator overlapping added/replaced executable lines in the frozen batching repair; unrelated unchanged loader/lifecycle source operations are inventoried and omitted from this additive selection. Parent-approved bounded contract-driven scope, not a universal gap-free claim.", + "addedRanges": [ + { + "lineStart": 355, + "lineEnd": 359 + }, + { + "lineStart": 444, + "lineEnd": 445 + }, + { + "lineStart": 456, + "lineEnd": 464 + }, + { + "lineStart": 474, + "lineEnd": 475 + }, + { + "lineStart": 482, + "lineEnd": 485 + }, + { + "lineStart": 487, + "lineEnd": 489 + }, + { + "lineStart": 505, + "lineEnd": 505 + }, + { + "lineStart": 652, + "lineEnd": 653 + }, + { + "lineStart": 656, + "lineEnd": 657 + }, + { + "lineStart": 659, + "lineEnd": 694 + }, + { + "lineStart": 700, + "lineEnd": 704 + } + ], + "fullStaticInventory": [ + { + "id": "powersync.ts:M1", + "number": 1, + "source": "src/powersync.ts", + "line": 44, + "position": 1465, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )" + }, + { + "id": "powersync.ts:M2", + "number": 2, + "source": "src/powersync.ts", + "line": 43, + "position": 1399, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~2" + }, + { + "id": "powersync.ts:M3", + "number": 3, + "source": "src/powersync.ts", + "line": 43, + "position": 1391, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:drop-not:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )" + }, + { + "id": "powersync.ts:M4", + "number": 4, + "source": "src/powersync.ts", + "line": 43, + "position": 1392, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:drop-not:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~2" + }, + { + "id": "powersync.ts:M5", + "number": 5, + "source": "src/powersync.ts", + "line": 44, + "position": 1433, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~3" + }, + { + "id": "powersync.ts:M6", + "number": 6, + "source": "src/powersync.ts", + "line": 44, + "position": 1420, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~4" + }, + { + "id": "powersync.ts:M7", + "number": 7, + "source": "src/powersync.ts", + "line": 44, + "position": 1449, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~5" + }, + { + "id": "powersync.ts:M8", + "number": 8, + "source": "src/powersync.ts", + "line": 45, + "position": 1514, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~6" + }, + { + "id": "powersync.ts:M9", + "number": 9, + "source": "src/powersync.ts", + "line": 243, + "position": 7926, + "operator": "value-undefined", + "original": "createPowerSyncCollectionConfig(config)", + "replacement": "undefined", + "anchor": "const outputConfig = createPowerSyncCollectionConfig ( config )", + "function": "powerSyncCollectionOptions", + "key": "src/powersync.ts#powerSyncCollectionOptions:value-undefined:const outputConfig = createPowerSyncCollectionConfig ( config )" + }, + { + "id": "powersync.ts:M10", + "number": 10, + "source": "src/powersync.ts", + "line": 244, + "position": 7975, + "operator": "value-undefined", + "original": "withCollectionConfigFactory(outputConfig, () =>\n createPowerSyncCollectionConfig(config),\n )", + "replacement": "undefined", + "anchor": "return withCollectionConfigFactory ( outputConfig , ( ) => createPowerSyncCollectionConfig ( config ) , )", + "function": "powerSyncCollectionOptions", + "key": "src/powersync.ts#powerSyncCollectionOptions:value-undefined:return withCollectionConfigFactory ( outputConfig , ( ) => createPowerSyncCollectionConfig ( config ) , )" + }, + { + "id": "powersync.ts:M11", + "number": 11, + "source": "src/powersync.ts", + "line": 245, + "position": 8027, + "operator": "value-undefined", + "original": "createPowerSyncCollectionConfig(config)", + "replacement": "undefined", + "anchor": "return withCollectionConfigFactory ( outputConfig , ( ) => createPowerSyncCollectionConfig ( config ) , )", + "function": "powerSyncCollectionOptions", + "key": "src/powersync.ts#powerSyncCollectionOptions:value-undefined:return withCollectionConfigFactory ( outputConfig , ( ) => createPowerSyncCollectionConfig ( config ) , )~2" + }, + { + "id": "powersync.ts:M12", + "number": 12, + "source": "src/powersync.ts", + "line": 282, + "position": 9125, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "const validationSchema = deserializationSchema || schema", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:binary-swap:const validationSchema = deserializationSchema || schema" + }, + { + "id": "powersync.ts:M13", + "number": 13, + "source": "src/powersync.ts", + "line": 283, + "position": 9158, + "operator": "value-undefined", + "original": "validationSchema[`~standard`].validate(value)", + "replacement": "undefined", + "anchor": "const validation = validationSchema [ `~standard` ] . validate ( value )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:value-undefined:const validation = validationSchema [ `~standard` ] . validate ( value )" + }, + { + "id": "powersync.ts:M14", + "number": 14, + "source": "src/powersync.ts", + "line": 284, + "position": 9212, + "operator": "if-true", + "original": "`value` in validation", + "replacement": "true", + "anchor": "if ( `value` in validation ) { return validation . value } else if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:if-true:if ( `value` in validation ) { return validation . value } else if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }" + }, + { + "id": "powersync.ts:M15", + "number": 15, + "source": "src/powersync.ts", + "line": 284, + "position": 9212, + "operator": "if-false", + "original": "`value` in validation", + "replacement": "false", + "anchor": "if ( `value` in validation ) { return validation . value } else if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:if-false:if ( `value` in validation ) { return validation . value } else if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }" + }, + { + "id": "powersync.ts:M16", + "number": 16, + "source": "src/powersync.ts", + "line": 286, + "position": 9282, + "operator": "if-true", + "original": "`issues` in validation", + "replacement": "true", + "anchor": "if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:if-true:if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }" + }, + { + "id": "powersync.ts:M17", + "number": 17, + "source": "src/powersync.ts", + "line": 286, + "position": 9282, + "operator": "if-false", + "original": "`issues` in validation", + "replacement": "false", + "anchor": "if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:if-false:if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }" + }, + { + "id": "powersync.ts:M18", + "number": 18, + "source": "src/powersync.ts", + "line": 287, + "position": 9396, + "operator": "value-undefined", + "original": "validation.issues.map((issue) => `${issue.path} - ${issue.message}`)", + "replacement": "undefined", + "anchor": "const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}`", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:value-undefined:const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}`" + }, + { + "id": "powersync.ts:M19", + "number": 19, + "source": "src/powersync.ts", + "line": 288, + "position": 9473, + "operator": "delete-call", + "original": "database.logger.log({ level: LogLevels.error, message: issueMessage })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : issueMessage } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:delete-call:database . logger . log ( { level : LogLevels . error , message : issueMessage } )" + }, + { + "id": "powersync.ts:M20", + "number": 20, + "source": "src/powersync.ts", + "line": 288, + "position": 9495, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : issueMessage } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:database . logger . log ( { level : LogLevels . error , message : issueMessage } )" + }, + { + "id": "powersync.ts:M21", + "number": 21, + "source": "src/powersync.ts", + "line": 288, + "position": 9519, + "operator": "field-drop", + "original": "message: issueMessage", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : issueMessage } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:database . logger . log ( { level : LogLevels . error , message : issueMessage } )~2" + }, + { + "id": "powersync.ts:M22", + "number": 22, + "source": "src/powersync.ts", + "line": 289, + "position": 9550, + "operator": "delete-call", + "original": "onDeserializationError!(validation)", + "replacement": ";", + "anchor": "onDeserializationError ! ( validation )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:delete-call:onDeserializationError ! ( validation )" + }, + { + "id": "powersync.ts:M23", + "number": 23, + "source": "src/powersync.ts", + "line": 293, + "position": 9723, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:delete-call:database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )" + }, + { + "id": "powersync.ts:M24", + "number": 24, + "source": "src/powersync.ts", + "line": 294, + "position": 9753, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )" + }, + { + "id": "powersync.ts:M25", + "number": 25, + "source": "src/powersync.ts", + "line": 295, + "position": 9785, + "operator": "field-drop", + "original": "message: unknownErrorMessage", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )~2" + }, + { + "id": "powersync.ts:M26", + "number": 26, + "source": "src/powersync.ts", + "line": 297, + "position": 9830, + "operator": "delete-call", + "original": "onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })", + "replacement": ";", + "anchor": "onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:delete-call:onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )" + }, + { + "id": "powersync.ts:M27", + "number": 27, + "source": "src/powersync.ts", + "line": 297, + "position": 9856, + "operator": "field-drop", + "original": "issues: [{ message: unknownErrorMessage }]", + "replacement": "", + "anchor": "onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )" + }, + { + "id": "powersync.ts:M28", + "number": 28, + "source": "src/powersync.ts", + "line": 297, + "position": 9867, + "operator": "field-drop", + "original": "message: unknownErrorMessage", + "replacement": "", + "anchor": "onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )~2" + }, + { + "id": "powersync.ts:M29", + "number": 29, + "source": "src/powersync.ts", + "line": 303, + "position": 10079, + "operator": "binary-swap", + "original": "??", + "replacement": "&&", + "anchor": "const schema = inputSchema ?? ( convertTableToSchema ( table ) as TSchema )", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:binary-swap:const schema = inputSchema ?? ( convertTableToSchema ( table ) as TSchema )" + }, + { + "id": "powersync.ts:M30", + "number": 30, + "source": "src/powersync.ts", + "line": 303, + "position": 10083, + "operator": "value-undefined", + "original": "convertTableToSchema(table)", + "replacement": "undefined", + "anchor": "const schema = inputSchema ?? ( convertTableToSchema ( table ) as TSchema )", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:value-undefined:const schema = inputSchema ?? ( convertTableToSchema ( table ) as TSchema )" + }, + { + "id": "powersync.ts:M31", + "number": 31, + "source": "src/powersync.ts", + "line": 316, + "position": 10762, + "operator": "value-undefined", + "original": "Math.floor(\n Math.random() * 0xffffffff,\n )\n .toString(16)\n .padStart(8, `0`)", + "replacement": "undefined", + "anchor": "const trackedTableName = `__${ viewName } _tracking_$ { Math . floor ( Math . random ( ) * 0xffffffff , ) . toString ( 16 ) . padStart ( 8 , `0` ) } `", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:value-undefined:const trackedTableName = `__${ viewName } _tracking_$ { Math . floor ( Math . random ( ) * 0xffffffff , ) . toString ( 16 ) . padStart ( 8 , `0` ) } `" + }, + { + "id": "powersync.ts:M32", + "number": 32, + "source": "src/powersync.ts", + "line": 317, + "position": 10778, + "operator": "value-undefined", + "original": "Math.random()", + "replacement": "undefined", + "anchor": "const trackedTableName = `__${ viewName } _tracking_$ { Math . floor ( Math . random ( ) * 0xffffffff , ) . toString ( 16 ) . padStart ( 8 , `0` ) } `", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:value-undefined:const trackedTableName = `__${ viewName } _tracking_$ { Math . floor ( Math . random ( ) * 0xffffffff , ) . toString ( 16 ) . padStart ( 8 , `0` ) } `~2" + }, + { + "id": "powersync.ts:M33", + "number": 33, + "source": "src/powersync.ts", + "line": 323, + "position": 10904, + "operator": "field-drop", + "original": "database", + "replacement": "", + "anchor": "const transactor = new PowerSyncTransactor ( { database , } )", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const transactor = new PowerSyncTransactor ( { database , } )" + }, + { + "id": "powersync.ts:M34", + "number": 34, + "source": "src/powersync.ts", + "line": 332, + "position": 11116, + "operator": "field-drop", + "original": "sync: (params) => {\n const persistence =\n params.metadata === undefined\n ? null\n : validateSyncPersistenceCapability(params.metadata.persistence)\n const {\n begin,\n write,\n collection,\n commit,\n fail,\n reportError,\n reportHealthy,\n } = params\n // Readiness of this sync run: a reload runs `sync` again while the\n // Collection still shows the previous run's status.\n let ready = false\n const markReady = () => {\n ready = true\n params.markReady()\n }\n const abortController = new AbortController()\n\n // Persisted first startup keeps ordinary batches even after timeout\n // fallback. A retained reload keeps its atomic replacement.\n const replacesRetainedRows =\n collection.health.reloading &&\n !collection._state.acceptedSyncedEntries().next().done\n let disposeTracking:\n ((options?: { context?: LockContext }) => Promise) | null = null\n let trackingSetup: Promise | null = null\n let activeTrackingDisposal: Promise | null = null\n let abortTrackingDisposal: Promise | null = null\n\n if (syncMode === `eager`) {\n return runEagerSync()\n } else {\n return runOnDemandSync()\n }\n\n /**\n * Disposes the current diff trigger, if one is active, and clears the\n * tracking state.\n */\n async function safelyDisposeTracking(\n context?: LockContext,\n ): Promise {\n // Cleanup can race trigger creation. Wait until the disposer has been\n // published so an abort cannot strand a freshly-created trigger.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n\n const dispose = disposeTracking\n if (!dispose) {\n // Reconciliation may have claimed the disposer after this caller\n // observed setup. Join that invocation instead of settling early.\n await activeTrackingDisposal\n return\n }\n\n disposeTracking = null\n let disposal: Promise\n try {\n disposal = Promise.resolve(dispose(context ? { context } : undefined))\n } catch (error) {\n disposal = Promise.reject(error)\n }\n activeTrackingDisposal = disposal\n try {\n await disposal\n } finally {\n if (activeTrackingDisposal === disposal) {\n activeTrackingDisposal = null\n }\n }\n }\n\n function disposeTrackingAfterAbort(): Promise {\n const disposal = (abortTrackingDisposal ??= safelyDisposeTracking())\n // Abort event dispatch does not observe a listener's returned promise.\n // The eager cleanup callback awaits this same promise below.\n void disposal.catch(() => undefined)\n return disposal\n }\n\n async function establishTracking(\n options: Parameters[0],\n appliedReceipts: Array,\n ): Promise {\n const setup = (async () => {\n const dispose = await createDiffTrigger(options, appliedReceipts)\n disposeTracking = dispose\n })()\n trackingSetup = setup\n\n try {\n await setup\n } finally {\n if (trackingSetup === setup) {\n trackingSetup = null\n }\n }\n }\n\n async function createDiffTrigger(\n options: {\n setupContext?: LockContext\n // The load reads every row and replaces the rows the Collection\n // holds.\n snapshot?: boolean\n afterRead?: () => Promise\n onInitialRow?: (value: OutputType) => void\n when: Record\n writeType: (rowId: string) => OperationType\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) => Promise>\n },\n appliedReceipts: Array,\n ) {\n const {\n setupContext,\n snapshot,\n afterRead,\n onInitialRow,\n when,\n writeType,\n batchQuery,\n } = options\n\n return await database.triggers.createDiffTrigger({\n source: viewName,\n destination: trackedTableName,\n columns: table.columns.map((column) => column.name),\n setupContext,\n when,\n hooks: {\n beforeCreate: async (context) => {\n // A retained reload uses one snapshot across its source reads.\n // Other initial loads and subset loads keep batch commits. A\n // transaction begins only once its rows are read, so a batch\n // that fails leaves none open.\n const batches: Array> = []\n const apply = () => {\n begin({ snapshot })\n for (const row of batches.splice(0).flat()) {\n const type = writeType(row.id)\n const value = deserializeSyncRow(row)\n onInitialRow?.(value)\n write({ type, value })\n }\n const applied = commit()\n if (applied !== true) void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n batches.push(batchItems)\n if (!snapshot) apply()\n }\n if (snapshot) apply()\n if (afterRead) await afterRead()\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n `SELECT * FROM ${trackedTableName} ORDER BY operation_id ASC`,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(`DELETE FROM ${trackedTableName}`)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is starting for ${viewName} into ${trackedTableName}`,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n `abort`,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: `delete`, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === `rejected` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n `PowerSync tracking disposal and load-hook cleanup both failed`,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === `rejected`) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: `delete`, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: `Could not clean up subset hook for ${viewName}`,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, `PowerSync cleanup failed`, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: `delete`, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: `Could not release subset tracking for ${viewName}; retrying`,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === `rejected`) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === `rejected`\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n }", + "replacement": "", + "anchor": "const sync : SyncConfig < OutputType , string > = { sync : ( params ) => { const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const { begin , write , collection , commit , fail , reportError , reportHealthy , } = params let ready = false const markReady = ( ) => { ready = true params . markReady ( ) } const abortController = new AbortController ( ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let disposeTracking : ( ( options ? : { context ? : LockContext } ) => Promise < void > ) | null = null let trackingSetup : Promise < void > | null = null let activeTrackingDisposal : Promise < void > | null = null let abortTrackingDisposal : Promise < void > | null = null if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) } async function safelyDisposeTracking ( context ? : LockContext , ) : Promise < void > { const setup = trackingSetup if ( setup ) { await setup . catch ( ( ) => undefined ) } const dispose = disposeTracking if ( ! dispose ) { await activeTrackingDisposal return } disposeTracking = null let disposal : Promise < void > try { disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) ) } catch ( error ) { disposal = Promise . reject ( error ) } activeTrackingDisposal = disposal try { await disposal } finally { if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null } } } function disposeTrackingAfterAbort ( ) : Promise < void > { const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) ) void disposal . catch ( ( ) => undefined ) return disposal } async function establishTracking ( options : Parameters < typeof createDiffTrigger > [ 0 ] , appliedReceipts : Array < SyncAppliedReceipt > , ) : Promise < void > { const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( ) trackingSetup = setup try { await setup } finally { if ( trackingSetup === setup ) { trackingSetup = null } } } async function createDiffTrigger ( options : { setupContext ? : LockContext snapshot ? : boolean afterRead ? : ( ) => Promise < void > onInitialRow ? : ( value : OutputType ) => void when : Record < DiffTriggerOperation , string > writeType : ( rowId : string ) => OperationType batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => Promise < Array < TableType >> } , appliedReceipts : Array < SyncAppliedReceipt > , ) { const { setupContext , snapshot , afterRead , onInitialRow , when , writeType , batchQuery , } = options return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n ` SELECT * FROM $ { trackedTableName } ORDER BY operation_id ASC `,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(` DELETE FROM $ { trackedTableName } `)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync is starting for $ { viewName } into $ { trackedTableName } `,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n ` abort `,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: ` delete `, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: ` TRUE `,\n [DiffTriggerOperation.UPDATE]: ` TRUE `,\n [DiffTriggerOperation.DELETE]: ` TRUE `,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL` SELECT * FROM $ { viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === ` rejected ` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n ` PowerSync tracking disposal and load - hook cleanup both failed `,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === ` rejected `) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: ` ( $ { newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not clean up subset hook for $ { viewName } `,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, ` PowerSync cleanup failed `, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: ` delete `, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not release subset tracking for $ { viewName } ; retrying `,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }", + "function": "sync", + "key": "src/powersync.ts#sync:field-drop:const sync : SyncConfig < OutputType , string > = { sync : ( params ) => { const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const { begin , write , collection , commit , fail , reportError , reportHealthy , } = params let ready = false const markReady = ( ) => { ready = true params . markReady ( ) } const abortController = new AbortController ( ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let disposeTracking : ( ( options ? : { context ? : LockContext } ) => Promise < void > ) | null = null let trackingSetup : Promise < void > | null = null let activeTrackingDisposal : Promise < void > | null = null let abortTrackingDisposal : Promise < void > | null = null if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) } async function safelyDisposeTracking ( context ? : LockContext , ) : Promise < void > { const setup = trackingSetup if ( setup ) { await setup . catch ( ( ) => undefined ) } const dispose = disposeTracking if ( ! dispose ) { await activeTrackingDisposal return } disposeTracking = null let disposal : Promise < void > try { disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) ) } catch ( error ) { disposal = Promise . reject ( error ) } activeTrackingDisposal = disposal try { await disposal } finally { if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null } } } function disposeTrackingAfterAbort ( ) : Promise < void > { const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) ) void disposal . catch ( ( ) => undefined ) return disposal } async function establishTracking ( options : Parameters < typeof createDiffTrigger > [ 0 ] , appliedReceipts : Array < SyncAppliedReceipt > , ) : Promise < void > { const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( ) trackingSetup = setup try { await setup } finally { if ( trackingSetup === setup ) { trackingSetup = null } } } async function createDiffTrigger ( options : { setupContext ? : LockContext snapshot ? : boolean afterRead ? : ( ) => Promise < void > onInitialRow ? : ( value : OutputType ) => void when : Record < DiffTriggerOperation , string > writeType : ( rowId : string ) => OperationType batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => Promise < Array < TableType >> } , appliedReceipts : Array < SyncAppliedReceipt > , ) { const { setupContext , snapshot , afterRead , onInitialRow , when , writeType , batchQuery , } = options return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n ` SELECT * FROM $ { trackedTableName } ORDER BY operation_id ASC `,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(` DELETE FROM $ { trackedTableName } `)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync is starting for $ { viewName } into $ { trackedTableName } `,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n ` abort `,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: ` delete `, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: ` TRUE `,\n [DiffTriggerOperation.UPDATE]: ` TRUE `,\n [DiffTriggerOperation.DELETE]: ` TRUE `,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL` SELECT * FROM $ { viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === ` rejected ` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n ` PowerSync tracking disposal and load - hook cleanup both failed `,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === ` rejected `) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: ` ( $ { newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not clean up subset hook for $ { viewName } `,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, ` PowerSync cleanup failed `, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: ` delete `, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not release subset tracking for $ { viewName } ; retrying `,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }" + }, + { + "id": "powersync.ts:M35", + "number": 35, + "source": "src/powersync.ts", + "line": 334, + "position": 11186, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "function": "sync", + "key": "src/powersync.ts#sync:binary-swap:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )" + }, + { + "id": "powersync.ts:M36", + "number": 36, + "source": "src/powersync.ts", + "line": 336, + "position": 11229, + "operator": "value-undefined", + "original": "validateSyncPersistenceCapability(params.metadata.persistence)", + "replacement": "undefined", + "anchor": "const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "function": "sync", + "key": "src/powersync.ts#sync:value-undefined:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )" + }, + { + "id": "powersync.ts:M37", + "number": 37, + "source": "src/powersync.ts", + "line": 348, + "position": 11598, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let ready = false", + "function": "sync", + "key": "src/powersync.ts#sync:bool-flip:let ready = false" + }, + { + "id": "powersync.ts:M38", + "number": 38, + "source": "src/powersync.ts", + "line": 350, + "position": 11652, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "ready = true", + "function": "markReady", + "key": "src/powersync.ts#markReady:bool-flip:ready = true" + }, + { + "id": "powersync.ts:M39", + "number": 39, + "source": "src/powersync.ts", + "line": 351, + "position": 11665, + "operator": "delete-call", + "original": "params.markReady()", + "replacement": ";", + "anchor": "params . markReady ( )", + "function": "markReady", + "key": "src/powersync.ts#markReady:delete-call:params . markReady ( )" + }, + { + "id": "powersync.ts:M40", + "number": 40, + "source": "src/powersync.ts", + "line": 358, + "position": 11958, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "key": "src/powersync.ts#sync:binary-swap:const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done" + }, + { + "id": "powersync.ts:M41", + "number": 41, + "source": "src/powersync.ts", + "line": 359, + "position": 11969, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "key": "src/powersync.ts#sync:drop-not:const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done" + }, + { + "id": "powersync.ts:M42", + "number": 42, + "source": "src/powersync.ts", + "line": 366, + "position": 12317, + "operator": "if-true", + "original": "syncMode === `eager`", + "replacement": "true", + "anchor": "if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }", + "function": "sync", + "key": "src/powersync.ts#sync:if-true:if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }" + }, + { + "id": "powersync.ts:M43", + "number": 43, + "source": "src/powersync.ts", + "line": 366, + "position": 12317, + "operator": "if-false", + "original": "syncMode === `eager`", + "replacement": "false", + "anchor": "if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }", + "function": "sync", + "key": "src/powersync.ts#sync:if-false:if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }" + }, + { + "id": "powersync.ts:M44", + "number": 44, + "source": "src/powersync.ts", + "line": 366, + "position": 12326, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }", + "function": "sync", + "key": "src/powersync.ts#sync:binary-swap:if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }" + }, + { + "id": "powersync.ts:M45", + "number": 45, + "source": "src/powersync.ts", + "line": 367, + "position": 12356, + "operator": "value-undefined", + "original": "runEagerSync()", + "replacement": "undefined", + "anchor": "return runEagerSync ( )", + "function": "sync", + "key": "src/powersync.ts#sync:value-undefined:return runEagerSync ( )" + }, + { + "id": "powersync.ts:M46", + "number": 46, + "source": "src/powersync.ts", + "line": 369, + "position": 12401, + "operator": "value-undefined", + "original": "runOnDemandSync()", + "replacement": "undefined", + "anchor": "return runOnDemandSync ( )", + "function": "sync", + "key": "src/powersync.ts#sync:value-undefined:return runOnDemandSync ( )" + }, + { + "id": "powersync.ts:M47", + "number": 47, + "source": "src/powersync.ts", + "line": 382, + "position": 12851, + "operator": "if-true", + "original": "setup", + "replacement": "true", + "anchor": "if ( setup ) { await setup . catch ( ( ) => undefined ) }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-true:if ( setup ) { await setup . catch ( ( ) => undefined ) }" + }, + { + "id": "powersync.ts:M48", + "number": 48, + "source": "src/powersync.ts", + "line": 382, + "position": 12851, + "operator": "if-false", + "original": "setup", + "replacement": "false", + "anchor": "if ( setup ) { await setup . catch ( ( ) => undefined ) }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-false:if ( setup ) { await setup . catch ( ( ) => undefined ) }" + }, + { + "id": "powersync.ts:M49", + "number": 49, + "source": "src/powersync.ts", + "line": 383, + "position": 12876, + "operator": "value-undefined", + "original": "setup.catch(() => undefined)", + "replacement": "undefined", + "anchor": "await setup . catch ( ( ) => undefined )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:value-undefined:await setup . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M50", + "number": 50, + "source": "src/powersync.ts", + "line": 387, + "position": 12968, + "operator": "if-true", + "original": "!dispose", + "replacement": "true", + "anchor": "if ( ! dispose ) { await activeTrackingDisposal return }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-true:if ( ! dispose ) { await activeTrackingDisposal return }" + }, + { + "id": "powersync.ts:M51", + "number": 51, + "source": "src/powersync.ts", + "line": 387, + "position": 12968, + "operator": "if-false", + "original": "!dispose", + "replacement": "false", + "anchor": "if ( ! dispose ) { await activeTrackingDisposal return }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-false:if ( ! dispose ) { await activeTrackingDisposal return }" + }, + { + "id": "powersync.ts:M52", + "number": 52, + "source": "src/powersync.ts", + "line": 387, + "position": 12968, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! dispose ) { await activeTrackingDisposal return }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:drop-not:if ( ! dispose ) { await activeTrackingDisposal return }" + }, + { + "id": "powersync.ts:M53", + "number": 53, + "source": "src/powersync.ts", + "line": 397, + "position": 13302, + "operator": "value-undefined", + "original": "Promise.resolve(dispose(context ? { context } : undefined))", + "replacement": "undefined", + "anchor": "disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:value-undefined:disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )" + }, + { + "id": "powersync.ts:M54", + "number": 54, + "source": "src/powersync.ts", + "line": 397, + "position": 13318, + "operator": "value-undefined", + "original": "dispose(context ? { context } : undefined)", + "replacement": "undefined", + "anchor": "disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:value-undefined:disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )~2" + }, + { + "id": "powersync.ts:M55", + "number": 55, + "source": "src/powersync.ts", + "line": 397, + "position": 13338, + "operator": "field-drop", + "original": "context", + "replacement": "", + "anchor": "disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:field-drop:disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )" + }, + { + "id": "powersync.ts:M56", + "number": 56, + "source": "src/powersync.ts", + "line": 399, + "position": 13409, + "operator": "value-undefined", + "original": "Promise.reject(error)", + "replacement": "undefined", + "anchor": "disposal = Promise . reject ( error )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:value-undefined:disposal = Promise . reject ( error )" + }, + { + "id": "powersync.ts:M57", + "number": 57, + "source": "src/powersync.ts", + "line": 405, + "position": 13556, + "operator": "if-true", + "original": "activeTrackingDisposal === disposal", + "replacement": "true", + "anchor": "if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-true:if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }" + }, + { + "id": "powersync.ts:M58", + "number": 58, + "source": "src/powersync.ts", + "line": 405, + "position": 13556, + "operator": "if-false", + "original": "activeTrackingDisposal === disposal", + "replacement": "false", + "anchor": "if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-false:if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }" + }, + { + "id": "powersync.ts:M59", + "number": 59, + "source": "src/powersync.ts", + "line": 405, + "position": 13579, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:binary-swap:if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }" + }, + { + "id": "powersync.ts:M60", + "number": 60, + "source": "src/powersync.ts", + "line": 412, + "position": 13780, + "operator": "value-undefined", + "original": "safelyDisposeTracking()", + "replacement": "undefined", + "anchor": "const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) )", + "function": "disposeTrackingAfterAbort", + "key": "src/powersync.ts#disposeTrackingAfterAbort:value-undefined:const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) )" + }, + { + "id": "powersync.ts:M61", + "number": 61, + "source": "src/powersync.ts", + "line": 415, + "position": 13968, + "operator": "value-undefined", + "original": "disposal.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void disposal . catch ( ( ) => undefined )", + "function": "disposeTrackingAfterAbort", + "key": "src/powersync.ts#disposeTrackingAfterAbort:value-undefined:void disposal . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M62", + "number": 62, + "source": "src/powersync.ts", + "line": 423, + "position": 14230, + "operator": "value-undefined", + "original": "(async () => {\n const dispose = await createDiffTrigger(options, appliedReceipts)\n disposeTracking = dispose\n })()", + "replacement": "undefined", + "anchor": "const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( )", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:value-undefined:const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( )" + }, + { + "id": "powersync.ts:M63", + "number": 63, + "source": "src/powersync.ts", + "line": 424, + "position": 14277, + "operator": "value-undefined", + "original": "createDiffTrigger(options, appliedReceipts)", + "replacement": "undefined", + "anchor": "const dispose = await createDiffTrigger ( options , appliedReceipts )", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:value-undefined:const dispose = await createDiffTrigger ( options , appliedReceipts )" + }, + { + "id": "powersync.ts:M64", + "number": 64, + "source": "src/powersync.ts", + "line": 432, + "position": 14471, + "operator": "if-true", + "original": "trackingSetup === setup", + "replacement": "true", + "anchor": "if ( trackingSetup === setup ) { trackingSetup = null }", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:if-true:if ( trackingSetup === setup ) { trackingSetup = null }" + }, + { + "id": "powersync.ts:M65", + "number": 65, + "source": "src/powersync.ts", + "line": 432, + "position": 14471, + "operator": "if-false", + "original": "trackingSetup === setup", + "replacement": "false", + "anchor": "if ( trackingSetup === setup ) { trackingSetup = null }", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:if-false:if ( trackingSetup === setup ) { trackingSetup = null }" + }, + { + "id": "powersync.ts:M66", + "number": 66, + "source": "src/powersync.ts", + "line": 432, + "position": 14485, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( trackingSetup === setup ) { trackingSetup = null }", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:binary-swap:if ( trackingSetup === setup ) { trackingSetup = null }" + }, + { + "id": "powersync.ts:M67", + "number": 67, + "source": "src/powersync.ts", + "line": 466, + "position": 15425, + "operator": "value-undefined", + "original": "database.triggers.createDiffTrigger({\n source: viewName,\n destination: trackedTableName,\n columns: table.columns.map((column) => column.name),\n setupContext,\n when,\n hooks: {\n beforeCreate: async (context) => {\n // A retained reload uses one snapshot across its source reads.\n // Other initial loads and subset loads keep batch commits. A\n // transaction begins only once its rows are read, so a batch\n // that fails leaves none open.\n const batches: Array> = []\n const apply = () => {\n begin({ snapshot })\n for (const row of batches.splice(0).flat()) {\n const type = writeType(row.id)\n const value = deserializeSyncRow(row)\n onInitialRow?.(value)\n write({ type, value })\n }\n const applied = commit()\n if (applied !== true) void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n batches.push(batchItems)\n if (!snapshot) apply()\n }\n if (snapshot) apply()\n if (afterRead) await afterRead()\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })\n },\n },\n })", + "replacement": "undefined", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:value-undefined:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })" + }, + { + "id": "powersync.ts:M68", + "number": 68, + "source": "src/powersync.ts", + "line": 467, + "position": 15473, + "operator": "field-drop", + "original": "source: viewName", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })" + }, + { + "id": "powersync.ts:M69", + "number": 69, + "source": "src/powersync.ts", + "line": 468, + "position": 15501, + "operator": "field-drop", + "original": "destination: trackedTableName", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~2" + }, + { + "id": "powersync.ts:M70", + "number": 70, + "source": "src/powersync.ts", + "line": 469, + "position": 15542, + "operator": "field-drop", + "original": "columns: table.columns.map((column) => column.name)", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~3" + }, + { + "id": "powersync.ts:M71", + "number": 71, + "source": "src/powersync.ts", + "line": 469, + "position": 15551, + "operator": "value-undefined", + "original": "table.columns.map((column) => column.name)", + "replacement": "undefined", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:value-undefined:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~2" + }, + { + "id": "powersync.ts:M72", + "number": 72, + "source": "src/powersync.ts", + "line": 470, + "position": 15605, + "operator": "field-drop", + "original": "setupContext", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~4" + }, + { + "id": "powersync.ts:M73", + "number": 73, + "source": "src/powersync.ts", + "line": 471, + "position": 15629, + "operator": "field-drop", + "original": "when", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~5" + }, + { + "id": "powersync.ts:M74", + "number": 74, + "source": "src/powersync.ts", + "line": 472, + "position": 15645, + "operator": "field-drop", + "original": "hooks: {\n beforeCreate: async (context) => {\n // A retained reload uses one snapshot across its source reads.\n // Other initial loads and subset loads keep batch commits. A\n // transaction begins only once its rows are read, so a batch\n // that fails leaves none open.\n const batches: Array> = []\n const apply = () => {\n begin({ snapshot })\n for (const row of batches.splice(0).flat()) {\n const type = writeType(row.id)\n const value = deserializeSyncRow(row)\n onInitialRow?.(value)\n write({ type, value })\n }\n const applied = commit()\n if (applied !== true) void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n batches.push(batchItems)\n if (!snapshot) apply()\n }\n if (snapshot) apply()\n if (afterRead) await afterRead()\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })\n },\n }", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~6" + }, + { + "id": "powersync.ts:M75", + "number": 75, + "source": "src/powersync.ts", + "line": 473, + "position": 15666, + "operator": "field-drop", + "original": "beforeCreate: async (context) => {\n // A retained reload uses one snapshot across its source reads.\n // Other initial loads and subset loads keep batch commits. A\n // transaction begins only once its rows are read, so a batch\n // that fails leaves none open.\n const batches: Array> = []\n const apply = () => {\n begin({ snapshot })\n for (const row of batches.splice(0).flat()) {\n const type = writeType(row.id)\n const value = deserializeSyncRow(row)\n onInitialRow?.(value)\n write({ type, value })\n }\n const applied = commit()\n if (applied !== true) void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n batches.push(batchItems)\n if (!snapshot) apply()\n }\n if (snapshot) apply()\n if (afterRead) await afterRead()\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })\n }", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })" + }, + { + "id": "powersync.ts:M76", + "number": 76, + "source": "src/powersync.ts", + "line": 480, + "position": 16087, + "operator": "delete-call", + "original": "begin({ snapshot })", + "replacement": ";", + "anchor": "begin ( { snapshot } )", + "function": "apply", + "key": "src/powersync.ts#apply:delete-call:begin ( { snapshot } )" + }, + { + "id": "powersync.ts:M77", + "number": 77, + "source": "src/powersync.ts", + "line": 480, + "position": 16095, + "operator": "field-drop", + "original": "snapshot", + "replacement": "", + "anchor": "begin ( { snapshot } )", + "function": "apply", + "key": "src/powersync.ts#apply:field-drop:begin ( { snapshot } )" + }, + { + "id": "powersync.ts:M78", + "number": 78, + "source": "src/powersync.ts", + "line": 481, + "position": 16141, + "operator": "value-undefined", + "original": "batches.splice(0).flat()", + "replacement": "undefined", + "anchor": "for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) }", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) }" + }, + { + "id": "powersync.ts:M79", + "number": 79, + "source": "src/powersync.ts", + "line": 482, + "position": 16200, + "operator": "value-undefined", + "original": "writeType(row.id)", + "replacement": "undefined", + "anchor": "const type = writeType ( row . id )", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:const type = writeType ( row . id )" + }, + { + "id": "powersync.ts:M80", + "number": 80, + "source": "src/powersync.ts", + "line": 483, + "position": 16250, + "operator": "value-undefined", + "original": "deserializeSyncRow(row)", + "replacement": "undefined", + "anchor": "const value = deserializeSyncRow ( row )", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:const value = deserializeSyncRow ( row )" + }, + { + "id": "powersync.ts:M81", + "number": 81, + "source": "src/powersync.ts", + "line": 484, + "position": 16292, + "operator": "delete-call", + "original": "onInitialRow?.(value)", + "replacement": ";", + "anchor": "onInitialRow ?. ( value )", + "function": "apply", + "key": "src/powersync.ts#apply:delete-call:onInitialRow ?. ( value )" + }, + { + "id": "powersync.ts:M82", + "number": 82, + "source": "src/powersync.ts", + "line": 485, + "position": 16332, + "operator": "delete-call", + "original": "write({ type, value })", + "replacement": ";", + "anchor": "write ( { type , value } )", + "function": "apply", + "key": "src/powersync.ts#apply:delete-call:write ( { type , value } )" + }, + { + "id": "powersync.ts:M83", + "number": 83, + "source": "src/powersync.ts", + "line": 485, + "position": 16340, + "operator": "field-drop", + "original": "type", + "replacement": "", + "anchor": "write ( { type , value } )", + "function": "apply", + "key": "src/powersync.ts#apply:field-drop:write ( { type , value } )" + }, + { + "id": "powersync.ts:M84", + "number": 84, + "source": "src/powersync.ts", + "line": 485, + "position": 16346, + "operator": "field-drop", + "original": "value", + "replacement": "", + "anchor": "write ( { type , value } )", + "function": "apply", + "key": "src/powersync.ts#apply:field-drop:write ( { type , value } )~2" + }, + { + "id": "powersync.ts:M85", + "number": 85, + "source": "src/powersync.ts", + "line": 487, + "position": 16405, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const applied = commit ( )", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:const applied = commit ( )" + }, + { + "id": "powersync.ts:M86", + "number": 86, + "source": "src/powersync.ts", + "line": 488, + "position": 16434, + "operator": "if-true", + "original": "applied !== true", + "replacement": "true", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:if-true:if ( applied !== true ) void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M87", + "number": 87, + "source": "src/powersync.ts", + "line": 488, + "position": 16434, + "operator": "if-false", + "original": "applied !== true", + "replacement": "false", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:if-false:if ( applied !== true ) void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M88", + "number": 88, + "source": "src/powersync.ts", + "line": 488, + "position": 16442, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:binary-swap:if ( applied !== true ) void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M89", + "number": 89, + "source": "src/powersync.ts", + "line": 488, + "position": 16446, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:bool-flip:if ( applied !== true ) void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M90", + "number": 90, + "source": "src/powersync.ts", + "line": 488, + "position": 16457, + "operator": "value-undefined", + "original": "applied.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M91", + "number": 91, + "source": "src/powersync.ts", + "line": 489, + "position": 16504, + "operator": "delete-call", + "original": "appliedReceipts.push(applied)", + "replacement": ";", + "anchor": "appliedReceipts . push ( applied )", + "function": "apply", + "key": "src/powersync.ts#apply:delete-call:appliedReceipts . push ( applied )" + }, + { + "id": "powersync.ts:M92", + "number": 92, + "source": "src/powersync.ts", + "line": 493, + "position": 16652, + "operator": "loop-false", + "original": "currentBatchCount == syncBatchSize", + "replacement": "false", + "anchor": "while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) }", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:loop-false:while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) }" + }, + { + "id": "powersync.ts:M93", + "number": 93, + "source": "src/powersync.ts", + "line": 494, + "position": 16731, + "operator": "value-undefined", + "original": "batchQuery(\n context,\n syncBatchSize,\n cursor,\n )", + "replacement": "undefined", + "anchor": "const batchItems = await batchQuery ( context , syncBatchSize , cursor , )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:value-undefined:const batchItems = await batchQuery ( context , syncBatchSize , cursor , )" + }, + { + "id": "powersync.ts:M94", + "number": 94, + "source": "src/powersync.ts", + "line": 501, + "position": 16961, + "operator": "delete-call", + "original": "batches.push(batchItems)", + "replacement": ";", + "anchor": "batches . push ( batchItems )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:delete-call:batches . push ( batchItems )" + }, + { + "id": "powersync.ts:M95", + "number": 95, + "source": "src/powersync.ts", + "line": 502, + "position": 17006, + "operator": "if-true", + "original": "!snapshot", + "replacement": "true", + "anchor": "if ( ! snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-true:if ( ! snapshot ) apply ( )" + }, + { + "id": "powersync.ts:M96", + "number": 96, + "source": "src/powersync.ts", + "line": 502, + "position": 17006, + "operator": "if-false", + "original": "!snapshot", + "replacement": "false", + "anchor": "if ( ! snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-false:if ( ! snapshot ) apply ( )" + }, + { + "id": "powersync.ts:M97", + "number": 97, + "source": "src/powersync.ts", + "line": 502, + "position": 17006, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:drop-not:if ( ! snapshot ) apply ( )" + }, + { + "id": "powersync.ts:M98", + "number": 98, + "source": "src/powersync.ts", + "line": 502, + "position": 17017, + "operator": "delete-call", + "original": "apply()", + "replacement": ";", + "anchor": "apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:delete-call:apply ( )" + }, + { + "id": "powersync.ts:M99", + "number": 99, + "source": "src/powersync.ts", + "line": 504, + "position": 17059, + "operator": "if-true", + "original": "snapshot", + "replacement": "true", + "anchor": "if ( snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-true:if ( snapshot ) apply ( )" + }, + { + "id": "powersync.ts:M100", + "number": 100, + "source": "src/powersync.ts", + "line": 504, + "position": 17059, + "operator": "if-false", + "original": "snapshot", + "replacement": "false", + "anchor": "if ( snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-false:if ( snapshot ) apply ( )" + }, + { + "id": "powersync.ts:M101", + "number": 101, + "source": "src/powersync.ts", + "line": 504, + "position": 17069, + "operator": "delete-call", + "original": "apply()", + "replacement": ";", + "anchor": "apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:delete-call:apply ( )~2" + }, + { + "id": "powersync.ts:M102", + "number": 102, + "source": "src/powersync.ts", + "line": 505, + "position": 17095, + "operator": "if-true", + "original": "afterRead", + "replacement": "true", + "anchor": "if ( afterRead ) await afterRead ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-true:if ( afterRead ) await afterRead ( )" + }, + { + "id": "powersync.ts:M103", + "number": 103, + "source": "src/powersync.ts", + "line": 505, + "position": 17095, + "operator": "if-false", + "original": "afterRead", + "replacement": "false", + "anchor": "if ( afterRead ) await afterRead ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-false:if ( afterRead ) await afterRead ( )" + }, + { + "id": "powersync.ts:M104", + "number": 104, + "source": "src/powersync.ts", + "line": 505, + "position": 17112, + "operator": "value-undefined", + "original": "afterRead()", + "replacement": "undefined", + "anchor": "await afterRead ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:value-undefined:await afterRead ( )" + }, + { + "id": "powersync.ts:M105", + "number": 105, + "source": "src/powersync.ts", + "line": 506, + "position": 17138, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:delete-call:database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })" + }, + { + "id": "powersync.ts:M106", + "number": 106, + "source": "src/powersync.ts", + "line": 507, + "position": 17176, + "operator": "field-drop", + "original": "level: LogLevels.info", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })" + }, + { + "id": "powersync.ts:M107", + "number": 107, + "source": "src/powersync.ts", + "line": 508, + "position": 17215, + "operator": "field-drop", + "original": "message: `Sync is ready for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })~2" + }, + { + "id": "powersync.ts:M108", + "number": 108, + "source": "src/powersync.ts", + "line": 520, + "position": 17679, + "operator": "if-true", + "original": "setup", + "replacement": "true", + "anchor": "if ( setup ) { await setup . catch ( ( ) => undefined ) }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:if-true:if ( setup ) { await setup . catch ( ( ) => undefined ) }" + }, + { + "id": "powersync.ts:M109", + "number": 109, + "source": "src/powersync.ts", + "line": 520, + "position": 17679, + "operator": "if-false", + "original": "setup", + "replacement": "false", + "anchor": "if ( setup ) { await setup . catch ( ( ) => undefined ) }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:if-false:if ( setup ) { await setup . catch ( ( ) => undefined ) }" + }, + { + "id": "powersync.ts:M110", + "number": 110, + "source": "src/powersync.ts", + "line": 521, + "position": 17704, + "operator": "value-undefined", + "original": "setup.catch(() => undefined)", + "replacement": "undefined", + "anchor": "await setup . catch ( ( ) => undefined )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:value-undefined:await setup . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M111", + "number": 111, + "source": "src/powersync.ts", + "line": 523, + "position": 17755, + "operator": "if-true", + "original": "!disposeTracking", + "replacement": "true", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:if-true:if ( ! disposeTracking ) { return }" + }, + { + "id": "powersync.ts:M112", + "number": 112, + "source": "src/powersync.ts", + "line": 523, + "position": 17755, + "operator": "if-false", + "original": "!disposeTracking", + "replacement": "false", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:if-false:if ( ! disposeTracking ) { return }" + }, + { + "id": "powersync.ts:M113", + "number": 113, + "source": "src/powersync.ts", + "line": 523, + "position": 17755, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:drop-not:if ( ! disposeTracking ) { return }" + }, + { + "id": "powersync.ts:M114", + "number": 114, + "source": "src/powersync.ts", + "line": 528, + "position": 17879, + "operator": "value-undefined", + "original": "database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })\n })", + "replacement": "undefined", + "anchor": "await database . writeTransaction ( async ( context ) => { await flushDiffRecordsWithContext ( context , ignoredReceipts ) } ) . catch ( ( error ) => { database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } ) } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:value-undefined:await database . writeTransaction ( async ( context ) => { await flushDiffRecordsWithContext ( context , ignoredReceipts ) } ) . catch ( ( error ) => { database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } ) } )" + }, + { + "id": "powersync.ts:M115", + "number": 115, + "source": "src/powersync.ts", + "line": 530, + "position": 17955, + "operator": "value-undefined", + "original": "flushDiffRecordsWithContext(context, ignoredReceipts)", + "replacement": "undefined", + "anchor": "await flushDiffRecordsWithContext ( context , ignoredReceipts )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:value-undefined:await flushDiffRecordsWithContext ( context , ignoredReceipts )" + }, + { + "id": "powersync.ts:M116", + "number": 116, + "source": "src/powersync.ts", + "line": 533, + "position": 18064, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:delete-call:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )" + }, + { + "id": "powersync.ts:M117", + "number": 117, + "source": "src/powersync.ts", + "line": 534, + "position": 18100, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )" + }, + { + "id": "powersync.ts:M118", + "number": 118, + "source": "src/powersync.ts", + "line": 535, + "position": 18138, + "operator": "field-drop", + "original": "message: `An error has been detected in the sync handler`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )~2" + }, + { + "id": "powersync.ts:M119", + "number": 119, + "source": "src/powersync.ts", + "line": 536, + "position": 18211, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )~3" + }, + { + "id": "powersync.ts:M120", + "number": 120, + "source": "src/powersync.ts", + "line": 547, + "position": 18600, + "operator": "if-true", + "original": "!disposeTracking", + "replacement": "true", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:if-true:if ( ! disposeTracking ) { return }" + }, + { + "id": "powersync.ts:M121", + "number": 121, + "source": "src/powersync.ts", + "line": 547, + "position": 18600, + "operator": "if-false", + "original": "!disposeTracking", + "replacement": "false", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:if-false:if ( ! disposeTracking ) { return }" + }, + { + "id": "powersync.ts:M122", + "number": 122, + "source": "src/powersync.ts", + "line": 547, + "position": 18600, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:drop-not:if ( ! disposeTracking ) { return }" + }, + { + "id": "powersync.ts:M123", + "number": 123, + "source": "src/powersync.ts", + "line": 552, + "position": 18672, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:begin ( )" + }, + { + "id": "powersync.ts:M124", + "number": 124, + "source": "src/powersync.ts", + "line": 553, + "position": 18715, + "operator": "value-undefined", + "original": "context.getAll(\n `SELECT * FROM ${trackedTableName} ORDER BY operation_id ASC`,\n )", + "replacement": "undefined", + "anchor": "const operations = await context . getAll < TriggerDiffRecord > ( `SELECT * FROM ${ trackedTableName } ORDER BY operation_id ASC `,\n )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const operations = await context . getAll < TriggerDiffRecord > ( `SELECT * FROM ${ trackedTableName } ORDER BY operation_id ASC `,\n )" + }, + { + "id": "powersync.ts:M125", + "number": 125, + "source": "src/powersync.ts", + "line": 560, + "position": 19034, + "operator": "value-undefined", + "original": "deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })", + "replacement": "undefined", + "anchor": "const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )" + }, + { + "id": "powersync.ts:M126", + "number": 126, + "source": "src/powersync.ts", + "line": 561, + "position": 19069, + "operator": "field-drop", + "original": "id", + "replacement": "", + "anchor": "const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )" + }, + { + "id": "powersync.ts:M127", + "number": 127, + "source": "src/powersync.ts", + "line": 562, + "position": 19090, + "operator": "value-undefined", + "original": "JSON.parse(value)", + "replacement": "undefined", + "anchor": "const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )~2" + }, + { + "id": "powersync.ts:M128", + "number": 128, + "source": "src/powersync.ts", + "line": 566, + "position": 19240, + "operator": "value-undefined", + "original": "deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })", + "replacement": "undefined", + "anchor": "const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined" + }, + { + "id": "powersync.ts:M129", + "number": 129, + "source": "src/powersync.ts", + "line": 567, + "position": 19281, + "operator": "field-drop", + "original": "id", + "replacement": "", + "anchor": "const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined" + }, + { + "id": "powersync.ts:M130", + "number": 130, + "source": "src/powersync.ts", + "line": 568, + "position": 19308, + "operator": "value-undefined", + "original": "JSON.parse(op.previous_value)", + "replacement": "undefined", + "anchor": "const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined~2" + }, + { + "id": "powersync.ts:M131", + "number": 131, + "source": "src/powersync.ts", + "line": 571, + "position": 19400, + "operator": "delete-call", + "original": "write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })", + "replacement": ";", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )" + }, + { + "id": "powersync.ts:M132", + "number": 132, + "source": "src/powersync.ts", + "line": 572, + "position": 19422, + "operator": "field-drop", + "original": "type: mapOperation(operation)", + "replacement": "", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )" + }, + { + "id": "powersync.ts:M133", + "number": 133, + "source": "src/powersync.ts", + "line": 572, + "position": 19428, + "operator": "value-undefined", + "original": "mapOperation(operation)", + "replacement": "undefined", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )" + }, + { + "id": "powersync.ts:M134", + "number": 134, + "source": "src/powersync.ts", + "line": 573, + "position": 19467, + "operator": "field-drop", + "original": "value: parsedValue", + "replacement": "", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )~2" + }, + { + "id": "powersync.ts:M135", + "number": 135, + "source": "src/powersync.ts", + "line": 574, + "position": 19501, + "operator": "field-drop", + "original": "previousValue: parsedPreviousValue", + "replacement": "", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )~3" + }, + { + "id": "powersync.ts:M136", + "number": 136, + "source": "src/powersync.ts", + "line": 576, + "position": 19564, + "operator": "delete-call", + "original": "pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })", + "replacement": ";", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )" + }, + { + "id": "powersync.ts:M137", + "number": 137, + "source": "src/powersync.ts", + "line": 577, + "position": 19603, + "operator": "field-drop", + "original": "id", + "replacement": "", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )" + }, + { + "id": "powersync.ts:M138", + "number": 138, + "source": "src/powersync.ts", + "line": 578, + "position": 19621, + "operator": "field-drop", + "original": "operation", + "replacement": "", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )~2" + }, + { + "id": "powersync.ts:M139", + "number": 139, + "source": "src/powersync.ts", + "line": 579, + "position": 19646, + "operator": "field-drop", + "original": "timestamp", + "replacement": "", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )~3" + }, + { + "id": "powersync.ts:M140", + "number": 140, + "source": "src/powersync.ts", + "line": 580, + "position": 19671, + "operator": "field-drop", + "original": "tableName: viewName", + "replacement": "", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )~4" + }, + { + "id": "powersync.ts:M141", + "number": 141, + "source": "src/powersync.ts", + "line": 585, + "position": 19778, + "operator": "value-undefined", + "original": "context.execute(`DELETE FROM ${trackedTableName}`)", + "replacement": "undefined", + "anchor": "await context . execute ( `DELETE FROM ${ trackedTableName } `)", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:await context . execute ( `DELETE FROM ${ trackedTableName } `)" + }, + { + "id": "powersync.ts:M142", + "number": 142, + "source": "src/powersync.ts", + "line": 587, + "position": 19856, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const applied = commit ( )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const applied = commit ( )" + }, + { + "id": "powersync.ts:M143", + "number": 143, + "source": "src/powersync.ts", + "line": 588, + "position": 19875, + "operator": "delete-call", + "original": "appliedReceipts.push(applied)", + "replacement": ";", + "anchor": "appliedReceipts . push ( applied )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:appliedReceipts . push ( applied )" + }, + { + "id": "powersync.ts:M144", + "number": 144, + "source": "src/powersync.ts", + "line": 593, + "position": 20195, + "operator": "delete-call", + "original": "pendingOperationStore.resolvePendingFor(pendingOperations)", + "replacement": ";", + "anchor": "pendingOperationStore . resolvePendingFor ( pendingOperations )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:pendingOperationStore . resolvePendingFor ( pendingOperations )" + }, + { + "id": "powersync.ts:M145", + "number": 145, + "source": "src/powersync.ts", + "line": 595, + "position": 20290, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )" + }, + { + "id": "powersync.ts:M146", + "number": 146, + "source": "src/powersync.ts", + "line": 596, + "position": 20324, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )" + }, + { + "id": "powersync.ts:M147", + "number": 147, + "source": "src/powersync.ts", + "line": 597, + "position": 20360, + "operator": "field-drop", + "original": "message: `An error has been detected in the sync handler`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )~2" + }, + { + "id": "powersync.ts:M148", + "number": 148, + "source": "src/powersync.ts", + "line": 598, + "position": 20431, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )~3" + }, + { + "id": "powersync.ts:M149", + "number": 149, + "source": "src/powersync.ts", + "line": 605, + "position": 20606, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.info,\n message: `Sync is starting for ${viewName} into ${trackedTableName}`,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "start", + "key": "src/powersync.ts#start:delete-call:database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })" + }, + { + "id": "powersync.ts:M150", + "number": 150, + "source": "src/powersync.ts", + "line": 606, + "position": 20638, + "operator": "field-drop", + "original": "level: LogLevels.info", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })" + }, + { + "id": "powersync.ts:M151", + "number": 151, + "source": "src/powersync.ts", + "line": 607, + "position": 20671, + "operator": "field-drop", + "original": "message: `Sync is starting for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })~2" + }, + { + "id": "powersync.ts:M152", + "number": 152, + "source": "src/powersync.ts", + "line": 609, + "position": 20760, + "operator": "delete-call", + "original": "database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )", + "replacement": ";", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:delete-call:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )" + }, + { + "id": "powersync.ts:M153", + "number": 153, + "source": "src/powersync.ts", + "line": 611, + "position": 20815, + "operator": "field-drop", + "original": "onChange: async () => {\n await flushDiffRecords()\n }", + "replacement": "", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "onChange", + "key": "src/powersync.ts#onChange:field-drop:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )" + }, + { + "id": "powersync.ts:M154", + "number": 154, + "source": "src/powersync.ts", + "line": 612, + "position": 20859, + "operator": "value-undefined", + "original": "flushDiffRecords()", + "replacement": "undefined", + "anchor": "await flushDiffRecords ( )", + "function": "onChange", + "key": "src/powersync.ts#onChange:value-undefined:await flushDiffRecords ( )" + }, + { + "id": "powersync.ts:M155", + "number": 155, + "source": "src/powersync.ts", + "line": 616, + "position": 20930, + "operator": "field-drop", + "original": "signal: abortController.signal", + "replacement": "", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )" + }, + { + "id": "powersync.ts:M156", + "number": 156, + "source": "src/powersync.ts", + "line": 617, + "position": 20974, + "operator": "field-drop", + "original": "triggerImmediate: false", + "replacement": "", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )~2" + }, + { + "id": "powersync.ts:M157", + "number": 157, + "source": "src/powersync.ts", + "line": 617, + "position": 20992, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:bool-flip:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )" + }, + { + "id": "powersync.ts:M158", + "number": 158, + "source": "src/powersync.ts", + "line": 618, + "position": 21011, + "operator": "field-drop", + "original": "tables: [trackedTableName]", + "replacement": "", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )~3" + }, + { + "id": "powersync.ts:M159", + "number": 159, + "source": "src/powersync.ts", + "line": 622, + "position": 21077, + "operator": "value-undefined", + "original": "afterOnChangeRegistered?.()", + "replacement": "undefined", + "anchor": "await afterOnChangeRegistered ?. ( )", + "function": "start", + "key": "src/powersync.ts#start:value-undefined:await afterOnChangeRegistered ?. ( )" + }, + { + "id": "powersync.ts:M160", + "number": 160, + "source": "src/powersync.ts", + "line": 625, + "position": 21200, + "operator": "if-true", + "original": "abortController.signal.aborted", + "replacement": "true", + "anchor": "if ( abortController . signal . aborted ) { await disposeTrackingAfterAbort ( ) } else { abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , ) }", + "function": "start", + "key": "src/powersync.ts#start:if-true:if ( abortController . signal . aborted ) { await disposeTrackingAfterAbort ( ) } else { abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , ) }" + }, + { + "id": "powersync.ts:M161", + "number": 161, + "source": "src/powersync.ts", + "line": 625, + "position": 21200, + "operator": "if-false", + "original": "abortController.signal.aborted", + "replacement": "false", + "anchor": "if ( abortController . signal . aborted ) { await disposeTrackingAfterAbort ( ) } else { abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , ) }", + "function": "start", + "key": "src/powersync.ts#start:if-false:if ( abortController . signal . aborted ) { await disposeTrackingAfterAbort ( ) } else { abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , ) }" + }, + { + "id": "powersync.ts:M162", + "number": 162, + "source": "src/powersync.ts", + "line": 626, + "position": 21250, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "await disposeTrackingAfterAbort ( )", + "function": "start", + "key": "src/powersync.ts#start:value-undefined:await disposeTrackingAfterAbort ( )" + }, + { + "id": "powersync.ts:M163", + "number": 163, + "source": "src/powersync.ts", + "line": 628, + "position": 21305, + "operator": "delete-call", + "original": "abortController.signal.addEventListener(\n `abort`,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )", + "replacement": ";", + "anchor": "abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )", + "function": "start", + "key": "src/powersync.ts#start:delete-call:abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )" + }, + { + "id": "powersync.ts:M164", + "number": 164, + "source": "src/powersync.ts", + "line": 631, + "position": 21406, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "void disposeTrackingAfterAbort ( )", + "function": "start", + "key": "src/powersync.ts#start:value-undefined:void disposeTrackingAfterAbort ( )" + }, + { + "id": "powersync.ts:M165", + "number": 165, + "source": "src/powersync.ts", + "line": 633, + "position": 21463, + "operator": "field-drop", + "original": "once: true", + "replacement": "", + "anchor": "abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )", + "function": "start", + "key": "src/powersync.ts#start:field-drop:abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )" + }, + { + "id": "powersync.ts:M166", + "number": 166, + "source": "src/powersync.ts", + "line": 633, + "position": 21469, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )", + "function": "start", + "key": "src/powersync.ts#start:bool-flip:abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )" + }, + { + "id": "powersync.ts:M167", + "number": 167, + "source": "src/powersync.ts", + "line": 642, + "position": 21700, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let onUnloadStarted = false", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:bool-flip:let onUnloadStarted = false" + }, + { + "id": "powersync.ts:M168", + "number": 168, + "source": "src/powersync.ts", + "line": 644, + "position": 21731, + "operator": "value-undefined", + "original": "start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: `delete`, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })", + "replacement": "undefined", + "anchor": "const startup = start ( async ( ) => { const cleanup = await restConfig . onLoad ?. ( ) onUnload = cleanup if ( abortController . signal . aborted ) { return } const appliedReceipts : Array < SyncAppliedReceipt > = [ ] const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const startup = start ( async ( ) => { const cleanup = await restConfig . onLoad ?. ( ) onUnload = cleanup if ( abortController . signal . aborted ) { return } const appliedReceipts : Array < SyncAppliedReceipt > = [ ] const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })" + }, + { + "id": "powersync.ts:M169", + "number": 169, + "source": "src/powersync.ts", + "line": 645, + "position": 21783, + "operator": "value-undefined", + "original": "restConfig.onLoad?.()", + "replacement": "undefined", + "anchor": "const cleanup = await restConfig . onLoad ?. ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const cleanup = await restConfig . onLoad ?. ( )" + }, + { + "id": "powersync.ts:M170", + "number": 170, + "source": "src/powersync.ts", + "line": 647, + "position": 21848, + "operator": "if-true", + "original": "abortController.signal.aborted", + "replacement": "true", + "anchor": "if ( abortController . signal . aborted ) { return }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( abortController . signal . aborted ) { return }" + }, + { + "id": "powersync.ts:M171", + "number": 171, + "source": "src/powersync.ts", + "line": 647, + "position": 21848, + "operator": "if-false", + "original": "abortController.signal.aborted", + "replacement": "false", + "anchor": "if ( abortController . signal . aborted ) { return }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( abortController . signal . aborted ) { return }" + }, + { + "id": "powersync.ts:M172", + "number": 172, + "source": "src/powersync.ts", + "line": 653, + "position": 22027, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined" + }, + { + "id": "powersync.ts:M173", + "number": 173, + "source": "src/powersync.ts", + "line": 653, + "position": 22030, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:drop-not:const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined" + }, + { + "id": "powersync.ts:M174", + "number": 174, + "source": "src/powersync.ts", + "line": 654, + "position": 22100, + "operator": "value-undefined", + "original": "establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: `delete`, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "replacement": "undefined", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M175", + "number": 175, + "source": "src/powersync.ts", + "line": 658, + "position": 22303, + "operator": "field-drop", + "original": "snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M176", + "number": 176, + "source": "src/powersync.ts", + "line": 659, + "position": 22341, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M177", + "number": 177, + "source": "src/powersync.ts", + "line": 660, + "position": 22370, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:drop-not:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M178", + "number": 178, + "source": "src/powersync.ts", + "line": 662, + "position": 22481, + "operator": "field-drop", + "original": "afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: `delete`, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~2" + }, + { + "id": "powersync.ts:M179", + "number": 179, + "source": "src/powersync.ts", + "line": 663, + "position": 22515, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~2" + }, + { + "id": "powersync.ts:M180", + "number": 180, + "source": "src/powersync.ts", + "line": 665, + "position": 22592, + "operator": "value-undefined", + "original": "persistence.hydrateBaseline()", + "replacement": "undefined", + "anchor": "await persistence . hydrateBaseline ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await persistence . hydrateBaseline ( )" + }, + { + "id": "powersync.ts:M181", + "number": 181, + "source": "src/powersync.ts", + "line": 666, + "position": 22648, + "operator": "if-true", + "original": "abortController.signal.aborted", + "replacement": "true", + "anchor": "if ( abortController . signal . aborted ) return", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( abortController . signal . aborted ) return" + }, + { + "id": "powersync.ts:M182", + "number": 182, + "source": "src/powersync.ts", + "line": 666, + "position": 22648, + "operator": "if-false", + "original": "abortController.signal.aborted", + "replacement": "false", + "anchor": "if ( abortController . signal . aborted ) return", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( abortController . signal . aborted ) return" + }, + { + "id": "powersync.ts:M183", + "number": 183, + "source": "src/powersync.ts", + "line": 673, + "position": 23076, + "operator": "value-undefined", + "original": "collection._state.acceptedSyncedEntries()", + "replacement": "undefined", + "anchor": "for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) }" + }, + { + "id": "powersync.ts:M184", + "number": 184, + "source": "src/powersync.ts", + "line": 674, + "position": 23149, + "operator": "if-true", + "original": "!listed.has(key)", + "replacement": "true", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( ! listed . has ( key ) ) gone . push ( key )" + }, + { + "id": "powersync.ts:M185", + "number": 185, + "source": "src/powersync.ts", + "line": 674, + "position": 23149, + "operator": "if-false", + "original": "!listed.has(key)", + "replacement": "false", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( ! listed . has ( key ) ) gone . push ( key )" + }, + { + "id": "powersync.ts:M186", + "number": 186, + "source": "src/powersync.ts", + "line": 674, + "position": 23149, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:drop-not:if ( ! listed . has ( key ) ) gone . push ( key )" + }, + { + "id": "powersync.ts:M187", + "number": 187, + "source": "src/powersync.ts", + "line": 674, + "position": 23150, + "operator": "value-undefined", + "original": "listed.has(key)", + "replacement": "undefined", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:if ( ! listed . has ( key ) ) gone . push ( key )" + }, + { + "id": "powersync.ts:M188", + "number": 188, + "source": "src/powersync.ts", + "line": 674, + "position": 23167, + "operator": "delete-call", + "original": "gone.push(key)", + "replacement": ";", + "anchor": "gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:gone . push ( key )" + }, + { + "id": "powersync.ts:M189", + "number": 189, + "source": "src/powersync.ts", + "line": 676, + "position": 23228, + "operator": "delete-call", + "original": "listed.clear()", + "replacement": ";", + "anchor": "listed . clear ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:listed . clear ( )" + }, + { + "id": "powersync.ts:M190", + "number": 190, + "source": "src/powersync.ts", + "line": 679, + "position": 23334, + "operator": "loop-false", + "original": "first < gone.length", + "replacement": "false", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:loop-false:for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) }" + }, + { + "id": "powersync.ts:M191", + "number": 191, + "source": "src/powersync.ts", + "line": 679, + "position": 23340, + "operator": "binary-swap", + "original": "<", + "replacement": "<=", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) }" + }, + { + "id": "powersync.ts:M192", + "number": 192, + "source": "src/powersync.ts", + "line": 682, + "position": 23452, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:begin ( )" + }, + { + "id": "powersync.ts:M193", + "number": 193, + "source": "src/powersync.ts", + "line": 683, + "position": 23502, + "operator": "value-undefined", + "original": "gone.slice(\n first,\n first + syncBatchSize,\n )", + "replacement": "undefined", + "anchor": "for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } )" + }, + { + "id": "powersync.ts:M194", + "number": 194, + "source": "src/powersync.ts", + "line": 687, + "position": 23649, + "operator": "delete-call", + "original": "write({ type: `delete`, key })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key } )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:write ( { type : `delete` , key } )" + }, + { + "id": "powersync.ts:M195", + "number": 195, + "source": "src/powersync.ts", + "line": 687, + "position": 23657, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:write ( { type : `delete` , key } )" + }, + { + "id": "powersync.ts:M196", + "number": 196, + "source": "src/powersync.ts", + "line": 687, + "position": 23673, + "operator": "field-drop", + "original": "key", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:write ( { type : `delete` , key } )~2" + }, + { + "id": "powersync.ts:M197", + "number": 197, + "source": "src/powersync.ts", + "line": 688, + "position": 23720, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const applied = commit ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const applied = commit ( )" + }, + { + "id": "powersync.ts:M198", + "number": 198, + "source": "src/powersync.ts", + "line": 689, + "position": 23757, + "operator": "if-true", + "original": "applied !== true", + "replacement": "true", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( applied !== true ) void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M199", + "number": 199, + "source": "src/powersync.ts", + "line": 689, + "position": 23757, + "operator": "if-false", + "original": "applied !== true", + "replacement": "false", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( applied !== true ) void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M200", + "number": 200, + "source": "src/powersync.ts", + "line": 689, + "position": 23765, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:if ( applied !== true ) void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M201", + "number": 201, + "source": "src/powersync.ts", + "line": 689, + "position": 23769, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:bool-flip:if ( applied !== true ) void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M202", + "number": 202, + "source": "src/powersync.ts", + "line": 690, + "position": 23806, + "operator": "value-undefined", + "original": "applied.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:void applied . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M203", + "number": 203, + "source": "src/powersync.ts", + "line": 691, + "position": 23861, + "operator": "delete-call", + "original": "appliedReceipts.push(applied)", + "replacement": ";", + "anchor": "appliedReceipts . push ( applied )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:appliedReceipts . push ( applied )" + }, + { + "id": "powersync.ts:M204", + "number": 204, + "source": "src/powersync.ts", + "line": 695, + "position": 23982, + "operator": "field-drop", + "original": "when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n }", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~3" + }, + { + "id": "powersync.ts:M205", + "number": 205, + "source": "src/powersync.ts", + "line": 696, + "position": 24006, + "operator": "field-drop", + "original": "[DiffTriggerOperation.INSERT]: `TRUE`", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~4" + }, + { + "id": "powersync.ts:M206", + "number": 206, + "source": "src/powersync.ts", + "line": 697, + "position": 24061, + "operator": "field-drop", + "original": "[DiffTriggerOperation.UPDATE]: `TRUE`", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~5" + }, + { + "id": "powersync.ts:M207", + "number": 207, + "source": "src/powersync.ts", + "line": 698, + "position": 24116, + "operator": "field-drop", + "original": "[DiffTriggerOperation.DELETE]: `TRUE`", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~6" + }, + { + "id": "powersync.ts:M208", + "number": 208, + "source": "src/powersync.ts", + "line": 700, + "position": 24186, + "operator": "field-drop", + "original": "onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~7" + }, + { + "id": "powersync.ts:M209", + "number": 209, + "source": "src/powersync.ts", + "line": 702, + "position": 24258, + "operator": "delete-call", + "original": "listed.add(asPowerSyncRecord(value).id)", + "replacement": ";", + "anchor": "listed . add ( asPowerSyncRecord ( value ) . id )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:listed . add ( asPowerSyncRecord ( value ) . id )" + }, + { + "id": "powersync.ts:M210", + "number": 210, + "source": "src/powersync.ts", + "line": 705, + "position": 24361, + "operator": "field-drop", + "original": "writeType: (_rowId: string) => `insert`", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "writeType", + "key": "src/powersync.ts#writeType:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M211", + "number": 211, + "source": "src/powersync.ts", + "line": 706, + "position": 24416, + "operator": "field-drop", + "original": "batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M212", + "number": 212, + "source": "src/powersync.ts", + "line": 711, + "position": 24574, + "operator": "value-undefined", + "original": "lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )", + "replacement": "undefined", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:value-undefined:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M213", + "number": 213, + "source": "src/powersync.ts", + "line": 720, + "position": 24960, + "operator": "value-undefined", + "original": "Promise.all(appliedReceipts.map(whenSyncAccepted))", + "replacement": "undefined", + "anchor": "await Promise . all ( appliedReceipts . map ( whenSyncAccepted ) )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await Promise . all ( appliedReceipts . map ( whenSyncAccepted ) )" + }, + { + "id": "powersync.ts:M214", + "number": 214, + "source": "src/powersync.ts", + "line": 720, + "position": 24972, + "operator": "value-undefined", + "original": "appliedReceipts.map(whenSyncAccepted)", + "replacement": "undefined", + "anchor": "await Promise . all ( appliedReceipts . map ( whenSyncAccepted ) )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await Promise . all ( appliedReceipts . map ( whenSyncAccepted ) )~2" + }, + { + "id": "powersync.ts:M215", + "number": 215, + "source": "src/powersync.ts", + "line": 721, + "position": 25021, + "operator": "delete-call", + "original": "markReady()", + "replacement": ";", + "anchor": "markReady ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:markReady ( )" + }, + { + "id": "powersync.ts:M216", + "number": 216, + "source": "src/powersync.ts", + "line": 723, + "position": 25073, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })" + }, + { + "id": "powersync.ts:M217", + "number": 217, + "source": "src/powersync.ts", + "line": 724, + "position": 25107, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })" + }, + { + "id": "powersync.ts:M218", + "number": 218, + "source": "src/powersync.ts", + "line": 725, + "position": 25143, + "operator": "field-drop", + "original": "message: `Could not start syncing process for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })~2" + }, + { + "id": "powersync.ts:M219", + "number": 219, + "source": "src/powersync.ts", + "line": 726, + "position": 25240, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })~3" + }, + { + "id": "powersync.ts:M220", + "number": 220, + "source": "src/powersync.ts", + "line": 728, + "position": 25274, + "operator": "if-true", + "original": "!ready", + "replacement": "true", + "anchor": "if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }" + }, + { + "id": "powersync.ts:M221", + "number": 221, + "source": "src/powersync.ts", + "line": 728, + "position": 25274, + "operator": "if-false", + "original": "!ready", + "replacement": "false", + "anchor": "if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }" + }, + { + "id": "powersync.ts:M222", + "number": 222, + "source": "src/powersync.ts", + "line": 728, + "position": 25274, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:drop-not:if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }" + }, + { + "id": "powersync.ts:M223", + "number": 223, + "source": "src/powersync.ts", + "line": 729, + "position": 25296, + "operator": "delete-call", + "original": "fail(error)", + "replacement": ";", + "anchor": "fail ( error )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:fail ( error )" + }, + { + "id": "powersync.ts:M224", + "number": 224, + "source": "src/powersync.ts", + "line": 732, + "position": 25477, + "operator": "delete-call", + "original": "abortController.abort()", + "replacement": ";", + "anchor": "abortController . abort ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:abortController . abort ( )" + }, + { + "id": "powersync.ts:M225", + "number": 225, + "source": "src/powersync.ts", + "line": 733, + "position": 25518, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "void disposeTrackingAfterAbort ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:void disposeTrackingAfterAbort ( )" + }, + { + "id": "powersync.ts:M226", + "number": 226, + "source": "src/powersync.ts", + "line": 738, + "position": 25645, + "operator": "if-true", + "original": "onUnloadStarted || !onUnload", + "replacement": "true", + "anchor": "if ( onUnloadStarted || ! onUnload ) return", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:if-true:if ( onUnloadStarted || ! onUnload ) return" + }, + { + "id": "powersync.ts:M227", + "number": 227, + "source": "src/powersync.ts", + "line": 738, + "position": 25645, + "operator": "if-false", + "original": "onUnloadStarted || !onUnload", + "replacement": "false", + "anchor": "if ( onUnloadStarted || ! onUnload ) return", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:if-false:if ( onUnloadStarted || ! onUnload ) return" + }, + { + "id": "powersync.ts:M228", + "number": 228, + "source": "src/powersync.ts", + "line": 738, + "position": 25661, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( onUnloadStarted || ! onUnload ) return", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:binary-swap:if ( onUnloadStarted || ! onUnload ) return" + }, + { + "id": "powersync.ts:M229", + "number": 229, + "source": "src/powersync.ts", + "line": 738, + "position": 25664, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( onUnloadStarted || ! onUnload ) return", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:drop-not:if ( onUnloadStarted || ! onUnload ) return" + }, + { + "id": "powersync.ts:M230", + "number": 230, + "source": "src/powersync.ts", + "line": 739, + "position": 25710, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "onUnloadStarted = true", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:bool-flip:onUnloadStarted = true" + }, + { + "id": "powersync.ts:M231", + "number": 231, + "source": "src/powersync.ts", + "line": 742, + "position": 25793, + "operator": "value-undefined", + "original": "cleanup()", + "replacement": "undefined", + "anchor": "return cleanup ( )", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:value-undefined:return cleanup ( )" + }, + { + "id": "powersync.ts:M232", + "number": 232, + "source": "src/powersync.ts", + "line": 746, + "position": 25853, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })" + }, + { + "id": "powersync.ts:M233", + "number": 233, + "source": "src/powersync.ts", + "line": 747, + "position": 25887, + "operator": "field-drop", + "original": "level: LogLevels.info", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })" + }, + { + "id": "powersync.ts:M234", + "number": 234, + "source": "src/powersync.ts", + "line": 748, + "position": 25922, + "operator": "field-drop", + "original": "message: `Sync has been stopped for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })~2" + }, + { + "id": "powersync.ts:M235", + "number": 235, + "source": "src/powersync.ts", + "line": 750, + "position": 26020, + "operator": "delete-call", + "original": "abortController.abort()", + "replacement": ";", + "anchor": "abortController . abort ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:abortController . abort ( )~2" + }, + { + "id": "powersync.ts:M236", + "number": 236, + "source": "src/powersync.ts", + "line": 751, + "position": 26079, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "const trackingDisposal = disposeTrackingAfterAbort ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const trackingDisposal = disposeTrackingAfterAbort ( )" + }, + { + "id": "powersync.ts:M237", + "number": 237, + "source": "src/powersync.ts", + "line": 756, + "position": 26255, + "operator": "value-undefined", + "original": "invokeOnUnload()", + "replacement": "undefined", + "anchor": "await invokeOnUnload ( )", + "function": "settleOnUnload", + "key": "src/powersync.ts#settleOnUnload:value-undefined:await invokeOnUnload ( )" + }, + { + "id": "powersync.ts:M238", + "number": 238, + "source": "src/powersync.ts", + "line": 758, + "position": 26338, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "onUnloadFailure ??= { error }", + "function": "settleOnUnload", + "key": "src/powersync.ts#settleOnUnload:field-drop:onUnloadFailure ??= { error }" + }, + { + "id": "powersync.ts:M239", + "number": 239, + "source": "src/powersync.ts", + "line": 764, + "position": 26569, + "operator": "value-undefined", + "original": "Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])", + "replacement": "undefined", + "anchor": "const settlements = await Promise . allSettled ( [ startup , trackingDisposal , settleOnUnload ( ) , ] )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const settlements = await Promise . allSettled ( [ startup , trackingDisposal , settleOnUnload ( ) , ] )" + }, + { + "id": "powersync.ts:M240", + "number": 240, + "source": "src/powersync.ts", + "line": 767, + "position": 26653, + "operator": "value-undefined", + "original": "settleOnUnload()", + "replacement": "undefined", + "anchor": "const settlements = await Promise . allSettled ( [ startup , trackingDisposal , settleOnUnload ( ) , ] )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const settlements = await Promise . allSettled ( [ startup , trackingDisposal , settleOnUnload ( ) , ] )~2" + }, + { + "id": "powersync.ts:M241", + "number": 241, + "source": "src/powersync.ts", + "line": 770, + "position": 26752, + "operator": "value-undefined", + "original": "settleOnUnload()", + "replacement": "undefined", + "anchor": "await settleOnUnload ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await settleOnUnload ( )" + }, + { + "id": "powersync.ts:M242", + "number": 242, + "source": "src/powersync.ts", + "line": 771, + "position": 26783, + "operator": "if-true", + "original": "disposalSettlement.status === `rejected` && onUnloadFailure", + "replacement": "true", + "anchor": "if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }" + }, + { + "id": "powersync.ts:M243", + "number": 243, + "source": "src/powersync.ts", + "line": 771, + "position": 26783, + "operator": "if-false", + "original": "disposalSettlement.status === `rejected` && onUnloadFailure", + "replacement": "false", + "anchor": "if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }" + }, + { + "id": "powersync.ts:M244", + "number": 244, + "source": "src/powersync.ts", + "line": 771, + "position": 26824, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }" + }, + { + "id": "powersync.ts:M245", + "number": 245, + "source": "src/powersync.ts", + "line": 771, + "position": 26809, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }~2" + }, + { + "id": "powersync.ts:M246", + "number": 246, + "source": "src/powersync.ts", + "line": 775, + "position": 27045, + "operator": "field-drop", + "original": "cause: disposalSettlement.reason", + "replacement": "", + "anchor": "throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , )" + }, + { + "id": "powersync.ts:M247", + "number": 247, + "source": "src/powersync.ts", + "line": 778, + "position": 27121, + "operator": "if-true", + "original": "disposalSettlement.status === `rejected`", + "replacement": "true", + "anchor": "if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }" + }, + { + "id": "powersync.ts:M248", + "number": 248, + "source": "src/powersync.ts", + "line": 778, + "position": 27121, + "operator": "if-false", + "original": "disposalSettlement.status === `rejected`", + "replacement": "false", + "anchor": "if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }" + }, + { + "id": "powersync.ts:M249", + "number": 249, + "source": "src/powersync.ts", + "line": 778, + "position": 27147, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }" + }, + { + "id": "powersync.ts:M250", + "number": 250, + "source": "src/powersync.ts", + "line": 781, + "position": 27235, + "operator": "if-true", + "original": "onUnloadFailure", + "replacement": "true", + "anchor": "if ( onUnloadFailure ) throw onUnloadFailure . error", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( onUnloadFailure ) throw onUnloadFailure . error" + }, + { + "id": "powersync.ts:M251", + "number": 251, + "source": "src/powersync.ts", + "line": 781, + "position": 27235, + "operator": "if-false", + "original": "onUnloadFailure", + "replacement": "false", + "anchor": "if ( onUnloadFailure ) throw onUnloadFailure . error", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( onUnloadFailure ) throw onUnloadFailure . error" + }, + { + "id": "powersync.ts:M252", + "number": 252, + "source": "src/powersync.ts", + "line": 817, + "position": 28419, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let stopped = false", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:bool-flip:let stopped = false" + }, + { + "id": "powersync.ts:M253", + "number": 253, + "source": "src/powersync.ts", + "line": 821, + "position": 28588, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let drainingReleases = false", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:bool-flip:let drainingReleases = false" + }, + { + "id": "powersync.ts:M254", + "number": 254, + "source": "src/powersync.ts", + "line": 824, + "position": 28764, + "operator": "value-undefined", + "original": "start()", + "replacement": "undefined", + "anchor": "const startup = start ( )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:value-undefined:const startup = start ( )" + }, + { + "id": "powersync.ts:M255", + "number": 255, + "source": "src/powersync.ts", + "line": 825, + "position": 28785, + "operator": "value-undefined", + "original": "startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n }),\n )", + "replacement": "undefined", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:value-undefined:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )" + }, + { + "id": "powersync.ts:M256", + "number": 256, + "source": "src/powersync.ts", + "line": 826, + "position": 28820, + "operator": "value-undefined", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n })", + "replacement": "undefined", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:value-undefined:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )~2" + }, + { + "id": "powersync.ts:M257", + "number": 257, + "source": "src/powersync.ts", + "line": 827, + "position": 28854, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )" + }, + { + "id": "powersync.ts:M258", + "number": 258, + "source": "src/powersync.ts", + "line": 828, + "position": 28890, + "operator": "field-drop", + "original": "message: `Could not start syncing process for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )~2" + }, + { + "id": "powersync.ts:M259", + "number": 259, + "source": "src/powersync.ts", + "line": 829, + "position": 28987, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )~3" + }, + { + "id": "powersync.ts:M260", + "number": 260, + "source": "src/powersync.ts", + "line": 834, + "position": 29074, + "operator": "value-undefined", + "original": "Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)", + "replacement": "undefined", + "anchor": "const activeWhereExpressions = ( ) => Array . from ( demands . values ( ) ) . filter ( ( demand ) => demand . active ) . map ( ( demand ) => demand . options . where )", + "function": "activeWhereExpressions", + "key": "src/powersync.ts#activeWhereExpressions:value-undefined:const activeWhereExpressions = ( ) => Array . from ( demands . values ( ) ) . filter ( ( demand ) => demand . active ) . map ( ( demand ) => demand . options . where )" + }, + { + "id": "powersync.ts:M261", + "number": 261, + "source": "src/powersync.ts", + "line": 834, + "position": 29085, + "operator": "value-undefined", + "original": "demands.values()", + "replacement": "undefined", + "anchor": "const activeWhereExpressions = ( ) => Array . from ( demands . values ( ) ) . filter ( ( demand ) => demand . active ) . map ( ( demand ) => demand . options . where )", + "function": "activeWhereExpressions", + "key": "src/powersync.ts#activeWhereExpressions:value-undefined:const activeWhereExpressions = ( ) => Array . from ( demands . values ( ) ) . filter ( ( demand ) => demand . active ) . map ( ( demand ) => demand . options . where )~2" + }, + { + "id": "powersync.ts:M262", + "number": 262, + "source": "src/powersync.ts", + "line": 841, + "position": 29437, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let trackingOutage = false", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:bool-flip:let trackingOutage = false" + }, + { + "id": "powersync.ts:M263", + "number": 263, + "source": "src/powersync.ts", + "line": 843, + "position": 29523, + "operator": "loop-false", + "original": "!stopped && reconciledTrackingRevision !== trackingRevision", + "replacement": "false", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:loop-false:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }" + }, + { + "id": "powersync.ts:M264", + "number": 264, + "source": "src/powersync.ts", + "line": 843, + "position": 29532, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:binary-swap:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }" + }, + { + "id": "powersync.ts:M265", + "number": 265, + "source": "src/powersync.ts", + "line": 843, + "position": 29523, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }" + }, + { + "id": "powersync.ts:M266", + "number": 266, + "source": "src/powersync.ts", + "line": 843, + "position": 29562, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:binary-swap:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }~2" + }, + { + "id": "powersync.ts:M267", + "number": 267, + "source": "src/powersync.ts", + "line": 845, + "position": 29677, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const isCurrent = ( ) => ! stopped && trackingRevision === revision", + "function": "isCurrent", + "key": "src/powersync.ts#isCurrent:binary-swap:const isCurrent = ( ) => ! stopped && trackingRevision === revision" + }, + { + "id": "powersync.ts:M268", + "number": 268, + "source": "src/powersync.ts", + "line": 845, + "position": 29668, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const isCurrent = ( ) => ! stopped && trackingRevision === revision", + "function": "isCurrent", + "key": "src/powersync.ts#isCurrent:drop-not:const isCurrent = ( ) => ! stopped && trackingRevision === revision" + }, + { + "id": "powersync.ts:M269", + "number": 269, + "source": "src/powersync.ts", + "line": 845, + "position": 29697, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const isCurrent = ( ) => ! stopped && trackingRevision === revision", + "function": "isCurrent", + "key": "src/powersync.ts#isCurrent:binary-swap:const isCurrent = ( ) => ! stopped && trackingRevision === revision~2" + }, + { + "id": "powersync.ts:M270", + "number": 270, + "source": "src/powersync.ts", + "line": 848, + "position": 29795, + "operator": "value-undefined", + "original": "database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: `delete`, key })\n appliedReceipts.push(commit())\n }\n })", + "replacement": "undefined", + "anchor": "await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })" + }, + { + "id": "powersync.ts:M271", + "number": 271, + "source": "src/powersync.ts", + "line": 849, + "position": 29849, + "operator": "if-true", + "original": "!isCurrent()", + "replacement": "true", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( ! isCurrent ( ) ) return" + }, + { + "id": "powersync.ts:M272", + "number": 272, + "source": "src/powersync.ts", + "line": 849, + "position": 29849, + "operator": "if-false", + "original": "!isCurrent()", + "replacement": "false", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( ! isCurrent ( ) ) return" + }, + { + "id": "powersync.ts:M273", + "number": 273, + "source": "src/powersync.ts", + "line": 849, + "position": 29849, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:if ( ! isCurrent ( ) ) return" + }, + { + "id": "powersync.ts:M274", + "number": 274, + "source": "src/powersync.ts", + "line": 849, + "position": 29850, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( ! isCurrent ( ) ) return" + }, + { + "id": "powersync.ts:M275", + "number": 275, + "source": "src/powersync.ts", + "line": 850, + "position": 29890, + "operator": "value-undefined", + "original": "flushDiffRecordsWithContext(ctx, appliedReceipts)", + "replacement": "undefined", + "anchor": "await flushDiffRecordsWithContext ( ctx , appliedReceipts )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await flushDiffRecordsWithContext ( ctx , appliedReceipts )" + }, + { + "id": "powersync.ts:M276", + "number": 276, + "source": "src/powersync.ts", + "line": 851, + "position": 29958, + "operator": "if-true", + "original": "!isCurrent()", + "replacement": "true", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( ! isCurrent ( ) ) return~2" + }, + { + "id": "powersync.ts:M277", + "number": 277, + "source": "src/powersync.ts", + "line": 851, + "position": 29958, + "operator": "if-false", + "original": "!isCurrent()", + "replacement": "false", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( ! isCurrent ( ) ) return~2" + }, + { + "id": "powersync.ts:M278", + "number": 278, + "source": "src/powersync.ts", + "line": 851, + "position": 29958, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:if ( ! isCurrent ( ) ) return~2" + }, + { + "id": "powersync.ts:M279", + "number": 279, + "source": "src/powersync.ts", + "line": 851, + "position": 29959, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( ! isCurrent ( ) ) return~2" + }, + { + "id": "powersync.ts:M280", + "number": 280, + "source": "src/powersync.ts", + "line": 852, + "position": 29999, + "operator": "value-undefined", + "original": "safelyDisposeTracking(ctx)", + "replacement": "undefined", + "anchor": "await safelyDisposeTracking ( ctx )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await safelyDisposeTracking ( ctx )" + }, + { + "id": "powersync.ts:M281", + "number": 281, + "source": "src/powersync.ts", + "line": 853, + "position": 30044, + "operator": "if-true", + "original": "!isCurrent()", + "replacement": "true", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( ! isCurrent ( ) ) return~3" + }, + { + "id": "powersync.ts:M282", + "number": 282, + "source": "src/powersync.ts", + "line": 853, + "position": 30044, + "operator": "if-false", + "original": "!isCurrent()", + "replacement": "false", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( ! isCurrent ( ) ) return~3" + }, + { + "id": "powersync.ts:M283", + "number": 283, + "source": "src/powersync.ts", + "line": 853, + "position": 30044, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:if ( ! isCurrent ( ) ) return~3" + }, + { + "id": "powersync.ts:M284", + "number": 284, + "source": "src/powersync.ts", + "line": 853, + "position": 30045, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( ! isCurrent ( ) ) return~3" + }, + { + "id": "powersync.ts:M285", + "number": 285, + "source": "src/powersync.ts", + "line": 855, + "position": 30095, + "operator": "value-undefined", + "original": "activeWhereExpressions()", + "replacement": "undefined", + "anchor": "const active = activeWhereExpressions ( )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const active = activeWhereExpressions ( )" + }, + { + "id": "powersync.ts:M286", + "number": 286, + "source": "src/powersync.ts", + "line": 859, + "position": 30348, + "operator": "value-undefined", + "original": "collection.keys()", + "replacement": "undefined", + "anchor": "const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined" + }, + { + "id": "powersync.ts:M287", + "number": 287, + "source": "src/powersync.ts", + "line": 861, + "position": 30413, + "operator": "if-true", + "original": "active.length > 0", + "replacement": "true", + "anchor": "if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }" + }, + { + "id": "powersync.ts:M288", + "number": 288, + "source": "src/powersync.ts", + "line": 861, + "position": 30413, + "operator": "if-false", + "original": "active.length > 0", + "replacement": "false", + "anchor": "if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }" + }, + { + "id": "powersync.ts:M289", + "number": 289, + "source": "src/powersync.ts", + "line": 861, + "position": 30427, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:binary-swap:if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }" + }, + { + "id": "powersync.ts:M290", + "number": 290, + "source": "src/powersync.ts", + "line": 863, + "position": 30504, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:binary-swap:const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )" + }, + { + "id": "powersync.ts:M291", + "number": 291, + "source": "src/powersync.ts", + "line": 865, + "position": 30564, + "operator": "value-undefined", + "original": "or(active[0], active[1], ...active.slice(2))", + "replacement": "undefined", + "anchor": "const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )" + }, + { + "id": "powersync.ts:M292", + "number": 292, + "source": "src/powersync.ts", + "line": 865, + "position": 30592, + "operator": "value-undefined", + "original": "active.slice(2)", + "replacement": "undefined", + "anchor": "const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )~2" + }, + { + "id": "powersync.ts:M293", + "number": 293, + "source": "src/powersync.ts", + "line": 866, + "position": 30649, + "operator": "value-undefined", + "original": "compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )", + "replacement": "undefined", + "anchor": "const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )" + }, + { + "id": "powersync.ts:M294", + "number": 294, + "source": "src/powersync.ts", + "line": 867, + "position": 30684, + "operator": "field-drop", + "original": "where: combinedWhere", + "replacement": "", + "anchor": "const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )" + }, + { + "id": "powersync.ts:M295", + "number": 295, + "source": "src/powersync.ts", + "line": 868, + "position": 30728, + "operator": "field-drop", + "original": "jsonColumn: 'NEW.data'", + "replacement": "", + "anchor": "const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )~2" + }, + { + "id": "powersync.ts:M296", + "number": 296, + "source": "src/powersync.ts", + "line": 870, + "position": 30812, + "operator": "value-undefined", + "original": "compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )", + "replacement": "undefined", + "anchor": "const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )" + }, + { + "id": "powersync.ts:M297", + "number": 297, + "source": "src/powersync.ts", + "line": 871, + "position": 30847, + "operator": "field-drop", + "original": "where: combinedWhere", + "replacement": "", + "anchor": "const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )" + }, + { + "id": "powersync.ts:M298", + "number": 298, + "source": "src/powersync.ts", + "line": 872, + "position": 30891, + "operator": "field-drop", + "original": "jsonColumn: 'OLD.data'", + "replacement": "", + "anchor": "const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )~2" + }, + { + "id": "powersync.ts:M299", + "number": 299, + "source": "src/powersync.ts", + "line": 874, + "position": 30972, + "operator": "value-undefined", + "original": "compileSQLite({ where: combinedWhere })", + "replacement": "undefined", + "anchor": "const compiledView = compileSQLite ( { where : combinedWhere } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const compiledView = compileSQLite ( { where : combinedWhere } )" + }, + { + "id": "powersync.ts:M300", + "number": 300, + "source": "src/powersync.ts", + "line": 874, + "position": 30988, + "operator": "field-drop", + "original": "where: combinedWhere", + "replacement": "", + "anchor": "const compiledView = compileSQLite ( { where : combinedWhere } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledView = compileSQLite ( { where : combinedWhere } )" + }, + { + "id": "powersync.ts:M301", + "number": 301, + "source": "src/powersync.ts", + "line": 875, + "position": 31054, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledNewData)", + "replacement": "undefined", + "anchor": "const newDataWhenClause = toInlinedWhereClause ( compiledNewData )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const newDataWhenClause = toInlinedWhereClause ( compiledNewData )" + }, + { + "id": "powersync.ts:M302", + "number": 302, + "source": "src/powersync.ts", + "line": 876, + "position": 31134, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledOldData)", + "replacement": "undefined", + "anchor": "const oldDataWhenClause = toInlinedWhereClause ( compiledOldData )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const oldDataWhenClause = toInlinedWhereClause ( compiledOldData )" + }, + { + "id": "powersync.ts:M303", + "number": 303, + "source": "src/powersync.ts", + "line": 877, + "position": 31212, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledView)", + "replacement": "undefined", + "anchor": "const viewWhereClause = toInlinedWhereClause ( compiledView )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const viewWhereClause = toInlinedWhereClause ( compiledView )" + }, + { + "id": "powersync.ts:M304", + "number": 304, + "source": "src/powersync.ts", + "line": 878, + "position": 31269, + "operator": "value-undefined", + "original": "establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "replacement": "undefined", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M305", + "number": 305, + "source": "src/powersync.ts", + "line": 880, + "position": 31328, + "operator": "field-drop", + "original": "setupContext: ctx", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M306", + "number": 306, + "source": "src/powersync.ts", + "line": 881, + "position": 31367, + "operator": "field-drop", + "original": "when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n }", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )~2" + }, + { + "id": "powersync.ts:M307", + "number": 307, + "source": "src/powersync.ts", + "line": 882, + "position": 31397, + "operator": "field-drop", + "original": "[DiffTriggerOperation.INSERT]: newDataWhenClause", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )~3" + }, + { + "id": "powersync.ts:M308", + "number": 308, + "source": "src/powersync.ts", + "line": 883, + "position": 31469, + "operator": "field-drop", + "original": "[DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )~4" + }, + { + "id": "powersync.ts:M309", + "number": 309, + "source": "src/powersync.ts", + "line": 884, + "position": 31574, + "operator": "field-drop", + "original": "[DiffTriggerOperation.DELETE]: oldDataWhenClause", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )~5" + }, + { + "id": "powersync.ts:M310", + "number": 310, + "source": "src/powersync.ts", + "line": 886, + "position": 31667, + "operator": "field-drop", + "original": "writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "writeType", + "key": "src/powersync.ts#writeType:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M311", + "number": 311, + "source": "src/powersync.ts", + "line": 887, + "position": 31719, + "operator": "value-undefined", + "original": "collection.has(rowId)", + "replacement": "undefined", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "writeType", + "key": "src/powersync.ts#writeType:value-undefined:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M312", + "number": 312, + "source": "src/powersync.ts", + "line": 888, + "position": 31784, + "operator": "field-drop", + "original": "batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n })", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M313", + "number": 313, + "source": "src/powersync.ts", + "line": 893, + "position": 31972, + "operator": "value-undefined", + "original": "lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n })", + "replacement": "undefined", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:value-undefined:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + { + "id": "powersync.ts:M314", + "number": 314, + "source": "src/powersync.ts", + "line": 899, + "position": 32290, + "operator": "delete-call", + "original": "missing?.delete(row.id)", + "replacement": ";", + "anchor": "missing ?. delete ( row . id )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:delete-call:missing ?. delete ( row . id )" + }, + { + "id": "powersync.ts:M315", + "number": 315, + "source": "src/powersync.ts", + "line": 906, + "position": 32488, + "operator": "if-true", + "original": "!isCurrent()", + "replacement": "true", + "anchor": "if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + { + "id": "powersync.ts:M316", + "number": 316, + "source": "src/powersync.ts", + "line": 906, + "position": 32488, + "operator": "if-false", + "original": "!isCurrent()", + "replacement": "false", + "anchor": "if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + { + "id": "powersync.ts:M317", + "number": 317, + "source": "src/powersync.ts", + "line": 906, + "position": 32488, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + { + "id": "powersync.ts:M318", + "number": 318, + "source": "src/powersync.ts", + "line": 906, + "position": 32489, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + { + "id": "powersync.ts:M319", + "number": 319, + "source": "src/powersync.ts", + "line": 906, + "position": 32508, + "operator": "value-undefined", + "original": "safelyDisposeTracking(ctx)", + "replacement": "undefined", + "anchor": "await safelyDisposeTracking ( ctx )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await safelyDisposeTracking ( ctx )~2" + }, + { + "id": "powersync.ts:M320", + "number": 320, + "source": "src/powersync.ts", + "line": 907, + "position": 32558, + "operator": "if-true", + "original": "missing?.size", + "replacement": "true", + "anchor": "if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + { + "id": "powersync.ts:M321", + "number": 321, + "source": "src/powersync.ts", + "line": 907, + "position": 32558, + "operator": "if-false", + "original": "missing?.size", + "replacement": "false", + "anchor": "if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + { + "id": "powersync.ts:M322", + "number": 322, + "source": "src/powersync.ts", + "line": 908, + "position": 32591, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:delete-call:begin ( )" + }, + { + "id": "powersync.ts:M323", + "number": 323, + "source": "src/powersync.ts", + "line": 909, + "position": 32642, + "operator": "delete-call", + "original": "write({ type: `delete`, key })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:delete-call:write ( { type : `delete` , key } )" + }, + { + "id": "powersync.ts:M324", + "number": 324, + "source": "src/powersync.ts", + "line": 909, + "position": 32650, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:write ( { type : `delete` , key } )" + }, + { + "id": "powersync.ts:M325", + "number": 325, + "source": "src/powersync.ts", + "line": 909, + "position": 32666, + "operator": "field-drop", + "original": "key", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:write ( { type : `delete` , key } )~2" + }, + { + "id": "powersync.ts:M326", + "number": 326, + "source": "src/powersync.ts", + "line": 910, + "position": 32689, + "operator": "delete-call", + "original": "appliedReceipts.push(commit())", + "replacement": ";", + "anchor": "appliedReceipts . push ( commit ( ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:delete-call:appliedReceipts . push ( commit ( ) )" + }, + { + "id": "powersync.ts:M327", + "number": 327, + "source": "src/powersync.ts", + "line": 910, + "position": 32710, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "appliedReceipts . push ( commit ( ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:appliedReceipts . push ( commit ( ) )" + }, + { + "id": "powersync.ts:M328", + "number": 328, + "source": "src/powersync.ts", + "line": 913, + "position": 32769, + "operator": "value-undefined", + "original": "Promise.all(appliedReceipts)", + "replacement": "undefined", + "anchor": "await Promise . all ( appliedReceipts )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await Promise . all ( appliedReceipts )" + }, + { + "id": "powersync.ts:M329", + "number": 329, + "source": "src/powersync.ts", + "line": 914, + "position": 32814, + "operator": "if-true", + "original": "isCurrent()", + "replacement": "true", + "anchor": "if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }" + }, + { + "id": "powersync.ts:M330", + "number": 330, + "source": "src/powersync.ts", + "line": 914, + "position": 32814, + "operator": "if-false", + "original": "isCurrent()", + "replacement": "false", + "anchor": "if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }" + }, + { + "id": "powersync.ts:M331", + "number": 331, + "source": "src/powersync.ts", + "line": 914, + "position": 32814, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }" + }, + { + "id": "powersync.ts:M332", + "number": 332, + "source": "src/powersync.ts", + "line": 918, + "position": 33056, + "operator": "if-true", + "original": "trackingOutage", + "replacement": "true", + "anchor": "if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) }" + }, + { + "id": "powersync.ts:M333", + "number": 333, + "source": "src/powersync.ts", + "line": 918, + "position": 33056, + "operator": "if-false", + "original": "trackingOutage", + "replacement": "false", + "anchor": "if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) }" + }, + { + "id": "powersync.ts:M334", + "number": 334, + "source": "src/powersync.ts", + "line": 919, + "position": 33107, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "trackingOutage = false", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:bool-flip:trackingOutage = false" + }, + { + "id": "powersync.ts:M335", + "number": 335, + "source": "src/powersync.ts", + "line": 920, + "position": 33129, + "operator": "delete-call", + "original": "reportHealthy()", + "replacement": ";", + "anchor": "reportHealthy ( )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:delete-call:reportHealthy ( )" + }, + { + "id": "powersync.ts:M336", + "number": 336, + "source": "src/powersync.ts", + "line": 929, + "position": 33430, + "operator": "loop-false", + "original": "!stopped && reconciledTrackingRevision !== trackingRevision", + "replacement": "false", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:loop-false:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }" + }, + { + "id": "powersync.ts:M337", + "number": 337, + "source": "src/powersync.ts", + "line": 929, + "position": 33439, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:binary-swap:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }" + }, + { + "id": "powersync.ts:M338", + "number": 338, + "source": "src/powersync.ts", + "line": 929, + "position": 33430, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:drop-not:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }" + }, + { + "id": "powersync.ts:M339", + "number": 339, + "source": "src/powersync.ts", + "line": 929, + "position": 33469, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:binary-swap:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }~2" + }, + { + "id": "powersync.ts:M340", + "number": 340, + "source": "src/powersync.ts", + "line": 930, + "position": 33531, + "operator": "value-undefined", + "original": "reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n })", + "replacement": "undefined", + "anchor": "await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:value-undefined:await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) )" + }, + { + "id": "powersync.ts:M341", + "number": 341, + "source": "src/powersync.ts", + "line": 935, + "position": 33844, + "operator": "if-true", + "original": "stopped", + "replacement": "true", + "anchor": "if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:if-true:if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )" + }, + { + "id": "powersync.ts:M342", + "number": 342, + "source": "src/powersync.ts", + "line": 935, + "position": 33844, + "operator": "if-false", + "original": "stopped", + "replacement": "false", + "anchor": "if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:if-false:if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )" + }, + { + "id": "powersync.ts:M343", + "number": 343, + "source": "src/powersync.ts", + "line": 937, + "position": 33929, + "operator": "if-true", + "original": "ready", + "replacement": "true", + "anchor": "if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:if-true:if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )" + }, + { + "id": "powersync.ts:M344", + "number": 344, + "source": "src/powersync.ts", + "line": 937, + "position": 33929, + "operator": "if-false", + "original": "ready", + "replacement": "false", + "anchor": "if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:if-false:if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )" + }, + { + "id": "powersync.ts:M345", + "number": 345, + "source": "src/powersync.ts", + "line": 938, + "position": 33973, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "trackingOutage = true", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:bool-flip:trackingOutage = true" + }, + { + "id": "powersync.ts:M346", + "number": 346, + "source": "src/powersync.ts", + "line": 939, + "position": 33996, + "operator": "delete-call", + "original": "reportError(error)", + "replacement": ";", + "anchor": "reportError ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:delete-call:reportError ( error )" + }, + { + "id": "powersync.ts:M347", + "number": 347, + "source": "src/powersync.ts", + "line": 940, + "position": 34038, + "operator": "delete-call", + "original": "fail(error)", + "replacement": ";", + "anchor": "fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:delete-call:fail ( error )" + }, + { + "id": "powersync.ts:M348", + "number": 348, + "source": "src/powersync.ts", + "line": 952, + "position": 34322, + "operator": "if-true", + "original": "stopped", + "replacement": "true", + "anchor": "if ( stopped ) return", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( stopped ) return" + }, + { + "id": "powersync.ts:M349", + "number": 349, + "source": "src/powersync.ts", + "line": 952, + "position": 34322, + "operator": "if-false", + "original": "stopped", + "replacement": "false", + "anchor": "if ( stopped ) return", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( stopped ) return" + }, + { + "id": "powersync.ts:M350", + "number": 350, + "source": "src/powersync.ts", + "line": 958, + "position": 34612, + "operator": "if-true", + "original": "stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted", + "replacement": "true", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }" + }, + { + "id": "powersync.ts:M351", + "number": 351, + "source": "src/powersync.ts", + "line": 958, + "position": 34612, + "operator": "if-false", + "original": "stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted", + "replacement": "false", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }" + }, + { + "id": "powersync.ts:M352", + "number": 352, + "source": "src/powersync.ts", + "line": 959, + "position": 34664, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }" + }, + { + "id": "powersync.ts:M353", + "number": 353, + "source": "src/powersync.ts", + "line": 958, + "position": 34620, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }~2" + }, + { + "id": "powersync.ts:M354", + "number": 354, + "source": "src/powersync.ts", + "line": 959, + "position": 34635, + "operator": "value-undefined", + "original": "releasedSubsets.has(options)", + "replacement": "undefined", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }" + }, + { + "id": "powersync.ts:M355", + "number": 355, + "source": "src/powersync.ts", + "line": 965, + "position": 34790, + "operator": "field-drop", + "original": "options", + "replacement": "", + "anchor": "const demand : DemandRecord = { options , active : false }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:field-drop:const demand : DemandRecord = { options , active : false }" + }, + { + "id": "powersync.ts:M356", + "number": 356, + "source": "src/powersync.ts", + "line": 965, + "position": 34799, + "operator": "field-drop", + "original": "active: false", + "replacement": "", + "anchor": "const demand : DemandRecord = { options , active : false }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:field-drop:const demand : DemandRecord = { options , active : false }~2" + }, + { + "id": "powersync.ts:M357", + "number": 357, + "source": "src/powersync.ts", + "line": 965, + "position": 34807, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "const demand : DemandRecord = { options , active : false }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:bool-flip:const demand : DemandRecord = { options , active : false }" + }, + { + "id": "powersync.ts:M358", + "number": 358, + "source": "src/powersync.ts", + "line": 966, + "position": 34825, + "operator": "delete-call", + "original": "demands.set(options, demand)", + "replacement": ";", + "anchor": "demands . set ( options , demand )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:delete-call:demands . set ( options , demand )" + }, + { + "id": "powersync.ts:M359", + "number": 359, + "source": "src/powersync.ts", + "line": 968, + "position": 34904, + "operator": "value-undefined", + "original": "restConfig.onLoadSubset?.(options)", + "replacement": "undefined", + "anchor": "const cleanup = await restConfig . onLoadSubset ?. ( options )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:const cleanup = await restConfig . onLoadSubset ?. ( options )" + }, + { + "id": "powersync.ts:M360", + "number": 360, + "source": "src/powersync.ts", + "line": 969, + "position": 34955, + "operator": "if-true", + "original": "cleanup", + "replacement": "true", + "anchor": "if ( cleanup ) demand . cleanup = cleanup", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( cleanup ) demand . cleanup = cleanup" + }, + { + "id": "powersync.ts:M361", + "number": 361, + "source": "src/powersync.ts", + "line": 969, + "position": 34955, + "operator": "if-false", + "original": "cleanup", + "replacement": "false", + "anchor": "if ( cleanup ) demand . cleanup = cleanup", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( cleanup ) demand . cleanup = cleanup" + }, + { + "id": "powersync.ts:M362", + "number": 362, + "source": "src/powersync.ts", + "line": 971, + "position": 35029, + "operator": "delete-call", + "original": "demands.delete(options)", + "replacement": ";", + "anchor": "demands . delete ( options )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:delete-call:demands . delete ( options )" + }, + { + "id": "powersync.ts:M363", + "number": 363, + "source": "src/powersync.ts", + "line": 973, + "position": 35122, + "operator": "if-true", + "original": "collector", + "replacement": "true", + "anchor": "if ( collector ) { collector . failures . push ( { order : collector . nextOrder ++ , error , } ) }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( collector ) { collector . failures . push ( { order : collector . nextOrder ++ , error , } ) }" + }, + { + "id": "powersync.ts:M364", + "number": 364, + "source": "src/powersync.ts", + "line": 973, + "position": 35122, + "operator": "if-false", + "original": "collector", + "replacement": "false", + "anchor": "if ( collector ) { collector . failures . push ( { order : collector . nextOrder ++ , error , } ) }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( collector ) { collector . failures . push ( { order : collector . nextOrder ++ , error , } ) }" + }, + { + "id": "powersync.ts:M365", + "number": 365, + "source": "src/powersync.ts", + "line": 974, + "position": 35149, + "operator": "delete-call", + "original": "collector.failures.push({\n order: collector.nextOrder++,\n error,\n })", + "replacement": ";", + "anchor": "collector . failures . push ( { order : collector . nextOrder ++ , error , } )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:delete-call:collector . failures . push ( { order : collector . nextOrder ++ , error , } )" + }, + { + "id": "powersync.ts:M366", + "number": 366, + "source": "src/powersync.ts", + "line": 975, + "position": 35191, + "operator": "field-drop", + "original": "order: collector.nextOrder++", + "replacement": "", + "anchor": "collector . failures . push ( { order : collector . nextOrder ++ , error , } )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:field-drop:collector . failures . push ( { order : collector . nextOrder ++ , error , } )" + }, + { + "id": "powersync.ts:M367", + "number": 367, + "source": "src/powersync.ts", + "line": 976, + "position": 35237, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "collector . failures . push ( { order : collector . nextOrder ++ , error , } )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:field-drop:collector . failures . push ( { order : collector . nextOrder ++ , error , } )~2" + }, + { + "id": "powersync.ts:M368", + "number": 368, + "source": "src/powersync.ts", + "line": 985, + "position": 35473, + "operator": "if-true", + "original": "stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand", + "replacement": "true", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }" + }, + { + "id": "powersync.ts:M369", + "number": 369, + "source": "src/powersync.ts", + "line": 985, + "position": 35473, + "operator": "if-false", + "original": "stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand", + "replacement": "false", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }" + }, + { + "id": "powersync.ts:M370", + "number": 370, + "source": "src/powersync.ts", + "line": 987, + "position": 35564, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }" + }, + { + "id": "powersync.ts:M371", + "number": 371, + "source": "src/powersync.ts", + "line": 986, + "position": 35525, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }~2" + }, + { + "id": "powersync.ts:M372", + "number": 372, + "source": "src/powersync.ts", + "line": 985, + "position": 35481, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }~3" + }, + { + "id": "powersync.ts:M373", + "number": 373, + "source": "src/powersync.ts", + "line": 986, + "position": 35496, + "operator": "value-undefined", + "original": "releasedSubsets.has(options)", + "replacement": "undefined", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }" + }, + { + "id": "powersync.ts:M374", + "number": 374, + "source": "src/powersync.ts", + "line": 988, + "position": 35600, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }~4" + }, + { + "id": "powersync.ts:M375", + "number": 375, + "source": "src/powersync.ts", + "line": 988, + "position": 35579, + "operator": "value-undefined", + "original": "demands.get(options)", + "replacement": "undefined", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }~2" + }, + { + "id": "powersync.ts:M376", + "number": 376, + "source": "src/powersync.ts", + "line": 990, + "position": 35637, + "operator": "delete-call", + "original": "cleanupDemand(demand)", + "replacement": ";", + "anchor": "cleanupDemand ( demand )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:delete-call:cleanupDemand ( demand )" + }, + { + "id": "powersync.ts:M377", + "number": 377, + "source": "src/powersync.ts", + "line": 994, + "position": 35717, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "demand . active = true", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:bool-flip:demand . active = true" + }, + { + "id": "powersync.ts:M378", + "number": 378, + "source": "src/powersync.ts", + "line": 996, + "position": 35767, + "operator": "value-undefined", + "original": "rebuildTracking()", + "replacement": "undefined", + "anchor": "await rebuildTracking ( )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:await rebuildTracking ( )" + }, + { + "id": "powersync.ts:M379", + "number": 379, + "source": "src/powersync.ts", + "line": 1000, + "position": 35900, + "operator": "value-undefined", + "original": "loadSubset(options)", + "replacement": "undefined", + "anchor": "const task = loadSubset ( options )", + "function": "trackLoadSubset", + "key": "src/powersync.ts#trackLoadSubset:value-undefined:const task = loadSubset ( options )" + }, + { + "id": "powersync.ts:M380", + "number": 380, + "source": "src/powersync.ts", + "line": 1001, + "position": 35930, + "operator": "delete-call", + "original": "pendingDemandLoads.add(task)", + "replacement": ";", + "anchor": "pendingDemandLoads . add ( task )", + "function": "trackLoadSubset", + "key": "src/powersync.ts#trackLoadSubset:delete-call:pendingDemandLoads . add ( task )" + }, + { + "id": "powersync.ts:M381", + "number": 381, + "source": "src/powersync.ts", + "line": 1002, + "position": 35990, + "operator": "value-undefined", + "original": "pendingDemandLoads.delete(task)", + "replacement": "undefined", + "anchor": "const finish = ( ) => pendingDemandLoads . delete ( task )", + "function": "finish", + "key": "src/powersync.ts#finish:value-undefined:const finish = ( ) => pendingDemandLoads . delete ( task )" + }, + { + "id": "powersync.ts:M382", + "number": 382, + "source": "src/powersync.ts", + "line": 1003, + "position": 36037, + "operator": "value-undefined", + "original": "task.then(finish, finish)", + "replacement": "undefined", + "anchor": "void task . then ( finish , finish )", + "function": "trackLoadSubset", + "key": "src/powersync.ts#trackLoadSubset:value-undefined:void task . then ( finish , finish )" + }, + { + "id": "powersync.ts:M383", + "number": 383, + "source": "src/powersync.ts", + "line": 1011, + "position": 36242, + "operator": "if-true", + "original": "!compiled.where", + "replacement": "true", + "anchor": "if ( ! compiled . where ) return 'TRUE'", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:if-true:if ( ! compiled . where ) return 'TRUE'" + }, + { + "id": "powersync.ts:M384", + "number": 384, + "source": "src/powersync.ts", + "line": 1011, + "position": 36242, + "operator": "if-false", + "original": "!compiled.where", + "replacement": "false", + "anchor": "if ( ! compiled . where ) return 'TRUE'", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:if-false:if ( ! compiled . where ) return 'TRUE'" + }, + { + "id": "powersync.ts:M385", + "number": 385, + "source": "src/powersync.ts", + "line": 1011, + "position": 36242, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! compiled . where ) return 'TRUE'", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:drop-not:if ( ! compiled . where ) return 'TRUE'" + }, + { + "id": "powersync.ts:M386", + "number": 386, + "source": "src/powersync.ts", + "line": 1012, + "position": 36300, + "operator": "value-undefined", + "original": "compiled.where.split('?')", + "replacement": "undefined", + "anchor": "const sqlParts = compiled . where . split ( '?' )", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:value-undefined:const sqlParts = compiled . where . split ( '?' )" + }, + { + "id": "powersync.ts:M387", + "number": 387, + "source": "src/powersync.ts", + "line": 1013, + "position": 36343, + "operator": "value-undefined", + "original": "sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )", + "replacement": "undefined", + "anchor": "return sanitizeSQL ( sqlParts as unknown as TemplateStringsArray , ... compiled . params , )", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:value-undefined:return sanitizeSQL ( sqlParts as unknown as TemplateStringsArray , ... compiled . params , )" + }, + { + "id": "powersync.ts:M388", + "number": 388, + "source": "src/powersync.ts", + "line": 1020, + "position": 36549, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `Could not clean up subset hook for ${viewName}`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })", + "function": "reportDemandCleanupFailure", + "key": "src/powersync.ts#reportDemandCleanupFailure:delete-call:database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })" + }, + { + "id": "powersync.ts:M389", + "number": 389, + "source": "src/powersync.ts", + "line": 1021, + "position": 36583, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })", + "function": "reportDemandCleanupFailure", + "key": "src/powersync.ts#reportDemandCleanupFailure:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })" + }, + { + "id": "powersync.ts:M390", + "number": 390, + "source": "src/powersync.ts", + "line": 1022, + "position": 36619, + "operator": "field-drop", + "original": "message: `Could not clean up subset hook for ${viewName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })", + "function": "reportDemandCleanupFailure", + "key": "src/powersync.ts#reportDemandCleanupFailure:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })~2" + }, + { + "id": "powersync.ts:M391", + "number": 391, + "source": "src/powersync.ts", + "line": 1023, + "position": 36690, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })", + "function": "reportDemandCleanupFailure", + "key": "src/powersync.ts#reportDemandCleanupFailure:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })~3" + }, + { + "id": "powersync.ts:M392", + "number": 392, + "source": "src/powersync.ts", + "line": 1028, + "position": 36795, + "operator": "delete-call", + "original": "demands.delete(demand.options)", + "replacement": ";", + "anchor": "demands . delete ( demand . options )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:demands . delete ( demand . options )" + }, + { + "id": "powersync.ts:M393", + "number": 393, + "source": "src/powersync.ts", + "line": 1034, + "position": 37081, + "operator": "value-undefined", + "original": "cleanup?.()", + "replacement": "undefined", + "anchor": "const result : unknown = cleanup ?. ( )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:const result : unknown = cleanup ?. ( )" + }, + { + "id": "powersync.ts:M394", + "number": 394, + "source": "src/powersync.ts", + "line": 1035, + "position": 37109, + "operator": "if-true", + "original": "!isPromiseLike(result)", + "replacement": "true", + "anchor": "if ( ! isPromiseLike ( result ) ) return", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-true:if ( ! isPromiseLike ( result ) ) return" + }, + { + "id": "powersync.ts:M395", + "number": 395, + "source": "src/powersync.ts", + "line": 1035, + "position": 37109, + "operator": "if-false", + "original": "!isPromiseLike(result)", + "replacement": "false", + "anchor": "if ( ! isPromiseLike ( result ) ) return", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-false:if ( ! isPromiseLike ( result ) ) return" + }, + { + "id": "powersync.ts:M396", + "number": 396, + "source": "src/powersync.ts", + "line": 1035, + "position": 37109, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isPromiseLike ( result ) ) return", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:drop-not:if ( ! isPromiseLike ( result ) ) return" + }, + { + "id": "powersync.ts:M397", + "number": 397, + "source": "src/powersync.ts", + "line": 1035, + "position": 37110, + "operator": "value-undefined", + "original": "isPromiseLike(result)", + "replacement": "undefined", + "anchor": "if ( ! isPromiseLike ( result ) ) return", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:if ( ! isPromiseLike ( result ) ) return" + }, + { + "id": "powersync.ts:M398", + "number": 398, + "source": "src/powersync.ts", + "line": 1037, + "position": 37166, + "operator": "value-undefined", + "original": "Promise.resolve(result)", + "replacement": "undefined", + "anchor": "const task = Promise . resolve ( result )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:const task = Promise . resolve ( result )" + }, + { + "id": "powersync.ts:M399", + "number": 399, + "source": "src/powersync.ts", + "line": 1038, + "position": 37202, + "operator": "delete-call", + "original": "pendingDemandCleanups.add(task)", + "replacement": ";", + "anchor": "pendingDemandCleanups . add ( task )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:pendingDemandCleanups . add ( task )" + }, + { + "id": "powersync.ts:M400", + "number": 400, + "source": "src/powersync.ts", + "line": 1039, + "position": 37267, + "operator": "value-undefined", + "original": "pendingDemandCleanups.delete(task)", + "replacement": "undefined", + "anchor": "const finish = ( ) => pendingDemandCleanups . delete ( task )", + "function": "finish", + "key": "src/powersync.ts#finish:value-undefined:const finish = ( ) => pendingDemandCleanups . delete ( task )" + }, + { + "id": "powersync.ts:M401", + "number": 401, + "source": "src/powersync.ts", + "line": 1040, + "position": 37319, + "operator": "value-undefined", + "original": "task.then(finish, finish)", + "replacement": "undefined", + "anchor": "void task . then ( finish , finish )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:void task . then ( finish , finish )" + }, + { + "id": "powersync.ts:M402", + "number": 402, + "source": "src/powersync.ts", + "line": 1041, + "position": 37361, + "operator": "if-true", + "original": "collector", + "replacement": "true", + "anchor": "if ( collector ) { collector . tasks . push ( { order : order ! , promise : task } ) collector . ownedTasks . add ( task ) void task . catch ( ( ) => undefined ) } else { void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-true:if ( collector ) { collector . tasks . push ( { order : order ! , promise : task } ) collector . ownedTasks . add ( task ) void task . catch ( ( ) => undefined ) } else { void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } ) }" + }, + { + "id": "powersync.ts:M403", + "number": 403, + "source": "src/powersync.ts", + "line": 1041, + "position": 37361, + "operator": "if-false", + "original": "collector", + "replacement": "false", + "anchor": "if ( collector ) { collector . tasks . push ( { order : order ! , promise : task } ) collector . ownedTasks . add ( task ) void task . catch ( ( ) => undefined ) } else { void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-false:if ( collector ) { collector . tasks . push ( { order : order ! , promise : task } ) collector . ownedTasks . add ( task ) void task . catch ( ( ) => undefined ) } else { void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } ) }" + }, + { + "id": "powersync.ts:M404", + "number": 404, + "source": "src/powersync.ts", + "line": 1042, + "position": 37388, + "operator": "delete-call", + "original": "collector.tasks.push({ order: order!, promise: task })", + "replacement": ";", + "anchor": "collector . tasks . push ( { order : order ! , promise : task } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:collector . tasks . push ( { order : order ! , promise : task } )" + }, + { + "id": "powersync.ts:M405", + "number": 405, + "source": "src/powersync.ts", + "line": 1042, + "position": 37411, + "operator": "field-drop", + "original": "order: order!", + "replacement": "", + "anchor": "collector . tasks . push ( { order : order ! , promise : task } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:field-drop:collector . tasks . push ( { order : order ! , promise : task } )" + }, + { + "id": "powersync.ts:M406", + "number": 406, + "source": "src/powersync.ts", + "line": 1042, + "position": 37426, + "operator": "field-drop", + "original": "promise: task", + "replacement": "", + "anchor": "collector . tasks . push ( { order : order ! , promise : task } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:field-drop:collector . tasks . push ( { order : order ! , promise : task } )~2" + }, + { + "id": "powersync.ts:M407", + "number": 407, + "source": "src/powersync.ts", + "line": 1043, + "position": 37457, + "operator": "delete-call", + "original": "collector.ownedTasks.add(task)", + "replacement": ";", + "anchor": "collector . ownedTasks . add ( task )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:collector . ownedTasks . add ( task )" + }, + { + "id": "powersync.ts:M408", + "number": 408, + "source": "src/powersync.ts", + "line": 1044, + "position": 37507, + "operator": "value-undefined", + "original": "task.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void task . catch ( ( ) => undefined )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:void task . catch ( ( ) => undefined )" + }, + { + "id": "powersync.ts:M409", + "number": 409, + "source": "src/powersync.ts", + "line": 1046, + "position": 37575, + "operator": "value-undefined", + "original": "task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })", + "replacement": "undefined", + "anchor": "void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } )" + }, + { + "id": "powersync.ts:M410", + "number": 410, + "source": "src/powersync.ts", + "line": 1047, + "position": 37619, + "operator": "if-true", + "original": "!demandCleanupCollector?.ownedTasks.has(task)", + "replacement": "true", + "anchor": "if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-true:if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }" + }, + { + "id": "powersync.ts:M411", + "number": 411, + "source": "src/powersync.ts", + "line": 1047, + "position": 37619, + "operator": "if-false", + "original": "!demandCleanupCollector?.ownedTasks.has(task)", + "replacement": "false", + "anchor": "if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-false:if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }" + }, + { + "id": "powersync.ts:M412", + "number": 412, + "source": "src/powersync.ts", + "line": 1047, + "position": 37619, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:drop-not:if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }" + }, + { + "id": "powersync.ts:M413", + "number": 413, + "source": "src/powersync.ts", + "line": 1047, + "position": 37620, + "operator": "value-undefined", + "original": "demandCleanupCollector?.ownedTasks.has(task)", + "replacement": "undefined", + "anchor": "if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }" + }, + { + "id": "powersync.ts:M414", + "number": 414, + "source": "src/powersync.ts", + "line": 1048, + "position": 37686, + "operator": "delete-call", + "original": "reportDemandCleanupFailure(error)", + "replacement": ";", + "anchor": "reportDemandCleanupFailure ( error )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:reportDemandCleanupFailure ( error )" + }, + { + "id": "powersync.ts:M415", + "number": 415, + "source": "src/powersync.ts", + "line": 1053, + "position": 37813, + "operator": "if-true", + "original": "collector", + "replacement": "true", + "anchor": "if ( collector ) { collector . failures . push ( { order : order ! , error } ) } else { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-true:if ( collector ) { collector . failures . push ( { order : order ! , error } ) } else { reportDemandCleanupFailure ( error ) }" + }, + { + "id": "powersync.ts:M416", + "number": 416, + "source": "src/powersync.ts", + "line": 1053, + "position": 37813, + "operator": "if-false", + "original": "collector", + "replacement": "false", + "anchor": "if ( collector ) { collector . failures . push ( { order : order ! , error } ) } else { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-false:if ( collector ) { collector . failures . push ( { order : order ! , error } ) } else { reportDemandCleanupFailure ( error ) }" + }, + { + "id": "powersync.ts:M417", + "number": 417, + "source": "src/powersync.ts", + "line": 1054, + "position": 37840, + "operator": "delete-call", + "original": "collector.failures.push({ order: order!, error })", + "replacement": ";", + "anchor": "collector . failures . push ( { order : order ! , error } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:collector . failures . push ( { order : order ! , error } )" + }, + { + "id": "powersync.ts:M418", + "number": 418, + "source": "src/powersync.ts", + "line": 1054, + "position": 37866, + "operator": "field-drop", + "original": "order: order!", + "replacement": "", + "anchor": "collector . failures . push ( { order : order ! , error } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:field-drop:collector . failures . push ( { order : order ! , error } )" + }, + { + "id": "powersync.ts:M419", + "number": 419, + "source": "src/powersync.ts", + "line": 1054, + "position": 37881, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "collector . failures . push ( { order : order ! , error } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:field-drop:collector . failures . push ( { order : order ! , error } )~2" + }, + { + "id": "powersync.ts:M420", + "number": 420, + "source": "src/powersync.ts", + "line": 1056, + "position": 37925, + "operator": "delete-call", + "original": "reportDemandCleanupFailure(error)", + "replacement": ";", + "anchor": "reportDemandCleanupFailure ( error )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:reportDemandCleanupFailure ( error )~2" + }, + { + "id": "powersync.ts:M421", + "number": 421, + "source": "src/powersync.ts", + "line": 1067, + "position": 38237, + "operator": "value-undefined", + "original": "collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error)", + "replacement": "undefined", + "anchor": "const errors = [ ... primaryFailures , ... collector . failures . slice ( ) . sort ( ( left , right ) => left . order - right . order ) . map ( ( { error } ) => error ) , ]", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:value-undefined:const errors = [ ... primaryFailures , ... collector . failures . slice ( ) . sort ( ( left , right ) => left . order - right . order ) . map ( ( { error } ) => error ) , ]" + }, + { + "id": "powersync.ts:M422", + "number": 422, + "source": "src/powersync.ts", + "line": 1072, + "position": 38410, + "operator": "if-true", + "original": "errors.length === 1", + "replacement": "true", + "anchor": "if ( errors . length === 1 ) throw errors [ 0 ]", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:if-true:if ( errors . length === 1 ) throw errors [ 0 ]" + }, + { + "id": "powersync.ts:M423", + "number": 423, + "source": "src/powersync.ts", + "line": 1072, + "position": 38410, + "operator": "if-false", + "original": "errors.length === 1", + "replacement": "false", + "anchor": "if ( errors . length === 1 ) throw errors [ 0 ]", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:if-false:if ( errors . length === 1 ) throw errors [ 0 ]" + }, + { + "id": "powersync.ts:M424", + "number": 424, + "source": "src/powersync.ts", + "line": 1072, + "position": 38424, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( errors . length === 1 ) throw errors [ 0 ]", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:binary-swap:if ( errors . length === 1 ) throw errors [ 0 ]" + }, + { + "id": "powersync.ts:M425", + "number": 425, + "source": "src/powersync.ts", + "line": 1073, + "position": 38461, + "operator": "if-true", + "original": "errors.length > 1", + "replacement": "true", + "anchor": "if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:if-true:if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }" + }, + { + "id": "powersync.ts:M426", + "number": 426, + "source": "src/powersync.ts", + "line": 1073, + "position": 38461, + "operator": "if-false", + "original": "errors.length > 1", + "replacement": "false", + "anchor": "if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:if-false:if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }" + }, + { + "id": "powersync.ts:M427", + "number": 427, + "source": "src/powersync.ts", + "line": 1073, + "position": 38475, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:binary-swap:if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }" + }, + { + "id": "powersync.ts:M428", + "number": 428, + "source": "src/powersync.ts", + "line": 1075, + "position": 38571, + "operator": "field-drop", + "original": "cause: errors[0]", + "replacement": "", + "anchor": "throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } )", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:field-drop:throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } )" + }, + { + "id": "powersync.ts:M429", + "number": 429, + "source": "src/powersync.ts", + "line": 1083, + "position": 38778, + "operator": "value-undefined", + "original": "compileSQLite({ where: options.where })", + "replacement": "undefined", + "anchor": "const compiledDeparting = compileSQLite ( { where : options . where } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const compiledDeparting = compileSQLite ( { where : options . where } )" + }, + { + "id": "powersync.ts:M430", + "number": 430, + "source": "src/powersync.ts", + "line": 1083, + "position": 38794, + "operator": "field-drop", + "original": "where: options.where", + "replacement": "", + "anchor": "const compiledDeparting = compileSQLite ( { where : options . where } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:field-drop:const compiledDeparting = compileSQLite ( { where : options . where } )" + }, + { + "id": "powersync.ts:M431", + "number": 431, + "source": "src/powersync.ts", + "line": 1084, + "position": 38854, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledDeparting)", + "replacement": "undefined", + "anchor": "const departingWhereSQL = toInlinedWhereClause ( compiledDeparting )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const departingWhereSQL = toInlinedWhereClause ( compiledDeparting )" + }, + { + "id": "powersync.ts:M432", + "number": 432, + "source": "src/powersync.ts", + "line": 1087, + "position": 38980, + "operator": "if-true", + "original": "stopped", + "replacement": "true", + "anchor": "if ( stopped ) return", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( stopped ) return" + }, + { + "id": "powersync.ts:M433", + "number": 433, + "source": "src/powersync.ts", + "line": 1087, + "position": 38980, + "operator": "if-false", + "original": "stopped", + "replacement": "false", + "anchor": "if ( stopped ) return", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( stopped ) return" + }, + { + "id": "powersync.ts:M434", + "number": 434, + "source": "src/powersync.ts", + "line": 1089, + "position": 39069, + "operator": "value-undefined", + "original": "activeWhereExpressions()", + "replacement": "undefined", + "anchor": "const active = activeWhereExpressions ( )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const active = activeWhereExpressions ( )" + }, + { + "id": "powersync.ts:M435", + "number": 435, + "source": "src/powersync.ts", + "line": 1091, + "position": 39146, + "operator": "if-true", + "original": "active.length === 0", + "replacement": "true", + "anchor": "if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }" + }, + { + "id": "powersync.ts:M436", + "number": 436, + "source": "src/powersync.ts", + "line": 1091, + "position": 39146, + "operator": "if-false", + "original": "active.length === 0", + "replacement": "false", + "anchor": "if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }" + }, + { + "id": "powersync.ts:M437", + "number": 437, + "source": "src/powersync.ts", + "line": 1091, + "position": 39160, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:binary-swap:if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }" + }, + { + "id": "powersync.ts:M438", + "number": 438, + "source": "src/powersync.ts", + "line": 1095, + "position": 39344, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:binary-swap:const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )" + }, + { + "id": "powersync.ts:M439", + "number": 439, + "source": "src/powersync.ts", + "line": 1097, + "position": 39401, + "operator": "value-undefined", + "original": "or(active[0], active[1], ...active.slice(2))", + "replacement": "undefined", + "anchor": "const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )" + }, + { + "id": "powersync.ts:M440", + "number": 440, + "source": "src/powersync.ts", + "line": 1097, + "position": 39429, + "operator": "value-undefined", + "original": "active.slice(2)", + "replacement": "undefined", + "anchor": "const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )~2" + }, + { + "id": "powersync.ts:M441", + "number": 441, + "source": "src/powersync.ts", + "line": 1098, + "position": 39486, + "operator": "value-undefined", + "original": "compileSQLite({\n where: combinedRemaining,\n })", + "replacement": "undefined", + "anchor": "const compiledRemaining = compileSQLite ( { where : combinedRemaining , } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const compiledRemaining = compileSQLite ( { where : combinedRemaining , } )" + }, + { + "id": "powersync.ts:M442", + "number": 442, + "source": "src/powersync.ts", + "line": 1099, + "position": 39518, + "operator": "field-drop", + "original": "where: combinedRemaining", + "replacement": "", + "anchor": "const compiledRemaining = compileSQLite ( { where : combinedRemaining , } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:field-drop:const compiledRemaining = compileSQLite ( { where : combinedRemaining , } )" + }, + { + "id": "powersync.ts:M443", + "number": 443, + "source": "src/powersync.ts", + "line": 1101, + "position": 39601, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledRemaining)", + "replacement": "undefined", + "anchor": "const remainingWhereSQL = toInlinedWhereClause ( compiledRemaining )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const remainingWhereSQL = toInlinedWhereClause ( compiledRemaining )" + }, + { + "id": "powersync.ts:M444", + "number": 444, + "source": "src/powersync.ts", + "line": 1105, + "position": 39805, + "operator": "value-undefined", + "original": "database.getAll<{ id: string }>(evictionSQL)", + "replacement": "undefined", + "anchor": "rowsToEvict = await database . getAll < { id : string } > ( evictionSQL )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:rowsToEvict = await database . getAll < { id : string } > ( evictionSQL )" + }, + { + "id": "powersync.ts:M445", + "number": 445, + "source": "src/powersync.ts", + "line": 1107, + "position": 39986, + "operator": "if-true", + "original": "stopped", + "replacement": "true", + "anchor": "if ( stopped ) return", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( stopped ) return~2" + }, + { + "id": "powersync.ts:M446", + "number": 446, + "source": "src/powersync.ts", + "line": 1107, + "position": 39986, + "operator": "if-false", + "original": "stopped", + "replacement": "false", + "anchor": "if ( stopped ) return", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( stopped ) return~2" + }, + { + "id": "powersync.ts:M447", + "number": 447, + "source": "src/powersync.ts", + "line": 1108, + "position": 40018, + "operator": "if-true", + "original": "trackingRevision === revision", + "replacement": "true", + "anchor": "if ( trackingRevision === revision ) break", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( trackingRevision === revision ) break" + }, + { + "id": "powersync.ts:M448", + "number": 448, + "source": "src/powersync.ts", + "line": 1108, + "position": 40018, + "operator": "if-false", + "original": "trackingRevision === revision", + "replacement": "false", + "anchor": "if ( trackingRevision === revision ) break", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( trackingRevision === revision ) break" + }, + { + "id": "powersync.ts:M449", + "number": 449, + "source": "src/powersync.ts", + "line": 1108, + "position": 40035, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( trackingRevision === revision ) break", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:binary-swap:if ( trackingRevision === revision ) break" + }, + { + "id": "powersync.ts:M450", + "number": 450, + "source": "src/powersync.ts", + "line": 1110, + "position": 40081, + "operator": "if-true", + "original": "rowsToEvict.length > 0", + "replacement": "true", + "anchor": "if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }" + }, + { + "id": "powersync.ts:M451", + "number": 451, + "source": "src/powersync.ts", + "line": 1110, + "position": 40081, + "operator": "if-false", + "original": "rowsToEvict.length > 0", + "replacement": "false", + "anchor": "if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }" + }, + { + "id": "powersync.ts:M452", + "number": 452, + "source": "src/powersync.ts", + "line": 1110, + "position": 40100, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:binary-swap:if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }" + }, + { + "id": "powersync.ts:M453", + "number": 453, + "source": "src/powersync.ts", + "line": 1111, + "position": 40119, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:delete-call:begin ( )" + }, + { + "id": "powersync.ts:M454", + "number": 454, + "source": "src/powersync.ts", + "line": 1113, + "position": 40189, + "operator": "delete-call", + "original": "write({ type: `delete`, key: id })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key : id } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:delete-call:write ( { type : `delete` , key : id } )" + }, + { + "id": "powersync.ts:M455", + "number": 455, + "source": "src/powersync.ts", + "line": 1113, + "position": 40197, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key : id } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:field-drop:write ( { type : `delete` , key : id } )" + }, + { + "id": "powersync.ts:M456", + "number": 456, + "source": "src/powersync.ts", + "line": 1113, + "position": 40213, + "operator": "field-drop", + "original": "key: id", + "replacement": "", + "anchor": "write ( { type : `delete` , key : id } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:field-drop:write ( { type : `delete` , key : id } )~2" + }, + { + "id": "powersync.ts:M457", + "number": 457, + "source": "src/powersync.ts", + "line": 1115, + "position": 40255, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "void commit ( )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:void commit ( )" + }, + { + "id": "powersync.ts:M458", + "number": 458, + "source": "src/powersync.ts", + "line": 1117, + "position": 40292, + "operator": "value-undefined", + "original": "rebuildTracking()", + "replacement": "undefined", + "anchor": "await rebuildTracking ( )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:await rebuildTracking ( )" + }, + { + "id": "powersync.ts:M459", + "number": 459, + "source": "src/powersync.ts", + "line": 1121, + "position": 40392, + "operator": "if-true", + "original": "stopped || drainingReleases || releaseRetryTimer", + "replacement": "true", + "anchor": "if ( stopped || drainingReleases || releaseRetryTimer ) return", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:if-true:if ( stopped || drainingReleases || releaseRetryTimer ) return" + }, + { + "id": "powersync.ts:M460", + "number": 460, + "source": "src/powersync.ts", + "line": 1121, + "position": 40392, + "operator": "if-false", + "original": "stopped || drainingReleases || releaseRetryTimer", + "replacement": "false", + "anchor": "if ( stopped || drainingReleases || releaseRetryTimer ) return", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:if-false:if ( stopped || drainingReleases || releaseRetryTimer ) return" + }, + { + "id": "powersync.ts:M461", + "number": 461, + "source": "src/powersync.ts", + "line": 1121, + "position": 40420, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || drainingReleases || releaseRetryTimer ) return", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:binary-swap:if ( stopped || drainingReleases || releaseRetryTimer ) return" + }, + { + "id": "powersync.ts:M462", + "number": 462, + "source": "src/powersync.ts", + "line": 1121, + "position": 40400, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || drainingReleases || releaseRetryTimer ) return", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:binary-swap:if ( stopped || drainingReleases || releaseRetryTimer ) return~2" + }, + { + "id": "powersync.ts:M463", + "number": 463, + "source": "src/powersync.ts", + "line": 1122, + "position": 40463, + "operator": "if-true", + "original": "delay > 0", + "replacement": "true", + "anchor": "if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:if-true:if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }" + }, + { + "id": "powersync.ts:M464", + "number": 464, + "source": "src/powersync.ts", + "line": 1122, + "position": 40463, + "operator": "if-false", + "original": "delay > 0", + "replacement": "false", + "anchor": "if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:if-false:if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }" + }, + { + "id": "powersync.ts:M465", + "number": 465, + "source": "src/powersync.ts", + "line": 1122, + "position": 40469, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:binary-swap:if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }" + }, + { + "id": "powersync.ts:M466", + "number": 466, + "source": "src/powersync.ts", + "line": 1123, + "position": 40508, + "operator": "value-undefined", + "original": "setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)", + "replacement": "undefined", + "anchor": "releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay )", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:value-undefined:releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay )" + }, + { + "id": "powersync.ts:M467", + "number": 467, + "source": "src/powersync.ts", + "line": 1125, + "position": 40590, + "operator": "value-undefined", + "original": "drainReleases()", + "replacement": "undefined", + "anchor": "void drainReleases ( )", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:value-undefined:void drainReleases ( )" + }, + { + "id": "powersync.ts:M468", + "number": 468, + "source": "src/powersync.ts", + "line": 1129, + "position": 40674, + "operator": "value-undefined", + "original": "drainReleases()", + "replacement": "undefined", + "anchor": "void drainReleases ( )", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:value-undefined:void drainReleases ( )~2" + }, + { + "id": "powersync.ts:M469", + "number": 469, + "source": "src/powersync.ts", + "line": 1133, + "position": 40771, + "operator": "if-true", + "original": "stopped || drainingReleases", + "replacement": "true", + "anchor": "if ( stopped || drainingReleases ) return", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:if-true:if ( stopped || drainingReleases ) return" + }, + { + "id": "powersync.ts:M470", + "number": 470, + "source": "src/powersync.ts", + "line": 1133, + "position": 40771, + "operator": "if-false", + "original": "stopped || drainingReleases", + "replacement": "false", + "anchor": "if ( stopped || drainingReleases ) return", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:if-false:if ( stopped || drainingReleases ) return" + }, + { + "id": "powersync.ts:M471", + "number": 471, + "source": "src/powersync.ts", + "line": 1133, + "position": 40779, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || drainingReleases ) return", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:if ( stopped || drainingReleases ) return" + }, + { + "id": "powersync.ts:M472", + "number": 472, + "source": "src/powersync.ts", + "line": 1134, + "position": 40836, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "drainingReleases = true", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:bool-flip:drainingReleases = true" + }, + { + "id": "powersync.ts:M473", + "number": 473, + "source": "src/powersync.ts", + "line": 1139, + "position": 41090, + "operator": "loop-false", + "original": "!stopped && index < attempts", + "replacement": "false", + "anchor": "for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:loop-false:for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }" + }, + { + "id": "powersync.ts:M474", + "number": 474, + "source": "src/powersync.ts", + "line": 1139, + "position": 41099, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }" + }, + { + "id": "powersync.ts:M475", + "number": 475, + "source": "src/powersync.ts", + "line": 1139, + "position": 41090, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:drop-not:for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }" + }, + { + "id": "powersync.ts:M476", + "number": 476, + "source": "src/powersync.ts", + "line": 1139, + "position": 41108, + "operator": "binary-swap", + "original": "<", + "replacement": "<=", + "anchor": "for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }~2" + }, + { + "id": "powersync.ts:M477", + "number": 477, + "source": "src/powersync.ts", + "line": 1140, + "position": 41161, + "operator": "value-undefined", + "original": "pendingReleases.shift()", + "replacement": "undefined", + "anchor": "const pending = pendingReleases . shift ( ) !", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:value-undefined:const pending = pendingReleases . shift ( ) !" + }, + { + "id": "powersync.ts:M478", + "number": 478, + "source": "src/powersync.ts", + "line": 1142, + "position": 41228, + "operator": "value-undefined", + "original": "performPhysicalRelease(pending.options)", + "replacement": "undefined", + "anchor": "await performPhysicalRelease ( pending . options )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:value-undefined:await performPhysicalRelease ( pending . options )" + }, + { + "id": "powersync.ts:M479", + "number": 479, + "source": "src/powersync.ts", + "line": 1145, + "position": 41351, + "operator": "delete-call", + "original": "pendingReleases.push(pending)", + "replacement": ";", + "anchor": "pendingReleases . push ( pending )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:delete-call:pendingReleases . push ( pending )" + }, + { + "id": "powersync.ts:M480", + "number": 480, + "source": "src/powersync.ts", + "line": 1146, + "position": 41411, + "operator": "value-undefined", + "original": "Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )", + "replacement": "undefined", + "anchor": "const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:value-undefined:const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , )" + }, + { + "id": "powersync.ts:M481", + "number": 481, + "source": "src/powersync.ts", + "line": 1147, + "position": 41469, + "operator": "arithmetic-swap", + "original": "-", + "replacement": "+", + "anchor": "const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:arithmetic-swap:const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , )" + }, + { + "id": "powersync.ts:M482", + "number": 482, + "source": "src/powersync.ts", + "line": 1151, + "position": 41576, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay )" + }, + { + "id": "powersync.ts:M483", + "number": 483, + "source": "src/powersync.ts", + "line": 1151, + "position": 41592, + "operator": "value-undefined", + "original": "Math.min(retryDelay, delay)", + "replacement": "undefined", + "anchor": "retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:value-undefined:retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay )" + }, + { + "id": "powersync.ts:M484", + "number": 484, + "source": "src/powersync.ts", + "line": 1152, + "position": 41636, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `Could not release subset tracking for ${viewName}; retrying`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:delete-call:database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })" + }, + { + "id": "powersync.ts:M485", + "number": 485, + "source": "src/powersync.ts", + "line": 1153, + "position": 41676, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })" + }, + { + "id": "powersync.ts:M486", + "number": 486, + "source": "src/powersync.ts", + "line": 1154, + "position": 41718, + "operator": "field-drop", + "original": "message: `Could not release subset tracking for ${viewName}; retrying`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })~2" + }, + { + "id": "powersync.ts:M487", + "number": 487, + "source": "src/powersync.ts", + "line": 1155, + "position": 41808, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })~3" + }, + { + "id": "powersync.ts:M488", + "number": 488, + "source": "src/powersync.ts", + "line": 1160, + "position": 41917, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "drainingReleases = false", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:bool-flip:drainingReleases = false" + }, + { + "id": "powersync.ts:M489", + "number": 489, + "source": "src/powersync.ts", + "line": 1162, + "position": 41949, + "operator": "if-true", + "original": "pendingReleases.length > 0", + "replacement": "true", + "anchor": "if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:if-true:if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )" + }, + { + "id": "powersync.ts:M490", + "number": 490, + "source": "src/powersync.ts", + "line": 1162, + "position": 41949, + "operator": "if-false", + "original": "pendingReleases.length > 0", + "replacement": "false", + "anchor": "if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:if-false:if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )" + }, + { + "id": "powersync.ts:M491", + "number": 491, + "source": "src/powersync.ts", + "line": 1162, + "position": 41972, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )" + }, + { + "id": "powersync.ts:M492", + "number": 492, + "source": "src/powersync.ts", + "line": 1162, + "position": 41977, + "operator": "delete-call", + "original": "scheduleReleaseDrain(retryDelay)", + "replacement": ";", + "anchor": "scheduleReleaseDrain ( retryDelay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:delete-call:scheduleReleaseDrain ( retryDelay )" + }, + { + "id": "powersync.ts:M493", + "number": 493, + "source": "src/powersync.ts", + "line": 1166, + "position": 42100, + "operator": "delete-call", + "original": "releasedSubsets.add(options)", + "replacement": ";", + "anchor": "releasedSubsets . add ( options )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:releasedSubsets . add ( options )" + }, + { + "id": "powersync.ts:M494", + "number": 494, + "source": "src/powersync.ts", + "line": 1167, + "position": 42154, + "operator": "value-undefined", + "original": "demands.get(options)", + "replacement": "undefined", + "anchor": "const demand = demands . get ( options )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:value-undefined:const demand = demands . get ( options )" + }, + { + "id": "powersync.ts:M495", + "number": 495, + "source": "src/powersync.ts", + "line": 1168, + "position": 42189, + "operator": "if-true", + "original": "!demand", + "replacement": "true", + "anchor": "if ( ! demand ) return", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-true:if ( ! demand ) return" + }, + { + "id": "powersync.ts:M496", + "number": 496, + "source": "src/powersync.ts", + "line": 1168, + "position": 42189, + "operator": "if-false", + "original": "!demand", + "replacement": "false", + "anchor": "if ( ! demand ) return", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-false:if ( ! demand ) return" + }, + { + "id": "powersync.ts:M497", + "number": 497, + "source": "src/powersync.ts", + "line": 1168, + "position": 42189, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! demand ) return", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:drop-not:if ( ! demand ) return" + }, + { + "id": "powersync.ts:M498", + "number": 498, + "source": "src/powersync.ts", + "line": 1171, + "position": 42262, + "operator": "if-true", + "original": "wasActive", + "replacement": "true", + "anchor": "if ( wasActive ) trackingRevision ++", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-true:if ( wasActive ) trackingRevision ++" + }, + { + "id": "powersync.ts:M499", + "number": 499, + "source": "src/powersync.ts", + "line": 1171, + "position": 42262, + "operator": "if-false", + "original": "wasActive", + "replacement": "false", + "anchor": "if ( wasActive ) trackingRevision ++", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-false:if ( wasActive ) trackingRevision ++" + }, + { + "id": "powersync.ts:M500", + "number": 500, + "source": "src/powersync.ts", + "line": 1172, + "position": 42302, + "operator": "delete-call", + "original": "cleanupDemand(demand)", + "replacement": ";", + "anchor": "cleanupDemand ( demand )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:cleanupDemand ( demand )" + }, + { + "id": "powersync.ts:M501", + "number": 501, + "source": "src/powersync.ts", + "line": 1174, + "position": 42339, + "operator": "if-true", + "original": "wasActive", + "replacement": "true", + "anchor": "if ( wasActive ) { pendingReleases . push ( { options , failures : 0 } ) clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined scheduleReleaseDrain ( ) }", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-true:if ( wasActive ) { pendingReleases . push ( { options , failures : 0 } ) clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined scheduleReleaseDrain ( ) }" + }, + { + "id": "powersync.ts:M502", + "number": 502, + "source": "src/powersync.ts", + "line": 1174, + "position": 42339, + "operator": "if-false", + "original": "wasActive", + "replacement": "false", + "anchor": "if ( wasActive ) { pendingReleases . push ( { options , failures : 0 } ) clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined scheduleReleaseDrain ( ) }", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-false:if ( wasActive ) { pendingReleases . push ( { options , failures : 0 } ) clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined scheduleReleaseDrain ( ) }" + }, + { + "id": "powersync.ts:M503", + "number": 503, + "source": "src/powersync.ts", + "line": 1175, + "position": 42364, + "operator": "delete-call", + "original": "pendingReleases.push({ options, failures: 0 })", + "replacement": ";", + "anchor": "pendingReleases . push ( { options , failures : 0 } )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:pendingReleases . push ( { options , failures : 0 } )" + }, + { + "id": "powersync.ts:M504", + "number": 504, + "source": "src/powersync.ts", + "line": 1175, + "position": 42387, + "operator": "field-drop", + "original": "options", + "replacement": "", + "anchor": "pendingReleases . push ( { options , failures : 0 } )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:field-drop:pendingReleases . push ( { options , failures : 0 } )" + }, + { + "id": "powersync.ts:M505", + "number": 505, + "source": "src/powersync.ts", + "line": 1175, + "position": 42396, + "operator": "field-drop", + "original": "failures: 0", + "replacement": "", + "anchor": "pendingReleases . push ( { options , failures : 0 } )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:field-drop:pendingReleases . push ( { options , failures : 0 } )~2" + }, + { + "id": "powersync.ts:M506", + "number": 506, + "source": "src/powersync.ts", + "line": 1177, + "position": 42492, + "operator": "delete-call", + "original": "clearTimeout(releaseRetryTimer)", + "replacement": ";", + "anchor": "clearTimeout ( releaseRetryTimer )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:clearTimeout ( releaseRetryTimer )" + }, + { + "id": "powersync.ts:M507", + "number": 507, + "source": "src/powersync.ts", + "line": 1179, + "position": 42578, + "operator": "delete-call", + "original": "scheduleReleaseDrain()", + "replacement": ";", + "anchor": "scheduleReleaseDrain ( )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:scheduleReleaseDrain ( )" + }, + { + "id": "powersync.ts:M508", + "number": 508, + "source": "src/powersync.ts", + "line": 1183, + "position": 42632, + "operator": "delete-call", + "original": "markReady()", + "replacement": ";", + "anchor": "markReady ( )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:delete-call:markReady ( )" + }, + { + "id": "powersync.ts:M509", + "number": 509, + "source": "src/powersync.ts", + "line": 1186, + "position": 42672, + "operator": "field-drop", + "original": "cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === `rejected`) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === `rejected`\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n }", + "replacement": "", + "anchor": "return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }" + }, + { + "id": "powersync.ts:M510", + "number": 510, + "source": "src/powersync.ts", + "line": 1187, + "position": 42711, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "stopped = true", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:bool-flip:stopped = true" + }, + { + "id": "powersync.ts:M511", + "number": 511, + "source": "src/powersync.ts", + "line": 1188, + "position": 42728, + "operator": "delete-call", + "original": "clearTimeout(releaseRetryTimer)", + "replacement": ";", + "anchor": "clearTimeout ( releaseRetryTimer )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:clearTimeout ( releaseRetryTimer )" + }, + { + "id": "powersync.ts:M512", + "number": 512, + "source": "src/powersync.ts", + "line": 1190, + "position": 42814, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })" + }, + { + "id": "powersync.ts:M513", + "number": 513, + "source": "src/powersync.ts", + "line": 1191, + "position": 42850, + "operator": "field-drop", + "original": "level: LogLevels.info", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })" + }, + { + "id": "powersync.ts:M514", + "number": 514, + "source": "src/powersync.ts", + "line": 1192, + "position": 42887, + "operator": "field-drop", + "original": "message: `Sync has been stopped for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })~2" + }, + { + "id": "powersync.ts:M515", + "number": 515, + "source": "src/powersync.ts", + "line": 1194, + "position": 42989, + "operator": "delete-call", + "original": "abortController.abort()", + "replacement": ";", + "anchor": "abortController . abort ( )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:abortController . abort ( )" + }, + { + "id": "powersync.ts:M516", + "number": 516, + "source": "src/powersync.ts", + "line": 1195, + "position": 43050, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "const trackingDisposal = disposeTrackingAfterAbort ( )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const trackingDisposal = disposeTrackingAfterAbort ( )" + }, + { + "id": "powersync.ts:M517", + "number": 517, + "source": "src/powersync.ts", + "line": 1199, + "position": 43265, + "operator": "field-drop", + "original": "tasks: pendingTasks.map((promise, order) => ({ order, promise }))", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }" + }, + { + "id": "powersync.ts:M518", + "number": 518, + "source": "src/powersync.ts", + "line": 1199, + "position": 43272, + "operator": "value-undefined", + "original": "pendingTasks.map((promise, order) => ({ order, promise }))", + "replacement": "undefined", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }" + }, + { + "id": "powersync.ts:M519", + "number": 519, + "source": "src/powersync.ts", + "line": 1199, + "position": 43312, + "operator": "field-drop", + "original": "order", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~2" + }, + { + "id": "powersync.ts:M520", + "number": 520, + "source": "src/powersync.ts", + "line": 1199, + "position": 43319, + "operator": "field-drop", + "original": "promise", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~3" + }, + { + "id": "powersync.ts:M521", + "number": 521, + "source": "src/powersync.ts", + "line": 1200, + "position": 43346, + "operator": "field-drop", + "original": "ownedTasks: new Set(pendingTasks)", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~4" + }, + { + "id": "powersync.ts:M522", + "number": 522, + "source": "src/powersync.ts", + "line": 1201, + "position": 43395, + "operator": "field-drop", + "original": "failures: []", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~5" + }, + { + "id": "powersync.ts:M523", + "number": 523, + "source": "src/powersync.ts", + "line": 1202, + "position": 43423, + "operator": "field-drop", + "original": "nextOrder: pendingTasks.length", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~6" + }, + { + "id": "powersync.ts:M524", + "number": 524, + "source": "src/powersync.ts", + "line": 1205, + "position": 43549, + "operator": "value-undefined", + "original": "demands.values()", + "replacement": "undefined", + "anchor": "for ( const demand of demands . values ( ) ) { cleanupDemand ( demand ) }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:for ( const demand of demands . values ( ) ) { cleanupDemand ( demand ) }" + }, + { + "id": "powersync.ts:M525", + "number": 525, + "source": "src/powersync.ts", + "line": 1206, + "position": 43583, + "operator": "delete-call", + "original": "cleanupDemand(demand)", + "replacement": ";", + "anchor": "cleanupDemand ( demand )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:cleanupDemand ( demand )" + }, + { + "id": "powersync.ts:M526", + "number": 526, + "source": "src/powersync.ts", + "line": 1210, + "position": 43678, + "operator": "value-undefined", + "original": "(async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === `rejected`) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === `rejected`\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })", + "replacement": "undefined", + "anchor": "return ( async ( ) => { const ownershipSettlements = await Promise . allSettled ( [ trackingDisposal , ... pendingLoads , ] ) const disposalSettlement = ownershipSettlements [ 0 ] let settledTasks = 0 while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } } throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , ) } ) ( ) . finally ( ( ) => { if ( demandCleanupCollector === collector ) { demandCleanupCollector = null } } )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:return ( async ( ) => { const ownershipSettlements = await Promise . allSettled ( [ trackingDisposal , ... pendingLoads , ] ) const disposalSettlement = ownershipSettlements [ 0 ] let settledTasks = 0 while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } } throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , ) } ) ( ) . finally ( ( ) => { if ( demandCleanupCollector === collector ) { demandCleanupCollector = null } } )" + }, + { + "id": "powersync.ts:M527", + "number": 527, + "source": "src/powersync.ts", + "line": 1211, + "position": 43742, + "operator": "value-undefined", + "original": "Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])", + "replacement": "undefined", + "anchor": "const ownershipSettlements = await Promise . allSettled ( [ trackingDisposal , ... pendingLoads , ] )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const ownershipSettlements = await Promise . allSettled ( [ trackingDisposal , ... pendingLoads , ] )" + }, + { + "id": "powersync.ts:M528", + "number": 528, + "source": "src/powersync.ts", + "line": 1218, + "position": 43969, + "operator": "loop-false", + "original": "settledTasks < collector.tasks.length", + "replacement": "false", + "anchor": "while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:loop-false:while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } }" + }, + { + "id": "powersync.ts:M529", + "number": 529, + "source": "src/powersync.ts", + "line": 1218, + "position": 43982, + "operator": "binary-swap", + "original": "<", + "replacement": "<=", + "anchor": "while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:binary-swap:while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } }" + }, + { + "id": "powersync.ts:M530", + "number": 530, + "source": "src/powersync.ts", + "line": 1219, + "position": 44040, + "operator": "value-undefined", + "original": "collector.tasks.slice(settledTasks)", + "replacement": "undefined", + "anchor": "const tasks = collector . tasks . slice ( settledTasks )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const tasks = collector . tasks . slice ( settledTasks )" + }, + { + "id": "powersync.ts:M531", + "number": 531, + "source": "src/powersync.ts", + "line": 1221, + "position": 44169, + "operator": "value-undefined", + "original": "Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )", + "replacement": "undefined", + "anchor": "const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , )" + }, + { + "id": "powersync.ts:M532", + "number": 532, + "source": "src/powersync.ts", + "line": 1222, + "position": 44207, + "operator": "value-undefined", + "original": "tasks.map(({ promise }) => promise)", + "replacement": "undefined", + "anchor": "const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , )~2" + }, + { + "id": "powersync.ts:M533", + "number": 533, + "source": "src/powersync.ts", + "line": 1224, + "position": 44309, + "operator": "value-undefined", + "original": "outcomes.entries()", + "replacement": "undefined", + "anchor": "for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } }" + }, + { + "id": "powersync.ts:M534", + "number": 534, + "source": "src/powersync.ts", + "line": 1225, + "position": 44353, + "operator": "if-true", + "original": "outcome.status === `rejected`", + "replacement": "true", + "anchor": "if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:if-true:if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }" + }, + { + "id": "powersync.ts:M535", + "number": 535, + "source": "src/powersync.ts", + "line": 1225, + "position": 44353, + "operator": "if-false", + "original": "outcome.status === `rejected`", + "replacement": "false", + "anchor": "if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:if-false:if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }" + }, + { + "id": "powersync.ts:M536", + "number": 536, + "source": "src/powersync.ts", + "line": 1225, + "position": 44368, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:binary-swap:if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }" + }, + { + "id": "powersync.ts:M537", + "number": 537, + "source": "src/powersync.ts", + "line": 1226, + "position": 44406, + "operator": "delete-call", + "original": "collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })", + "replacement": ";", + "anchor": "collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )" + }, + { + "id": "powersync.ts:M538", + "number": 538, + "source": "src/powersync.ts", + "line": 1227, + "position": 44454, + "operator": "field-drop", + "original": "order: tasks[index]!.order", + "replacement": "", + "anchor": "collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )" + }, + { + "id": "powersync.ts:M539", + "number": 539, + "source": "src/powersync.ts", + "line": 1228, + "position": 44504, + "operator": "field-drop", + "original": "error: outcome.reason", + "replacement": "", + "anchor": "collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )~2" + }, + { + "id": "powersync.ts:M540", + "number": 540, + "source": "src/powersync.ts", + "line": 1233, + "position": 44618, + "operator": "delete-call", + "original": "throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === `rejected`\n ? [disposalSettlement.reason]\n : [],\n )", + "replacement": ";", + "anchor": "throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , )" + }, + { + "id": "powersync.ts:M541", + "number": 541, + "source": "src/powersync.ts", + "line": 1235, + "position": 44715, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:binary-swap:throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , )" + }, + { + "id": "powersync.ts:M542", + "number": 542, + "source": "src/powersync.ts", + "line": 1240, + "position": 44869, + "operator": "if-true", + "original": "demandCleanupCollector === collector", + "replacement": "true", + "anchor": "if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:if-true:if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }" + }, + { + "id": "powersync.ts:M543", + "number": 543, + "source": "src/powersync.ts", + "line": 1240, + "position": 44869, + "operator": "if-false", + "original": "demandCleanupCollector === collector", + "replacement": "false", + "anchor": "if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:if-false:if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }" + }, + { + "id": "powersync.ts:M544", + "number": 544, + "source": "src/powersync.ts", + "line": 1240, + "position": 44892, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:binary-swap:if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }" + }, + { + "id": "powersync.ts:M545", + "number": 545, + "source": "src/powersync.ts", + "line": 1245, + "position": 45009, + "operator": "field-drop", + "original": "loadSubset: trackLoadSubset", + "replacement": "", + "anchor": "return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }" + }, + { + "id": "powersync.ts:M546", + "number": 546, + "source": "src/powersync.ts", + "line": 1246, + "position": 45048, + "operator": "field-drop", + "original": "unloadSubset", + "replacement": "", + "anchor": "return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }~2" + }, + { + "id": "powersync.ts:M547", + "number": 547, + "source": "src/powersync.ts", + "line": 1251, + "position": 45134, + "operator": "field-drop", + "original": "getSyncMetadata: undefined", + "replacement": "", + "anchor": "const sync : SyncConfig < OutputType , string > = { sync : ( params ) => { const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const { begin , write , collection , commit , fail , reportError , reportHealthy , } = params let ready = false const markReady = ( ) => { ready = true params . markReady ( ) } const abortController = new AbortController ( ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let disposeTracking : ( ( options ? : { context ? : LockContext } ) => Promise < void > ) | null = null let trackingSetup : Promise < void > | null = null let activeTrackingDisposal : Promise < void > | null = null let abortTrackingDisposal : Promise < void > | null = null if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) } async function safelyDisposeTracking ( context ? : LockContext , ) : Promise < void > { const setup = trackingSetup if ( setup ) { await setup . catch ( ( ) => undefined ) } const dispose = disposeTracking if ( ! dispose ) { await activeTrackingDisposal return } disposeTracking = null let disposal : Promise < void > try { disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) ) } catch ( error ) { disposal = Promise . reject ( error ) } activeTrackingDisposal = disposal try { await disposal } finally { if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null } } } function disposeTrackingAfterAbort ( ) : Promise < void > { const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) ) void disposal . catch ( ( ) => undefined ) return disposal } async function establishTracking ( options : Parameters < typeof createDiffTrigger > [ 0 ] , appliedReceipts : Array < SyncAppliedReceipt > , ) : Promise < void > { const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( ) trackingSetup = setup try { await setup } finally { if ( trackingSetup === setup ) { trackingSetup = null } } } async function createDiffTrigger ( options : { setupContext ? : LockContext snapshot ? : boolean afterRead ? : ( ) => Promise < void > onInitialRow ? : ( value : OutputType ) => void when : Record < DiffTriggerOperation , string > writeType : ( rowId : string ) => OperationType batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => Promise < Array < TableType >> } , appliedReceipts : Array < SyncAppliedReceipt > , ) { const { setupContext , snapshot , afterRead , onInitialRow , when , writeType , batchQuery , } = options return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n ` SELECT * FROM $ { trackedTableName } ORDER BY operation_id ASC `,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(` DELETE FROM $ { trackedTableName } `)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync is starting for $ { viewName } into $ { trackedTableName } `,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n ` abort `,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: ` delete `, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: ` TRUE `,\n [DiffTriggerOperation.UPDATE]: ` TRUE `,\n [DiffTriggerOperation.DELETE]: ` TRUE `,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL` SELECT * FROM $ { viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === ` rejected ` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n ` PowerSync tracking disposal and load - hook cleanup both failed `,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === ` rejected `) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: ` ( $ { newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not clean up subset hook for $ { viewName } `,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, ` PowerSync cleanup failed `, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: ` delete `, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not release subset tracking for $ { viewName } ; retrying `,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const sync : SyncConfig < OutputType , string > = { sync : ( params ) => { const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const { begin , write , collection , commit , fail , reportError , reportHealthy , } = params let ready = false const markReady = ( ) => { ready = true params . markReady ( ) } const abortController = new AbortController ( ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let disposeTracking : ( ( options ? : { context ? : LockContext } ) => Promise < void > ) | null = null let trackingSetup : Promise < void > | null = null let activeTrackingDisposal : Promise < void > | null = null let abortTrackingDisposal : Promise < void > | null = null if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) } async function safelyDisposeTracking ( context ? : LockContext , ) : Promise < void > { const setup = trackingSetup if ( setup ) { await setup . catch ( ( ) => undefined ) } const dispose = disposeTracking if ( ! dispose ) { await activeTrackingDisposal return } disposeTracking = null let disposal : Promise < void > try { disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) ) } catch ( error ) { disposal = Promise . reject ( error ) } activeTrackingDisposal = disposal try { await disposal } finally { if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null } } } function disposeTrackingAfterAbort ( ) : Promise < void > { const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) ) void disposal . catch ( ( ) => undefined ) return disposal } async function establishTracking ( options : Parameters < typeof createDiffTrigger > [ 0 ] , appliedReceipts : Array < SyncAppliedReceipt > , ) : Promise < void > { const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( ) trackingSetup = setup try { await setup } finally { if ( trackingSetup === setup ) { trackingSetup = null } } } async function createDiffTrigger ( options : { setupContext ? : LockContext snapshot ? : boolean afterRead ? : ( ) => Promise < void > onInitialRow ? : ( value : OutputType ) => void when : Record < DiffTriggerOperation , string > writeType : ( rowId : string ) => OperationType batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => Promise < Array < TableType >> } , appliedReceipts : Array < SyncAppliedReceipt > , ) { const { setupContext , snapshot , afterRead , onInitialRow , when , writeType , batchQuery , } = options return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n ` SELECT * FROM $ { trackedTableName } ORDER BY operation_id ASC `,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(` DELETE FROM $ { trackedTableName } `)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync is starting for $ { viewName } into $ { trackedTableName } `,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n ` abort `,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: ` delete `, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: ` TRUE `,\n [DiffTriggerOperation.UPDATE]: ` TRUE `,\n [DiffTriggerOperation.DELETE]: ` TRUE `,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL` SELECT * FROM $ { viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === ` rejected ` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n ` PowerSync tracking disposal and load - hook cleanup both failed `,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === ` rejected `) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: ` ( $ { newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not clean up subset hook for $ { viewName } `,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, ` PowerSync cleanup failed `, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: ` delete `, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not release subset tracking for $ { viewName } ; retrying `,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }" + }, + { + "id": "powersync.ts:M548", + "number": 548, + "source": "src/powersync.ts", + "line": 1265, + "position": 45545, + "operator": "field-drop", + "original": "deserializationSchema", + "replacement": "", + "anchor": "const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }" + }, + { + "id": "powersync.ts:M549", + "number": 549, + "source": "src/powersync.ts", + "line": 1266, + "position": 45572, + "operator": "field-drop", + "original": "serializer", + "replacement": "", + "anchor": "const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }~2" + }, + { + "id": "powersync.ts:M550", + "number": 550, + "source": "src/powersync.ts", + "line": 1267, + "position": 45588, + "operator": "field-drop", + "original": "onDeserializationError", + "replacement": "", + "anchor": "const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }~3" + }, + { + "id": "powersync.ts:M551", + "number": 551, + "source": "src/powersync.ts", + "line": 1277, + "position": 45770, + "operator": "field-drop", + "original": "schema", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + { + "id": "powersync.ts:M552", + "number": 552, + "source": "src/powersync.ts", + "line": 1278, + "position": 45782, + "operator": "field-drop", + "original": "getKey", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~2" + }, + { + "id": "powersync.ts:M553", + "number": 553, + "source": "src/powersync.ts", + "line": 1280, + "position": 45885, + "operator": "field-drop", + "original": "startSync: true", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~3" + }, + { + "id": "powersync.ts:M554", + "number": 554, + "source": "src/powersync.ts", + "line": 1280, + "position": 45896, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:bool-flip:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + { + "id": "powersync.ts:M555", + "number": 555, + "source": "src/powersync.ts", + "line": 1281, + "position": 45906, + "operator": "field-drop", + "original": "syncMode", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~4" + }, + { + "id": "powersync.ts:M556", + "number": 556, + "source": "src/powersync.ts", + "line": 1282, + "position": 45920, + "operator": "field-drop", + "original": "sync", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~5" + }, + { + "id": "powersync.ts:M557", + "number": 557, + "source": "src/powersync.ts", + "line": 1283, + "position": 45930, + "operator": "field-drop", + "original": "onInsert: async (params) => {\n // The transaction here should only ever contain a single insert mutation\n return await transactor.applyTransaction(params.transaction)\n }", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "onInsert", + "key": "src/powersync.ts#onInsert:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + { + "id": "powersync.ts:M558", + "number": 558, + "source": "src/powersync.ts", + "line": 1285, + "position": 46059, + "operator": "value-undefined", + "original": "transactor.applyTransaction(params.transaction)", + "replacement": "undefined", + "anchor": "return await transactor . applyTransaction ( params . transaction )", + "function": "onInsert", + "key": "src/powersync.ts#onInsert:value-undefined:return await transactor . applyTransaction ( params . transaction )" + }, + { + "id": "powersync.ts:M559", + "number": 559, + "source": "src/powersync.ts", + "line": 1287, + "position": 46118, + "operator": "field-drop", + "original": "onUpdate: async (params) => {\n // The transaction here should only ever contain a single update mutation\n return await transactor.applyTransaction(params.transaction)\n }", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "onUpdate", + "key": "src/powersync.ts#onUpdate:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + { + "id": "powersync.ts:M560", + "number": 560, + "source": "src/powersync.ts", + "line": 1289, + "position": 46247, + "operator": "value-undefined", + "original": "transactor.applyTransaction(params.transaction)", + "replacement": "undefined", + "anchor": "return await transactor . applyTransaction ( params . transaction )", + "function": "onUpdate", + "key": "src/powersync.ts#onUpdate:value-undefined:return await transactor . applyTransaction ( params . transaction )" + }, + { + "id": "powersync.ts:M561", + "number": 561, + "source": "src/powersync.ts", + "line": 1291, + "position": 46306, + "operator": "field-drop", + "original": "onDelete: async (params) => {\n // The transaction here should only ever contain a single delete mutation\n return await transactor.applyTransaction(params.transaction)\n }", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "onDelete", + "key": "src/powersync.ts#onDelete:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + { + "id": "powersync.ts:M562", + "number": 562, + "source": "src/powersync.ts", + "line": 1293, + "position": 46435, + "operator": "value-undefined", + "original": "transactor.applyTransaction(params.transaction)", + "replacement": "undefined", + "anchor": "return await transactor . applyTransaction ( params . transaction )", + "function": "onDelete", + "key": "src/powersync.ts#onDelete:value-undefined:return await transactor . applyTransaction ( params . transaction )" + }, + { + "id": "powersync.ts:M563", + "number": 563, + "source": "src/powersync.ts", + "line": 1295, + "position": 46494, + "operator": "field-drop", + "original": "utils: {\n getMeta: () => ({\n tableName: viewName,\n trackedTableName,\n metadataIsTracked,\n serializeValue: (value) =>\n serializeForSQLite(value, table, serializer),\n }),\n }", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~6" + }, + { + "id": "powersync.ts:M564", + "number": 564, + "source": "src/powersync.ts", + "line": 1296, + "position": 46509, + "operator": "field-drop", + "original": "getMeta: () => ({\n tableName: viewName,\n trackedTableName,\n metadataIsTracked,\n serializeValue: (value) =>\n serializeForSQLite(value, table, serializer),\n })", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "getMeta", + "key": "src/powersync.ts#getMeta:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + { + "id": "powersync.ts:M565", + "number": 565, + "source": "src/powersync.ts", + "line": 1297, + "position": 46535, + "operator": "field-drop", + "original": "tableName: viewName", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "getMeta", + "key": "src/powersync.ts#getMeta:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~2" + }, + { + "id": "powersync.ts:M566", + "number": 566, + "source": "src/powersync.ts", + "line": 1298, + "position": 46564, + "operator": "field-drop", + "original": "trackedTableName", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "getMeta", + "key": "src/powersync.ts#getMeta:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~3" + }, + { + "id": "powersync.ts:M567", + "number": 567, + "source": "src/powersync.ts", + "line": 1299, + "position": 46590, + "operator": "field-drop", + "original": "metadataIsTracked", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "getMeta", + "key": "src/powersync.ts#getMeta:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~4" + }, + { + "id": "powersync.ts:M568", + "number": 568, + "source": "src/powersync.ts", + "line": 1300, + "position": 46617, + "operator": "field-drop", + "original": "serializeValue: (value) =>\n serializeForSQLite(value, table, serializer)", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "serializeValue", + "key": "src/powersync.ts#serializeValue:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + { + "id": "powersync.ts:M569", + "number": 569, + "source": "src/powersync.ts", + "line": 1301, + "position": 46654, + "operator": "value-undefined", + "original": "serializeForSQLite(value, table, serializer)", + "replacement": "undefined", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "serializeValue", + "key": "src/powersync.ts#serializeValue:value-undefined:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + } + ], + "selected": [ + { + "id": "powersync.ts:M40", + "number": 40, + "source": "src/powersync.ts", + "line": 358, + "position": 11958, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "key": "src/powersync.ts#sync:binary-swap:const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "span": { + "start": 11958, + "end": 11960 + }, + "execution": { + "id": "powersync.ts:M40", + "number": 40, + "source": "src/powersync.ts", + "line": 358, + "position": 11958, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "crashed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.powersync.controlled > fixed seed 2367", + "adapter-conformance.persisted-baseline-lifecycle.powersync.controlled > random seed" + ], + "detail": "Error: Persisted startup driver did not reach its premise", + "seconds": 44.67125 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M41", + "number": 41, + "source": "src/powersync.ts", + "line": 359, + "position": 11969, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "key": "src/powersync.ts#sync:drop-not:const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "span": { + "start": 11969, + "end": 11970 + }, + "execution": { + "id": "powersync.ts:M41", + "number": 41, + "source": "src/powersync.ts", + "line": 359, + "position": 11969, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 44.67125, + "changedTests": [], + "class": "no-observed-difference" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M79", + "number": 79, + "source": "src/powersync.ts", + "line": 482, + "position": 16200, + "operator": "value-undefined", + "original": "writeType(row.id)", + "replacement": "undefined", + "anchor": "const type = writeType ( row . id )", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:const type = writeType ( row . id )", + "span": { + "start": 16200, + "end": 16217 + }, + "execution": { + "id": "powersync.ts:M79", + "number": 79, + "source": "src/powersync.ts", + "line": 482, + "position": 16200, + "operator": "value-undefined", + "original": "writeType(row.id)", + "replacement": "undefined", + "anchor": "const type = writeType ( row . id )", + "function": "apply", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 44.67125, + "changedTests": [], + "class": "no-observed-difference" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M80", + "number": 80, + "source": "src/powersync.ts", + "line": 483, + "position": 16250, + "operator": "value-undefined", + "original": "deserializeSyncRow(row)", + "replacement": "undefined", + "anchor": "const value = deserializeSyncRow ( row )", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:const value = deserializeSyncRow ( row )", + "span": { + "start": 16250, + "end": 16273 + }, + "execution": { + "id": "powersync.ts:M80", + "number": 80, + "source": "src/powersync.ts", + "line": 483, + "position": 16250, + "operator": "value-undefined", + "original": "deserializeSyncRow(row)", + "replacement": "undefined", + "anchor": "const value = deserializeSyncRow ( row )", + "function": "apply", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 77.416682249 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M81", + "number": 81, + "source": "src/powersync.ts", + "line": 484, + "position": 16292, + "operator": "delete-call", + "original": "onInitialRow?.(value)", + "replacement": ";", + "anchor": "onInitialRow ?. ( value )", + "function": "apply", + "key": "src/powersync.ts#apply:delete-call:onInitialRow ?. ( value )", + "span": { + "start": 16292, + "end": 16313 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M82", + "number": 82, + "source": "src/powersync.ts", + "line": 485, + "position": 16332, + "operator": "delete-call", + "original": "write({ type, value })", + "replacement": ";", + "anchor": "write ( { type , value } )", + "function": "apply", + "key": "src/powersync.ts#apply:delete-call:write ( { type , value } )", + "span": { + "start": 16332, + "end": 16354 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M83", + "number": 83, + "source": "src/powersync.ts", + "line": 485, + "position": 16340, + "operator": "field-drop", + "original": "type", + "replacement": "", + "anchor": "write ( { type , value } )", + "function": "apply", + "key": "src/powersync.ts#apply:field-drop:write ( { type , value } )", + "span": { + "start": 16340, + "end": 16344 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M84", + "number": 84, + "source": "src/powersync.ts", + "line": 485, + "position": 16346, + "operator": "field-drop", + "original": "value", + "replacement": "", + "anchor": "write ( { type , value } )", + "function": "apply", + "key": "src/powersync.ts#apply:field-drop:write ( { type , value } )~2", + "span": { + "start": 16346, + "end": 16351 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M85", + "number": 85, + "source": "src/powersync.ts", + "line": 487, + "position": 16405, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const applied = commit ( )", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:const applied = commit ( )", + "span": { + "start": 16405, + "end": 16413 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M86", + "number": 86, + "source": "src/powersync.ts", + "line": 488, + "position": 16434, + "operator": "if-true", + "original": "applied !== true", + "replacement": "true", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:if-true:if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "span": { + "start": 16434, + "end": 16450 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M87", + "number": 87, + "source": "src/powersync.ts", + "line": 488, + "position": 16434, + "operator": "if-false", + "original": "applied !== true", + "replacement": "false", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:if-false:if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "span": { + "start": 16434, + "end": 16450 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M88", + "number": 88, + "source": "src/powersync.ts", + "line": 488, + "position": 16442, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:binary-swap:if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "span": { + "start": 16442, + "end": 16445 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M89", + "number": 89, + "source": "src/powersync.ts", + "line": 488, + "position": 16446, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:bool-flip:if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "span": { + "start": 16446, + "end": 16450 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M90", + "number": 90, + "source": "src/powersync.ts", + "line": 488, + "position": 16457, + "operator": "value-undefined", + "original": "applied.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void applied . catch ( ( ) => undefined )", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:void applied . catch ( ( ) => undefined )", + "span": { + "start": 16457, + "end": 16487 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M91", + "number": 91, + "source": "src/powersync.ts", + "line": 489, + "position": 16504, + "operator": "delete-call", + "original": "appliedReceipts.push(applied)", + "replacement": ";", + "anchor": "appliedReceipts . push ( applied )", + "function": "apply", + "key": "src/powersync.ts#apply:delete-call:appliedReceipts . push ( applied )", + "span": { + "start": 16504, + "end": 16533 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M102", + "number": 102, + "source": "src/powersync.ts", + "line": 505, + "position": 17095, + "operator": "if-true", + "original": "afterRead", + "replacement": "true", + "anchor": "if ( afterRead ) await afterRead ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-true:if ( afterRead ) await afterRead ( )", + "span": { + "start": 17095, + "end": 17104 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M103", + "number": 103, + "source": "src/powersync.ts", + "line": 505, + "position": 17095, + "operator": "if-false", + "original": "afterRead", + "replacement": "false", + "anchor": "if ( afterRead ) await afterRead ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-false:if ( afterRead ) await afterRead ( )", + "span": { + "start": 17095, + "end": 17104 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M104", + "number": 104, + "source": "src/powersync.ts", + "line": 505, + "position": 17112, + "operator": "value-undefined", + "original": "afterRead()", + "replacement": "undefined", + "anchor": "await afterRead ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:value-undefined:await afterRead ( )", + "span": { + "start": 17112, + "end": 17123 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M172", + "number": 172, + "source": "src/powersync.ts", + "line": 653, + "position": 22027, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "span": { + "start": 22027, + "end": 22029 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M173", + "number": 173, + "source": "src/powersync.ts", + "line": 653, + "position": 22030, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:drop-not:const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "span": { + "start": 22030, + "end": 22031 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M175", + "number": 175, + "source": "src/powersync.ts", + "line": 658, + "position": 22303, + "operator": "field-drop", + "original": "snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "span": { + "start": 22303, + "end": 22465 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M176", + "number": 176, + "source": "src/powersync.ts", + "line": 659, + "position": 22341, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "span": { + "start": 22341, + "end": 22344 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M177", + "number": 177, + "source": "src/powersync.ts", + "line": 660, + "position": 22370, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:drop-not:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "span": { + "start": 22370, + "end": 22371 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M178", + "number": 178, + "source": "src/powersync.ts", + "line": 662, + "position": 22481, + "operator": "field-drop", + "original": "afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: `delete`, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~2", + "span": { + "start": 22481, + "end": 23966 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M179", + "number": 179, + "source": "src/powersync.ts", + "line": 663, + "position": 22515, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~2", + "span": { + "start": 22515, + "end": 22517 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M180", + "number": 180, + "source": "src/powersync.ts", + "line": 665, + "position": 22592, + "operator": "value-undefined", + "original": "persistence.hydrateBaseline()", + "replacement": "undefined", + "anchor": "await persistence . hydrateBaseline ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await persistence . hydrateBaseline ( )", + "span": { + "start": 22592, + "end": 22621 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M181", + "number": 181, + "source": "src/powersync.ts", + "line": 666, + "position": 22648, + "operator": "if-true", + "original": "abortController.signal.aborted", + "replacement": "true", + "anchor": "if ( abortController . signal . aborted ) return", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( abortController . signal . aborted ) return", + "span": { + "start": 22648, + "end": 22678 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M182", + "number": 182, + "source": "src/powersync.ts", + "line": 666, + "position": 22648, + "operator": "if-false", + "original": "abortController.signal.aborted", + "replacement": "false", + "anchor": "if ( abortController . signal . aborted ) return", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( abortController . signal . aborted ) return", + "span": { + "start": 22648, + "end": 22678 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M183", + "number": 183, + "source": "src/powersync.ts", + "line": 673, + "position": 23076, + "operator": "value-undefined", + "original": "collection._state.acceptedSyncedEntries()", + "replacement": "undefined", + "anchor": "for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) }", + "span": { + "start": 23076, + "end": 23117 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M184", + "number": 184, + "source": "src/powersync.ts", + "line": 674, + "position": 23149, + "operator": "if-true", + "original": "!listed.has(key)", + "replacement": "true", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( ! listed . has ( key ) ) gone . push ( key )", + "span": { + "start": 23149, + "end": 23165 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M185", + "number": 185, + "source": "src/powersync.ts", + "line": 674, + "position": 23149, + "operator": "if-false", + "original": "!listed.has(key)", + "replacement": "false", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( ! listed . has ( key ) ) gone . push ( key )", + "span": { + "start": 23149, + "end": 23165 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M186", + "number": 186, + "source": "src/powersync.ts", + "line": 674, + "position": 23149, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:drop-not:if ( ! listed . has ( key ) ) gone . push ( key )", + "span": { + "start": 23149, + "end": 23150 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M187", + "number": 187, + "source": "src/powersync.ts", + "line": 674, + "position": 23150, + "operator": "value-undefined", + "original": "listed.has(key)", + "replacement": "undefined", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:if ( ! listed . has ( key ) ) gone . push ( key )", + "span": { + "start": 23150, + "end": 23165 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M188", + "number": 188, + "source": "src/powersync.ts", + "line": 674, + "position": 23167, + "operator": "delete-call", + "original": "gone.push(key)", + "replacement": ";", + "anchor": "gone . push ( key )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:gone . push ( key )", + "span": { + "start": 23167, + "end": 23181 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M189", + "number": 189, + "source": "src/powersync.ts", + "line": 676, + "position": 23228, + "operator": "delete-call", + "original": "listed.clear()", + "replacement": ";", + "anchor": "listed . clear ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:listed . clear ( )", + "span": { + "start": 23228, + "end": 23242 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M190", + "number": 190, + "source": "src/powersync.ts", + "line": 679, + "position": 23334, + "operator": "loop-false", + "original": "first < gone.length", + "replacement": "false", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:loop-false:for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) }", + "span": { + "start": 23334, + "end": 23353 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M191", + "number": 191, + "source": "src/powersync.ts", + "line": 679, + "position": 23340, + "operator": "binary-swap", + "original": "<", + "replacement": "<=", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) }", + "span": { + "start": 23340, + "end": 23341 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M192", + "number": 192, + "source": "src/powersync.ts", + "line": 682, + "position": 23452, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:begin ( )", + "span": { + "start": 23452, + "end": 23459 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M193", + "number": 193, + "source": "src/powersync.ts", + "line": 683, + "position": 23502, + "operator": "value-undefined", + "original": "gone.slice(\n first,\n first + syncBatchSize,\n )", + "replacement": "undefined", + "anchor": "for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } )", + "span": { + "start": 23502, + "end": 23621 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M194", + "number": 194, + "source": "src/powersync.ts", + "line": 687, + "position": 23649, + "operator": "delete-call", + "original": "write({ type: `delete`, key })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key } )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:write ( { type : `delete` , key } )", + "span": { + "start": 23649, + "end": 23679 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M195", + "number": 195, + "source": "src/powersync.ts", + "line": 687, + "position": 23657, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:write ( { type : `delete` , key } )", + "span": { + "start": 23657, + "end": 23671 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M196", + "number": 196, + "source": "src/powersync.ts", + "line": 687, + "position": 23673, + "operator": "field-drop", + "original": "key", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:write ( { type : `delete` , key } )~2", + "span": { + "start": 23673, + "end": 23676 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M197", + "number": 197, + "source": "src/powersync.ts", + "line": 688, + "position": 23720, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const applied = commit ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const applied = commit ( )", + "span": { + "start": 23720, + "end": 23728 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M198", + "number": 198, + "source": "src/powersync.ts", + "line": 689, + "position": 23757, + "operator": "if-true", + "original": "applied !== true", + "replacement": "true", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "span": { + "start": 23757, + "end": 23773 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M199", + "number": 199, + "source": "src/powersync.ts", + "line": 689, + "position": 23757, + "operator": "if-false", + "original": "applied !== true", + "replacement": "false", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "span": { + "start": 23757, + "end": 23773 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M200", + "number": 200, + "source": "src/powersync.ts", + "line": 689, + "position": 23765, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "span": { + "start": 23765, + "end": 23768 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M201", + "number": 201, + "source": "src/powersync.ts", + "line": 689, + "position": 23769, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:bool-flip:if ( applied !== true ) void applied . catch ( ( ) => undefined )", + "span": { + "start": 23769, + "end": 23773 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M202", + "number": 202, + "source": "src/powersync.ts", + "line": 690, + "position": 23806, + "operator": "value-undefined", + "original": "applied.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void applied . catch ( ( ) => undefined )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:void applied . catch ( ( ) => undefined )", + "span": { + "start": 23806, + "end": 23836 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M203", + "number": 203, + "source": "src/powersync.ts", + "line": 691, + "position": 23861, + "operator": "delete-call", + "original": "appliedReceipts.push(applied)", + "replacement": ";", + "anchor": "appliedReceipts . push ( applied )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:appliedReceipts . push ( applied )", + "span": { + "start": 23861, + "end": 23890 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M208", + "number": 208, + "source": "src/powersync.ts", + "line": 700, + "position": 24186, + "operator": "field-drop", + "original": "onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~7", + "span": { + "start": 24186, + "end": 24345 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "powersync.ts:M209", + "number": 209, + "source": "src/powersync.ts", + "line": 702, + "position": 24258, + "operator": "delete-call", + "original": "listed.add(asPowerSyncRecord(value).id)", + "replacement": ";", + "anchor": "listed . add ( asPowerSyncRecord ( value ) . id )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:listed . add ( asPowerSyncRecord ( value ) . id )", + "span": { + "start": 24258, + "end": 24297 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + } + ], + "omitted": [ + { + "operator": { + "id": "powersync.ts:M1", + "number": 1, + "source": "src/powersync.ts", + "line": 44, + "position": 1465, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M2", + "number": 2, + "source": "src/powersync.ts", + "line": 43, + "position": 1399, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M3", + "number": 3, + "source": "src/powersync.ts", + "line": 43, + "position": 1391, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:drop-not:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M4", + "number": 4, + "source": "src/powersync.ts", + "line": 43, + "position": 1392, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:drop-not:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M5", + "number": 5, + "source": "src/powersync.ts", + "line": 44, + "position": 1433, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M6", + "number": 6, + "source": "src/powersync.ts", + "line": 44, + "position": 1420, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M7", + "number": 7, + "source": "src/powersync.ts", + "line": 44, + "position": 1449, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~5" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M8", + "number": 8, + "source": "src/powersync.ts", + "line": 45, + "position": 1514, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )", + "function": "isPromiseLike", + "key": "src/powersync.ts#isPromiseLike:binary-swap:return ( ! ! value && ( typeof value === `object` || typeof value === `function` ) && typeof ( value as { then ? : unknown } ) . then === `function` )~6" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M9", + "number": 9, + "source": "src/powersync.ts", + "line": 243, + "position": 7926, + "operator": "value-undefined", + "original": "createPowerSyncCollectionConfig(config)", + "replacement": "undefined", + "anchor": "const outputConfig = createPowerSyncCollectionConfig ( config )", + "function": "powerSyncCollectionOptions", + "key": "src/powersync.ts#powerSyncCollectionOptions:value-undefined:const outputConfig = createPowerSyncCollectionConfig ( config )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M10", + "number": 10, + "source": "src/powersync.ts", + "line": 244, + "position": 7975, + "operator": "value-undefined", + "original": "withCollectionConfigFactory(outputConfig, () =>\n createPowerSyncCollectionConfig(config),\n )", + "replacement": "undefined", + "anchor": "return withCollectionConfigFactory ( outputConfig , ( ) => createPowerSyncCollectionConfig ( config ) , )", + "function": "powerSyncCollectionOptions", + "key": "src/powersync.ts#powerSyncCollectionOptions:value-undefined:return withCollectionConfigFactory ( outputConfig , ( ) => createPowerSyncCollectionConfig ( config ) , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M11", + "number": 11, + "source": "src/powersync.ts", + "line": 245, + "position": 8027, + "operator": "value-undefined", + "original": "createPowerSyncCollectionConfig(config)", + "replacement": "undefined", + "anchor": "return withCollectionConfigFactory ( outputConfig , ( ) => createPowerSyncCollectionConfig ( config ) , )", + "function": "powerSyncCollectionOptions", + "key": "src/powersync.ts#powerSyncCollectionOptions:value-undefined:return withCollectionConfigFactory ( outputConfig , ( ) => createPowerSyncCollectionConfig ( config ) , )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M12", + "number": 12, + "source": "src/powersync.ts", + "line": 282, + "position": 9125, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "const validationSchema = deserializationSchema || schema", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:binary-swap:const validationSchema = deserializationSchema || schema" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M13", + "number": 13, + "source": "src/powersync.ts", + "line": 283, + "position": 9158, + "operator": "value-undefined", + "original": "validationSchema[`~standard`].validate(value)", + "replacement": "undefined", + "anchor": "const validation = validationSchema [ `~standard` ] . validate ( value )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:value-undefined:const validation = validationSchema [ `~standard` ] . validate ( value )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M14", + "number": 14, + "source": "src/powersync.ts", + "line": 284, + "position": 9212, + "operator": "if-true", + "original": "`value` in validation", + "replacement": "true", + "anchor": "if ( `value` in validation ) { return validation . value } else if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:if-true:if ( `value` in validation ) { return validation . value } else if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M15", + "number": 15, + "source": "src/powersync.ts", + "line": 284, + "position": 9212, + "operator": "if-false", + "original": "`value` in validation", + "replacement": "false", + "anchor": "if ( `value` in validation ) { return validation . value } else if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:if-false:if ( `value` in validation ) { return validation . value } else if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M16", + "number": 16, + "source": "src/powersync.ts", + "line": 286, + "position": 9282, + "operator": "if-true", + "original": "`issues` in validation", + "replacement": "true", + "anchor": "if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:if-true:if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M17", + "number": 17, + "source": "src/powersync.ts", + "line": 286, + "position": 9282, + "operator": "if-false", + "original": "`issues` in validation", + "replacement": "false", + "anchor": "if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:if-false:if ( `issues` in validation ) { const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}` database . logger . log ( { level : LogLevels . error , message : issueMessage } ) onDeserializationError ! ( validation ) throw new Error ( issueMessage ) } else { const unknownErrorMessage = `Unknown deserialization error for ${ viewName } `\n database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M18", + "number": 18, + "source": "src/powersync.ts", + "line": 287, + "position": 9396, + "operator": "value-undefined", + "original": "validation.issues.map((issue) => `${issue.path} - ${issue.message}`)", + "replacement": "undefined", + "anchor": "const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}`", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:value-undefined:const issueMessage = `Failed to validate incoming data for ${ viewName } . Issues : $ { validation . issues . map ( ( issue ) => `${ issue . path } - $ { issue . message } `)}`" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M19", + "number": 19, + "source": "src/powersync.ts", + "line": 288, + "position": 9473, + "operator": "delete-call", + "original": "database.logger.log({ level: LogLevels.error, message: issueMessage })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : issueMessage } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:delete-call:database . logger . log ( { level : LogLevels . error , message : issueMessage } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M20", + "number": 20, + "source": "src/powersync.ts", + "line": 288, + "position": 9495, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : issueMessage } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:database . logger . log ( { level : LogLevels . error , message : issueMessage } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M21", + "number": 21, + "source": "src/powersync.ts", + "line": 288, + "position": 9519, + "operator": "field-drop", + "original": "message: issueMessage", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : issueMessage } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:database . logger . log ( { level : LogLevels . error , message : issueMessage } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M22", + "number": 22, + "source": "src/powersync.ts", + "line": 289, + "position": 9550, + "operator": "delete-call", + "original": "onDeserializationError!(validation)", + "replacement": ";", + "anchor": "onDeserializationError ! ( validation )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:delete-call:onDeserializationError ! ( validation )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M23", + "number": 23, + "source": "src/powersync.ts", + "line": 293, + "position": 9723, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: unknownErrorMessage,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:delete-call:database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M24", + "number": 24, + "source": "src/powersync.ts", + "line": 294, + "position": 9753, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M25", + "number": 25, + "source": "src/powersync.ts", + "line": 295, + "position": 9785, + "operator": "field-drop", + "original": "message: unknownErrorMessage", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:database . logger . log ( { level : LogLevels . error , message : unknownErrorMessage , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M26", + "number": 26, + "source": "src/powersync.ts", + "line": 297, + "position": 9830, + "operator": "delete-call", + "original": "onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })", + "replacement": ";", + "anchor": "onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:delete-call:onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M27", + "number": 27, + "source": "src/powersync.ts", + "line": 297, + "position": 9856, + "operator": "field-drop", + "original": "issues: [{ message: unknownErrorMessage }]", + "replacement": "", + "anchor": "onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M28", + "number": 28, + "source": "src/powersync.ts", + "line": 297, + "position": 9867, + "operator": "field-drop", + "original": "message: unknownErrorMessage", + "replacement": "", + "anchor": "onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )", + "function": "deserializeSyncRow", + "key": "src/powersync.ts#deserializeSyncRow:field-drop:onDeserializationError ! ( { issues : [ { message : unknownErrorMessage } ] } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M29", + "number": 29, + "source": "src/powersync.ts", + "line": 303, + "position": 10079, + "operator": "binary-swap", + "original": "??", + "replacement": "&&", + "anchor": "const schema = inputSchema ?? ( convertTableToSchema ( table ) as TSchema )", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:binary-swap:const schema = inputSchema ?? ( convertTableToSchema ( table ) as TSchema )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M30", + "number": 30, + "source": "src/powersync.ts", + "line": 303, + "position": 10083, + "operator": "value-undefined", + "original": "convertTableToSchema(table)", + "replacement": "undefined", + "anchor": "const schema = inputSchema ?? ( convertTableToSchema ( table ) as TSchema )", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:value-undefined:const schema = inputSchema ?? ( convertTableToSchema ( table ) as TSchema )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M31", + "number": 31, + "source": "src/powersync.ts", + "line": 316, + "position": 10762, + "operator": "value-undefined", + "original": "Math.floor(\n Math.random() * 0xffffffff,\n )\n .toString(16)\n .padStart(8, `0`)", + "replacement": "undefined", + "anchor": "const trackedTableName = `__${ viewName } _tracking_$ { Math . floor ( Math . random ( ) * 0xffffffff , ) . toString ( 16 ) . padStart ( 8 , `0` ) } `", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:value-undefined:const trackedTableName = `__${ viewName } _tracking_$ { Math . floor ( Math . random ( ) * 0xffffffff , ) . toString ( 16 ) . padStart ( 8 , `0` ) } `" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M32", + "number": 32, + "source": "src/powersync.ts", + "line": 317, + "position": 10778, + "operator": "value-undefined", + "original": "Math.random()", + "replacement": "undefined", + "anchor": "const trackedTableName = `__${ viewName } _tracking_$ { Math . floor ( Math . random ( ) * 0xffffffff , ) . toString ( 16 ) . padStart ( 8 , `0` ) } `", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:value-undefined:const trackedTableName = `__${ viewName } _tracking_$ { Math . floor ( Math . random ( ) * 0xffffffff , ) . toString ( 16 ) . padStart ( 8 , `0` ) } `~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M33", + "number": 33, + "source": "src/powersync.ts", + "line": 323, + "position": 10904, + "operator": "field-drop", + "original": "database", + "replacement": "", + "anchor": "const transactor = new PowerSyncTransactor ( { database , } )", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const transactor = new PowerSyncTransactor ( { database , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M34", + "number": 34, + "source": "src/powersync.ts", + "line": 332, + "position": 11116, + "operator": "field-drop", + "original": "sync: (params) => {\n const persistence =\n params.metadata === undefined\n ? null\n : validateSyncPersistenceCapability(params.metadata.persistence)\n const {\n begin,\n write,\n collection,\n commit,\n fail,\n reportError,\n reportHealthy,\n } = params\n // Readiness of this sync run: a reload runs `sync` again while the\n // Collection still shows the previous run's status.\n let ready = false\n const markReady = () => {\n ready = true\n params.markReady()\n }\n const abortController = new AbortController()\n\n // Persisted first startup keeps ordinary batches even after timeout\n // fallback. A retained reload keeps its atomic replacement.\n const replacesRetainedRows =\n collection.health.reloading &&\n !collection._state.acceptedSyncedEntries().next().done\n let disposeTracking:\n ((options?: { context?: LockContext }) => Promise) | null = null\n let trackingSetup: Promise | null = null\n let activeTrackingDisposal: Promise | null = null\n let abortTrackingDisposal: Promise | null = null\n\n if (syncMode === `eager`) {\n return runEagerSync()\n } else {\n return runOnDemandSync()\n }\n\n /**\n * Disposes the current diff trigger, if one is active, and clears the\n * tracking state.\n */\n async function safelyDisposeTracking(\n context?: LockContext,\n ): Promise {\n // Cleanup can race trigger creation. Wait until the disposer has been\n // published so an abort cannot strand a freshly-created trigger.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n\n const dispose = disposeTracking\n if (!dispose) {\n // Reconciliation may have claimed the disposer after this caller\n // observed setup. Join that invocation instead of settling early.\n await activeTrackingDisposal\n return\n }\n\n disposeTracking = null\n let disposal: Promise\n try {\n disposal = Promise.resolve(dispose(context ? { context } : undefined))\n } catch (error) {\n disposal = Promise.reject(error)\n }\n activeTrackingDisposal = disposal\n try {\n await disposal\n } finally {\n if (activeTrackingDisposal === disposal) {\n activeTrackingDisposal = null\n }\n }\n }\n\n function disposeTrackingAfterAbort(): Promise {\n const disposal = (abortTrackingDisposal ??= safelyDisposeTracking())\n // Abort event dispatch does not observe a listener's returned promise.\n // The eager cleanup callback awaits this same promise below.\n void disposal.catch(() => undefined)\n return disposal\n }\n\n async function establishTracking(\n options: Parameters[0],\n appliedReceipts: Array,\n ): Promise {\n const setup = (async () => {\n const dispose = await createDiffTrigger(options, appliedReceipts)\n disposeTracking = dispose\n })()\n trackingSetup = setup\n\n try {\n await setup\n } finally {\n if (trackingSetup === setup) {\n trackingSetup = null\n }\n }\n }\n\n async function createDiffTrigger(\n options: {\n setupContext?: LockContext\n // The load reads every row and replaces the rows the Collection\n // holds.\n snapshot?: boolean\n afterRead?: () => Promise\n onInitialRow?: (value: OutputType) => void\n when: Record\n writeType: (rowId: string) => OperationType\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) => Promise>\n },\n appliedReceipts: Array,\n ) {\n const {\n setupContext,\n snapshot,\n afterRead,\n onInitialRow,\n when,\n writeType,\n batchQuery,\n } = options\n\n return await database.triggers.createDiffTrigger({\n source: viewName,\n destination: trackedTableName,\n columns: table.columns.map((column) => column.name),\n setupContext,\n when,\n hooks: {\n beforeCreate: async (context) => {\n // A retained reload uses one snapshot across its source reads.\n // Other initial loads and subset loads keep batch commits. A\n // transaction begins only once its rows are read, so a batch\n // that fails leaves none open.\n const batches: Array> = []\n const apply = () => {\n begin({ snapshot })\n for (const row of batches.splice(0).flat()) {\n const type = writeType(row.id)\n const value = deserializeSyncRow(row)\n onInitialRow?.(value)\n write({ type, value })\n }\n const applied = commit()\n if (applied !== true) void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n batches.push(batchItems)\n if (!snapshot) apply()\n }\n if (snapshot) apply()\n if (afterRead) await afterRead()\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n `SELECT * FROM ${trackedTableName} ORDER BY operation_id ASC`,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(`DELETE FROM ${trackedTableName}`)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is starting for ${viewName} into ${trackedTableName}`,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n `abort`,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: `delete`, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === `rejected` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n `PowerSync tracking disposal and load-hook cleanup both failed`,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === `rejected`) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: `delete`, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: `Could not clean up subset hook for ${viewName}`,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, `PowerSync cleanup failed`, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: `delete`, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: `Could not release subset tracking for ${viewName}; retrying`,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === `rejected`) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === `rejected`\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n }", + "replacement": "", + "anchor": "const sync : SyncConfig < OutputType , string > = { sync : ( params ) => { const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const { begin , write , collection , commit , fail , reportError , reportHealthy , } = params let ready = false const markReady = ( ) => { ready = true params . markReady ( ) } const abortController = new AbortController ( ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let disposeTracking : ( ( options ? : { context ? : LockContext } ) => Promise < void > ) | null = null let trackingSetup : Promise < void > | null = null let activeTrackingDisposal : Promise < void > | null = null let abortTrackingDisposal : Promise < void > | null = null if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) } async function safelyDisposeTracking ( context ? : LockContext , ) : Promise < void > { const setup = trackingSetup if ( setup ) { await setup . catch ( ( ) => undefined ) } const dispose = disposeTracking if ( ! dispose ) { await activeTrackingDisposal return } disposeTracking = null let disposal : Promise < void > try { disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) ) } catch ( error ) { disposal = Promise . reject ( error ) } activeTrackingDisposal = disposal try { await disposal } finally { if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null } } } function disposeTrackingAfterAbort ( ) : Promise < void > { const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) ) void disposal . catch ( ( ) => undefined ) return disposal } async function establishTracking ( options : Parameters < typeof createDiffTrigger > [ 0 ] , appliedReceipts : Array < SyncAppliedReceipt > , ) : Promise < void > { const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( ) trackingSetup = setup try { await setup } finally { if ( trackingSetup === setup ) { trackingSetup = null } } } async function createDiffTrigger ( options : { setupContext ? : LockContext snapshot ? : boolean afterRead ? : ( ) => Promise < void > onInitialRow ? : ( value : OutputType ) => void when : Record < DiffTriggerOperation , string > writeType : ( rowId : string ) => OperationType batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => Promise < Array < TableType >> } , appliedReceipts : Array < SyncAppliedReceipt > , ) { const { setupContext , snapshot , afterRead , onInitialRow , when , writeType , batchQuery , } = options return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n ` SELECT * FROM $ { trackedTableName } ORDER BY operation_id ASC `,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(` DELETE FROM $ { trackedTableName } `)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync is starting for $ { viewName } into $ { trackedTableName } `,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n ` abort `,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: ` delete `, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: ` TRUE `,\n [DiffTriggerOperation.UPDATE]: ` TRUE `,\n [DiffTriggerOperation.DELETE]: ` TRUE `,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL` SELECT * FROM $ { viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === ` rejected ` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n ` PowerSync tracking disposal and load - hook cleanup both failed `,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === ` rejected `) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: ` ( $ { newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not clean up subset hook for $ { viewName } `,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, ` PowerSync cleanup failed `, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: ` delete `, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not release subset tracking for $ { viewName } ; retrying `,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }", + "function": "sync", + "key": "src/powersync.ts#sync:field-drop:const sync : SyncConfig < OutputType , string > = { sync : ( params ) => { const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const { begin , write , collection , commit , fail , reportError , reportHealthy , } = params let ready = false const markReady = ( ) => { ready = true params . markReady ( ) } const abortController = new AbortController ( ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let disposeTracking : ( ( options ? : { context ? : LockContext } ) => Promise < void > ) | null = null let trackingSetup : Promise < void > | null = null let activeTrackingDisposal : Promise < void > | null = null let abortTrackingDisposal : Promise < void > | null = null if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) } async function safelyDisposeTracking ( context ? : LockContext , ) : Promise < void > { const setup = trackingSetup if ( setup ) { await setup . catch ( ( ) => undefined ) } const dispose = disposeTracking if ( ! dispose ) { await activeTrackingDisposal return } disposeTracking = null let disposal : Promise < void > try { disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) ) } catch ( error ) { disposal = Promise . reject ( error ) } activeTrackingDisposal = disposal try { await disposal } finally { if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null } } } function disposeTrackingAfterAbort ( ) : Promise < void > { const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) ) void disposal . catch ( ( ) => undefined ) return disposal } async function establishTracking ( options : Parameters < typeof createDiffTrigger > [ 0 ] , appliedReceipts : Array < SyncAppliedReceipt > , ) : Promise < void > { const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( ) trackingSetup = setup try { await setup } finally { if ( trackingSetup === setup ) { trackingSetup = null } } } async function createDiffTrigger ( options : { setupContext ? : LockContext snapshot ? : boolean afterRead ? : ( ) => Promise < void > onInitialRow ? : ( value : OutputType ) => void when : Record < DiffTriggerOperation , string > writeType : ( rowId : string ) => OperationType batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => Promise < Array < TableType >> } , appliedReceipts : Array < SyncAppliedReceipt > , ) { const { setupContext , snapshot , afterRead , onInitialRow , when , writeType , batchQuery , } = options return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n ` SELECT * FROM $ { trackedTableName } ORDER BY operation_id ASC `,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(` DELETE FROM $ { trackedTableName } `)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync is starting for $ { viewName } into $ { trackedTableName } `,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n ` abort `,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: ` delete `, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: ` TRUE `,\n [DiffTriggerOperation.UPDATE]: ` TRUE `,\n [DiffTriggerOperation.DELETE]: ` TRUE `,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL` SELECT * FROM $ { viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === ` rejected ` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n ` PowerSync tracking disposal and load - hook cleanup both failed `,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === ` rejected `) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: ` ( $ { newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not clean up subset hook for $ { viewName } `,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, ` PowerSync cleanup failed `, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: ` delete `, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not release subset tracking for $ { viewName } ; retrying `,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M35", + "number": 35, + "source": "src/powersync.ts", + "line": 334, + "position": 11186, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "function": "sync", + "key": "src/powersync.ts#sync:binary-swap:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M36", + "number": 36, + "source": "src/powersync.ts", + "line": 336, + "position": 11229, + "operator": "value-undefined", + "original": "validateSyncPersistenceCapability(params.metadata.persistence)", + "replacement": "undefined", + "anchor": "const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "function": "sync", + "key": "src/powersync.ts#sync:value-undefined:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M37", + "number": 37, + "source": "src/powersync.ts", + "line": 348, + "position": 11598, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let ready = false", + "function": "sync", + "key": "src/powersync.ts#sync:bool-flip:let ready = false" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M38", + "number": 38, + "source": "src/powersync.ts", + "line": 350, + "position": 11652, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "ready = true", + "function": "markReady", + "key": "src/powersync.ts#markReady:bool-flip:ready = true" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M39", + "number": 39, + "source": "src/powersync.ts", + "line": 351, + "position": 11665, + "operator": "delete-call", + "original": "params.markReady()", + "replacement": ";", + "anchor": "params . markReady ( )", + "function": "markReady", + "key": "src/powersync.ts#markReady:delete-call:params . markReady ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M42", + "number": 42, + "source": "src/powersync.ts", + "line": 366, + "position": 12317, + "operator": "if-true", + "original": "syncMode === `eager`", + "replacement": "true", + "anchor": "if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }", + "function": "sync", + "key": "src/powersync.ts#sync:if-true:if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M43", + "number": 43, + "source": "src/powersync.ts", + "line": 366, + "position": 12317, + "operator": "if-false", + "original": "syncMode === `eager`", + "replacement": "false", + "anchor": "if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }", + "function": "sync", + "key": "src/powersync.ts#sync:if-false:if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M44", + "number": 44, + "source": "src/powersync.ts", + "line": 366, + "position": 12326, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }", + "function": "sync", + "key": "src/powersync.ts#sync:binary-swap:if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M45", + "number": 45, + "source": "src/powersync.ts", + "line": 367, + "position": 12356, + "operator": "value-undefined", + "original": "runEagerSync()", + "replacement": "undefined", + "anchor": "return runEagerSync ( )", + "function": "sync", + "key": "src/powersync.ts#sync:value-undefined:return runEagerSync ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M46", + "number": 46, + "source": "src/powersync.ts", + "line": 369, + "position": 12401, + "operator": "value-undefined", + "original": "runOnDemandSync()", + "replacement": "undefined", + "anchor": "return runOnDemandSync ( )", + "function": "sync", + "key": "src/powersync.ts#sync:value-undefined:return runOnDemandSync ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M47", + "number": 47, + "source": "src/powersync.ts", + "line": 382, + "position": 12851, + "operator": "if-true", + "original": "setup", + "replacement": "true", + "anchor": "if ( setup ) { await setup . catch ( ( ) => undefined ) }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-true:if ( setup ) { await setup . catch ( ( ) => undefined ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M48", + "number": 48, + "source": "src/powersync.ts", + "line": 382, + "position": 12851, + "operator": "if-false", + "original": "setup", + "replacement": "false", + "anchor": "if ( setup ) { await setup . catch ( ( ) => undefined ) }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-false:if ( setup ) { await setup . catch ( ( ) => undefined ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M49", + "number": 49, + "source": "src/powersync.ts", + "line": 383, + "position": 12876, + "operator": "value-undefined", + "original": "setup.catch(() => undefined)", + "replacement": "undefined", + "anchor": "await setup . catch ( ( ) => undefined )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:value-undefined:await setup . catch ( ( ) => undefined )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M50", + "number": 50, + "source": "src/powersync.ts", + "line": 387, + "position": 12968, + "operator": "if-true", + "original": "!dispose", + "replacement": "true", + "anchor": "if ( ! dispose ) { await activeTrackingDisposal return }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-true:if ( ! dispose ) { await activeTrackingDisposal return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M51", + "number": 51, + "source": "src/powersync.ts", + "line": 387, + "position": 12968, + "operator": "if-false", + "original": "!dispose", + "replacement": "false", + "anchor": "if ( ! dispose ) { await activeTrackingDisposal return }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-false:if ( ! dispose ) { await activeTrackingDisposal return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M52", + "number": 52, + "source": "src/powersync.ts", + "line": 387, + "position": 12968, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! dispose ) { await activeTrackingDisposal return }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:drop-not:if ( ! dispose ) { await activeTrackingDisposal return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M53", + "number": 53, + "source": "src/powersync.ts", + "line": 397, + "position": 13302, + "operator": "value-undefined", + "original": "Promise.resolve(dispose(context ? { context } : undefined))", + "replacement": "undefined", + "anchor": "disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:value-undefined:disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M54", + "number": 54, + "source": "src/powersync.ts", + "line": 397, + "position": 13318, + "operator": "value-undefined", + "original": "dispose(context ? { context } : undefined)", + "replacement": "undefined", + "anchor": "disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:value-undefined:disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M55", + "number": 55, + "source": "src/powersync.ts", + "line": 397, + "position": 13338, + "operator": "field-drop", + "original": "context", + "replacement": "", + "anchor": "disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:field-drop:disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M56", + "number": 56, + "source": "src/powersync.ts", + "line": 399, + "position": 13409, + "operator": "value-undefined", + "original": "Promise.reject(error)", + "replacement": "undefined", + "anchor": "disposal = Promise . reject ( error )", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:value-undefined:disposal = Promise . reject ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M57", + "number": 57, + "source": "src/powersync.ts", + "line": 405, + "position": 13556, + "operator": "if-true", + "original": "activeTrackingDisposal === disposal", + "replacement": "true", + "anchor": "if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-true:if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M58", + "number": 58, + "source": "src/powersync.ts", + "line": 405, + "position": 13556, + "operator": "if-false", + "original": "activeTrackingDisposal === disposal", + "replacement": "false", + "anchor": "if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:if-false:if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M59", + "number": 59, + "source": "src/powersync.ts", + "line": 405, + "position": 13579, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }", + "function": "safelyDisposeTracking", + "key": "src/powersync.ts#safelyDisposeTracking:binary-swap:if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M60", + "number": 60, + "source": "src/powersync.ts", + "line": 412, + "position": 13780, + "operator": "value-undefined", + "original": "safelyDisposeTracking()", + "replacement": "undefined", + "anchor": "const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) )", + "function": "disposeTrackingAfterAbort", + "key": "src/powersync.ts#disposeTrackingAfterAbort:value-undefined:const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M61", + "number": 61, + "source": "src/powersync.ts", + "line": 415, + "position": 13968, + "operator": "value-undefined", + "original": "disposal.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void disposal . catch ( ( ) => undefined )", + "function": "disposeTrackingAfterAbort", + "key": "src/powersync.ts#disposeTrackingAfterAbort:value-undefined:void disposal . catch ( ( ) => undefined )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M62", + "number": 62, + "source": "src/powersync.ts", + "line": 423, + "position": 14230, + "operator": "value-undefined", + "original": "(async () => {\n const dispose = await createDiffTrigger(options, appliedReceipts)\n disposeTracking = dispose\n })()", + "replacement": "undefined", + "anchor": "const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( )", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:value-undefined:const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M63", + "number": 63, + "source": "src/powersync.ts", + "line": 424, + "position": 14277, + "operator": "value-undefined", + "original": "createDiffTrigger(options, appliedReceipts)", + "replacement": "undefined", + "anchor": "const dispose = await createDiffTrigger ( options , appliedReceipts )", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:value-undefined:const dispose = await createDiffTrigger ( options , appliedReceipts )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M64", + "number": 64, + "source": "src/powersync.ts", + "line": 432, + "position": 14471, + "operator": "if-true", + "original": "trackingSetup === setup", + "replacement": "true", + "anchor": "if ( trackingSetup === setup ) { trackingSetup = null }", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:if-true:if ( trackingSetup === setup ) { trackingSetup = null }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M65", + "number": 65, + "source": "src/powersync.ts", + "line": 432, + "position": 14471, + "operator": "if-false", + "original": "trackingSetup === setup", + "replacement": "false", + "anchor": "if ( trackingSetup === setup ) { trackingSetup = null }", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:if-false:if ( trackingSetup === setup ) { trackingSetup = null }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M66", + "number": 66, + "source": "src/powersync.ts", + "line": 432, + "position": 14485, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( trackingSetup === setup ) { trackingSetup = null }", + "function": "establishTracking", + "key": "src/powersync.ts#establishTracking:binary-swap:if ( trackingSetup === setup ) { trackingSetup = null }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M67", + "number": 67, + "source": "src/powersync.ts", + "line": 466, + "position": 15425, + "operator": "value-undefined", + "original": "database.triggers.createDiffTrigger({\n source: viewName,\n destination: trackedTableName,\n columns: table.columns.map((column) => column.name),\n setupContext,\n when,\n hooks: {\n beforeCreate: async (context) => {\n // A retained reload uses one snapshot across its source reads.\n // Other initial loads and subset loads keep batch commits. A\n // transaction begins only once its rows are read, so a batch\n // that fails leaves none open.\n const batches: Array> = []\n const apply = () => {\n begin({ snapshot })\n for (const row of batches.splice(0).flat()) {\n const type = writeType(row.id)\n const value = deserializeSyncRow(row)\n onInitialRow?.(value)\n write({ type, value })\n }\n const applied = commit()\n if (applied !== true) void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n batches.push(batchItems)\n if (!snapshot) apply()\n }\n if (snapshot) apply()\n if (afterRead) await afterRead()\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })\n },\n },\n })", + "replacement": "undefined", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:value-undefined:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M68", + "number": 68, + "source": "src/powersync.ts", + "line": 467, + "position": 15473, + "operator": "field-drop", + "original": "source: viewName", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M69", + "number": 69, + "source": "src/powersync.ts", + "line": 468, + "position": 15501, + "operator": "field-drop", + "original": "destination: trackedTableName", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M70", + "number": 70, + "source": "src/powersync.ts", + "line": 469, + "position": 15542, + "operator": "field-drop", + "original": "columns: table.columns.map((column) => column.name)", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M71", + "number": 71, + "source": "src/powersync.ts", + "line": 469, + "position": 15551, + "operator": "value-undefined", + "original": "table.columns.map((column) => column.name)", + "replacement": "undefined", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:value-undefined:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M72", + "number": 72, + "source": "src/powersync.ts", + "line": 470, + "position": 15605, + "operator": "field-drop", + "original": "setupContext", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M73", + "number": 73, + "source": "src/powersync.ts", + "line": 471, + "position": 15629, + "operator": "field-drop", + "original": "when", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~5" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M74", + "number": 74, + "source": "src/powersync.ts", + "line": 472, + "position": 15645, + "operator": "field-drop", + "original": "hooks: {\n beforeCreate: async (context) => {\n // A retained reload uses one snapshot across its source reads.\n // Other initial loads and subset loads keep batch commits. A\n // transaction begins only once its rows are read, so a batch\n // that fails leaves none open.\n const batches: Array> = []\n const apply = () => {\n begin({ snapshot })\n for (const row of batches.splice(0).flat()) {\n const type = writeType(row.id)\n const value = deserializeSyncRow(row)\n onInitialRow?.(value)\n write({ type, value })\n }\n const applied = commit()\n if (applied !== true) void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n batches.push(batchItems)\n if (!snapshot) apply()\n }\n if (snapshot) apply()\n if (afterRead) await afterRead()\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })\n },\n }", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "createDiffTrigger", + "key": "src/powersync.ts#createDiffTrigger:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })~6" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M75", + "number": 75, + "source": "src/powersync.ts", + "line": 473, + "position": 15666, + "operator": "field-drop", + "original": "beforeCreate: async (context) => {\n // A retained reload uses one snapshot across its source reads.\n // Other initial loads and subset loads keep batch commits. A\n // transaction begins only once its rows are read, so a batch\n // that fails leaves none open.\n const batches: Array> = []\n const apply = () => {\n begin({ snapshot })\n for (const row of batches.splice(0).flat()) {\n const type = writeType(row.id)\n const value = deserializeSyncRow(row)\n onInitialRow?.(value)\n write({ type, value })\n }\n const applied = commit()\n if (applied !== true) void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n batches.push(batchItems)\n if (!snapshot) apply()\n }\n if (snapshot) apply()\n if (afterRead) await afterRead()\n database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })\n }", + "replacement": "", + "anchor": "return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:field-drop:return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M76", + "number": 76, + "source": "src/powersync.ts", + "line": 480, + "position": 16087, + "operator": "delete-call", + "original": "begin({ snapshot })", + "replacement": ";", + "anchor": "begin ( { snapshot } )", + "function": "apply", + "key": "src/powersync.ts#apply:delete-call:begin ( { snapshot } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M77", + "number": 77, + "source": "src/powersync.ts", + "line": 480, + "position": 16095, + "operator": "field-drop", + "original": "snapshot", + "replacement": "", + "anchor": "begin ( { snapshot } )", + "function": "apply", + "key": "src/powersync.ts#apply:field-drop:begin ( { snapshot } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M78", + "number": 78, + "source": "src/powersync.ts", + "line": 481, + "position": 16141, + "operator": "value-undefined", + "original": "batches.splice(0).flat()", + "replacement": "undefined", + "anchor": "for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) }", + "function": "apply", + "key": "src/powersync.ts#apply:value-undefined:for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M92", + "number": 92, + "source": "src/powersync.ts", + "line": 493, + "position": 16652, + "operator": "loop-false", + "original": "currentBatchCount == syncBatchSize", + "replacement": "false", + "anchor": "while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) }", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:loop-false:while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M93", + "number": 93, + "source": "src/powersync.ts", + "line": 494, + "position": 16731, + "operator": "value-undefined", + "original": "batchQuery(\n context,\n syncBatchSize,\n cursor,\n )", + "replacement": "undefined", + "anchor": "const batchItems = await batchQuery ( context , syncBatchSize , cursor , )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:value-undefined:const batchItems = await batchQuery ( context , syncBatchSize , cursor , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M94", + "number": 94, + "source": "src/powersync.ts", + "line": 501, + "position": 16961, + "operator": "delete-call", + "original": "batches.push(batchItems)", + "replacement": ";", + "anchor": "batches . push ( batchItems )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:delete-call:batches . push ( batchItems )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M95", + "number": 95, + "source": "src/powersync.ts", + "line": 502, + "position": 17006, + "operator": "if-true", + "original": "!snapshot", + "replacement": "true", + "anchor": "if ( ! snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-true:if ( ! snapshot ) apply ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M96", + "number": 96, + "source": "src/powersync.ts", + "line": 502, + "position": 17006, + "operator": "if-false", + "original": "!snapshot", + "replacement": "false", + "anchor": "if ( ! snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-false:if ( ! snapshot ) apply ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M97", + "number": 97, + "source": "src/powersync.ts", + "line": 502, + "position": 17006, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:drop-not:if ( ! snapshot ) apply ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M98", + "number": 98, + "source": "src/powersync.ts", + "line": 502, + "position": 17017, + "operator": "delete-call", + "original": "apply()", + "replacement": ";", + "anchor": "apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:delete-call:apply ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M99", + "number": 99, + "source": "src/powersync.ts", + "line": 504, + "position": 17059, + "operator": "if-true", + "original": "snapshot", + "replacement": "true", + "anchor": "if ( snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-true:if ( snapshot ) apply ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M100", + "number": 100, + "source": "src/powersync.ts", + "line": 504, + "position": 17059, + "operator": "if-false", + "original": "snapshot", + "replacement": "false", + "anchor": "if ( snapshot ) apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:if-false:if ( snapshot ) apply ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M101", + "number": 101, + "source": "src/powersync.ts", + "line": 504, + "position": 17069, + "operator": "delete-call", + "original": "apply()", + "replacement": ";", + "anchor": "apply ( )", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:delete-call:apply ( )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M105", + "number": 105, + "source": "src/powersync.ts", + "line": 506, + "position": 17138, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.info,\n message: `Sync is ready for ${viewName} into ${trackedTableName}`,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:delete-call:database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M106", + "number": 106, + "source": "src/powersync.ts", + "line": 507, + "position": 17176, + "operator": "field-drop", + "original": "level: LogLevels.info", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M107", + "number": 107, + "source": "src/powersync.ts", + "line": 508, + "position": 17215, + "operator": "field-drop", + "original": "message: `Sync is ready for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "beforeCreate", + "key": "src/powersync.ts#beforeCreate:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M108", + "number": 108, + "source": "src/powersync.ts", + "line": 520, + "position": 17679, + "operator": "if-true", + "original": "setup", + "replacement": "true", + "anchor": "if ( setup ) { await setup . catch ( ( ) => undefined ) }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:if-true:if ( setup ) { await setup . catch ( ( ) => undefined ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M109", + "number": 109, + "source": "src/powersync.ts", + "line": 520, + "position": 17679, + "operator": "if-false", + "original": "setup", + "replacement": "false", + "anchor": "if ( setup ) { await setup . catch ( ( ) => undefined ) }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:if-false:if ( setup ) { await setup . catch ( ( ) => undefined ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M110", + "number": 110, + "source": "src/powersync.ts", + "line": 521, + "position": 17704, + "operator": "value-undefined", + "original": "setup.catch(() => undefined)", + "replacement": "undefined", + "anchor": "await setup . catch ( ( ) => undefined )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:value-undefined:await setup . catch ( ( ) => undefined )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M111", + "number": 111, + "source": "src/powersync.ts", + "line": 523, + "position": 17755, + "operator": "if-true", + "original": "!disposeTracking", + "replacement": "true", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:if-true:if ( ! disposeTracking ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M112", + "number": 112, + "source": "src/powersync.ts", + "line": 523, + "position": 17755, + "operator": "if-false", + "original": "!disposeTracking", + "replacement": "false", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:if-false:if ( ! disposeTracking ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M113", + "number": 113, + "source": "src/powersync.ts", + "line": 523, + "position": 17755, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:drop-not:if ( ! disposeTracking ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M114", + "number": 114, + "source": "src/powersync.ts", + "line": 528, + "position": 17879, + "operator": "value-undefined", + "original": "database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })\n })", + "replacement": "undefined", + "anchor": "await database . writeTransaction ( async ( context ) => { await flushDiffRecordsWithContext ( context , ignoredReceipts ) } ) . catch ( ( error ) => { database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } ) } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:value-undefined:await database . writeTransaction ( async ( context ) => { await flushDiffRecordsWithContext ( context , ignoredReceipts ) } ) . catch ( ( error ) => { database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } ) } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M115", + "number": 115, + "source": "src/powersync.ts", + "line": 530, + "position": 17955, + "operator": "value-undefined", + "original": "flushDiffRecordsWithContext(context, ignoredReceipts)", + "replacement": "undefined", + "anchor": "await flushDiffRecordsWithContext ( context , ignoredReceipts )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:value-undefined:await flushDiffRecordsWithContext ( context , ignoredReceipts )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M116", + "number": 116, + "source": "src/powersync.ts", + "line": 533, + "position": 18064, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:delete-call:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M117", + "number": 117, + "source": "src/powersync.ts", + "line": 534, + "position": 18100, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M118", + "number": 118, + "source": "src/powersync.ts", + "line": 535, + "position": 18138, + "operator": "field-drop", + "original": "message: `An error has been detected in the sync handler`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M119", + "number": 119, + "source": "src/powersync.ts", + "line": 536, + "position": 18211, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecords", + "key": "src/powersync.ts#flushDiffRecords:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M120", + "number": 120, + "source": "src/powersync.ts", + "line": 547, + "position": 18600, + "operator": "if-true", + "original": "!disposeTracking", + "replacement": "true", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:if-true:if ( ! disposeTracking ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M121", + "number": 121, + "source": "src/powersync.ts", + "line": 547, + "position": 18600, + "operator": "if-false", + "original": "!disposeTracking", + "replacement": "false", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:if-false:if ( ! disposeTracking ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M122", + "number": 122, + "source": "src/powersync.ts", + "line": 547, + "position": 18600, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! disposeTracking ) { return }", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:drop-not:if ( ! disposeTracking ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M123", + "number": 123, + "source": "src/powersync.ts", + "line": 552, + "position": 18672, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:begin ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M124", + "number": 124, + "source": "src/powersync.ts", + "line": 553, + "position": 18715, + "operator": "value-undefined", + "original": "context.getAll(\n `SELECT * FROM ${trackedTableName} ORDER BY operation_id ASC`,\n )", + "replacement": "undefined", + "anchor": "const operations = await context . getAll < TriggerDiffRecord > ( `SELECT * FROM ${ trackedTableName } ORDER BY operation_id ASC `,\n )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const operations = await context . getAll < TriggerDiffRecord > ( `SELECT * FROM ${ trackedTableName } ORDER BY operation_id ASC `,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M125", + "number": 125, + "source": "src/powersync.ts", + "line": 560, + "position": 19034, + "operator": "value-undefined", + "original": "deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })", + "replacement": "undefined", + "anchor": "const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M126", + "number": 126, + "source": "src/powersync.ts", + "line": 561, + "position": 19069, + "operator": "field-drop", + "original": "id", + "replacement": "", + "anchor": "const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M127", + "number": 127, + "source": "src/powersync.ts", + "line": 562, + "position": 19090, + "operator": "value-undefined", + "original": "JSON.parse(value)", + "replacement": "undefined", + "anchor": "const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const parsedValue = deserializeSyncRow ( { id , ... JSON . parse ( value ) , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M128", + "number": 128, + "source": "src/powersync.ts", + "line": 566, + "position": 19240, + "operator": "value-undefined", + "original": "deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })", + "replacement": "undefined", + "anchor": "const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M129", + "number": 129, + "source": "src/powersync.ts", + "line": 567, + "position": 19281, + "operator": "field-drop", + "original": "id", + "replacement": "", + "anchor": "const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M130", + "number": 130, + "source": "src/powersync.ts", + "line": 568, + "position": 19308, + "operator": "value-undefined", + "original": "JSON.parse(op.previous_value)", + "replacement": "undefined", + "anchor": "const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const parsedPreviousValue = op . operation == DiffTriggerOperation . UPDATE ? deserializeSyncRow ( { id , ... JSON . parse ( op . previous_value ) , } ) : undefined~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M131", + "number": 131, + "source": "src/powersync.ts", + "line": 571, + "position": 19400, + "operator": "delete-call", + "original": "write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })", + "replacement": ";", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M132", + "number": 132, + "source": "src/powersync.ts", + "line": 572, + "position": 19422, + "operator": "field-drop", + "original": "type: mapOperation(operation)", + "replacement": "", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M133", + "number": 133, + "source": "src/powersync.ts", + "line": 572, + "position": 19428, + "operator": "value-undefined", + "original": "mapOperation(operation)", + "replacement": "undefined", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M134", + "number": 134, + "source": "src/powersync.ts", + "line": 573, + "position": 19467, + "operator": "field-drop", + "original": "value: parsedValue", + "replacement": "", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M135", + "number": 135, + "source": "src/powersync.ts", + "line": 574, + "position": 19501, + "operator": "field-drop", + "original": "previousValue: parsedPreviousValue", + "replacement": "", + "anchor": "write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:write ( { type : mapOperation ( operation ) , value : parsedValue , previousValue : parsedPreviousValue , } )~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M136", + "number": 136, + "source": "src/powersync.ts", + "line": 576, + "position": 19564, + "operator": "delete-call", + "original": "pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })", + "replacement": ";", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M137", + "number": 137, + "source": "src/powersync.ts", + "line": 577, + "position": 19603, + "operator": "field-drop", + "original": "id", + "replacement": "", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M138", + "number": 138, + "source": "src/powersync.ts", + "line": 578, + "position": 19621, + "operator": "field-drop", + "original": "operation", + "replacement": "", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M139", + "number": 139, + "source": "src/powersync.ts", + "line": 579, + "position": 19646, + "operator": "field-drop", + "original": "timestamp", + "replacement": "", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M140", + "number": 140, + "source": "src/powersync.ts", + "line": 580, + "position": 19671, + "operator": "field-drop", + "original": "tableName: viewName", + "replacement": "", + "anchor": "pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:pendingOperations . push ( { id , operation , timestamp , tableName : viewName , } )~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M141", + "number": 141, + "source": "src/powersync.ts", + "line": 585, + "position": 19778, + "operator": "value-undefined", + "original": "context.execute(`DELETE FROM ${trackedTableName}`)", + "replacement": "undefined", + "anchor": "await context . execute ( `DELETE FROM ${ trackedTableName } `)", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:await context . execute ( `DELETE FROM ${ trackedTableName } `)" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M142", + "number": 142, + "source": "src/powersync.ts", + "line": 587, + "position": 19856, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const applied = commit ( )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:value-undefined:const applied = commit ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M143", + "number": 143, + "source": "src/powersync.ts", + "line": 588, + "position": 19875, + "operator": "delete-call", + "original": "appliedReceipts.push(applied)", + "replacement": ";", + "anchor": "appliedReceipts . push ( applied )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:appliedReceipts . push ( applied )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M144", + "number": 144, + "source": "src/powersync.ts", + "line": 593, + "position": 20195, + "operator": "delete-call", + "original": "pendingOperationStore.resolvePendingFor(pendingOperations)", + "replacement": ";", + "anchor": "pendingOperationStore . resolvePendingFor ( pendingOperations )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:pendingOperationStore . resolvePendingFor ( pendingOperations )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M145", + "number": 145, + "source": "src/powersync.ts", + "line": 595, + "position": 20290, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `An error has been detected in the sync handler`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:delete-call:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M146", + "number": 146, + "source": "src/powersync.ts", + "line": 596, + "position": 20324, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M147", + "number": 147, + "source": "src/powersync.ts", + "line": 597, + "position": 20360, + "operator": "field-drop", + "original": "message: `An error has been detected in the sync handler`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M148", + "number": 148, + "source": "src/powersync.ts", + "line": 598, + "position": 20431, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )", + "function": "flushDiffRecordsWithContext", + "key": "src/powersync.ts#flushDiffRecordsWithContext:field-drop:database . logger . log ( { level : LogLevels . error , message : `An error has been detected in the sync handler` , error , } )~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M149", + "number": 149, + "source": "src/powersync.ts", + "line": 605, + "position": 20606, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.info,\n message: `Sync is starting for ${viewName} into ${trackedTableName}`,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "start", + "key": "src/powersync.ts#start:delete-call:database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M150", + "number": 150, + "source": "src/powersync.ts", + "line": 606, + "position": 20638, + "operator": "field-drop", + "original": "level: LogLevels.info", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M151", + "number": 151, + "source": "src/powersync.ts", + "line": 607, + "position": 20671, + "operator": "field-drop", + "original": "message: `Sync is starting for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync is starting for ${ viewName } into $ { trackedTableName } `,\n })~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M152", + "number": 152, + "source": "src/powersync.ts", + "line": 609, + "position": 20760, + "operator": "delete-call", + "original": "database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )", + "replacement": ";", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:delete-call:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M153", + "number": 153, + "source": "src/powersync.ts", + "line": 611, + "position": 20815, + "operator": "field-drop", + "original": "onChange: async () => {\n await flushDiffRecords()\n }", + "replacement": "", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "onChange", + "key": "src/powersync.ts#onChange:field-drop:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M154", + "number": 154, + "source": "src/powersync.ts", + "line": 612, + "position": 20859, + "operator": "value-undefined", + "original": "flushDiffRecords()", + "replacement": "undefined", + "anchor": "await flushDiffRecords ( )", + "function": "onChange", + "key": "src/powersync.ts#onChange:value-undefined:await flushDiffRecords ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M155", + "number": 155, + "source": "src/powersync.ts", + "line": 616, + "position": 20930, + "operator": "field-drop", + "original": "signal: abortController.signal", + "replacement": "", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M156", + "number": 156, + "source": "src/powersync.ts", + "line": 617, + "position": 20974, + "operator": "field-drop", + "original": "triggerImmediate: false", + "replacement": "", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M157", + "number": 157, + "source": "src/powersync.ts", + "line": 617, + "position": 20992, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:bool-flip:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M158", + "number": 158, + "source": "src/powersync.ts", + "line": 618, + "position": 21011, + "operator": "field-drop", + "original": "tables: [trackedTableName]", + "replacement": "", + "anchor": "database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )", + "function": "start", + "key": "src/powersync.ts#start:field-drop:database . onChangeWithCallback ( { onChange : async ( ) => { await flushDiffRecords ( ) } , } , { signal : abortController . signal , triggerImmediate : false , tables : [ trackedTableName ] , } , )~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M159", + "number": 159, + "source": "src/powersync.ts", + "line": 622, + "position": 21077, + "operator": "value-undefined", + "original": "afterOnChangeRegistered?.()", + "replacement": "undefined", + "anchor": "await afterOnChangeRegistered ?. ( )", + "function": "start", + "key": "src/powersync.ts#start:value-undefined:await afterOnChangeRegistered ?. ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M160", + "number": 160, + "source": "src/powersync.ts", + "line": 625, + "position": 21200, + "operator": "if-true", + "original": "abortController.signal.aborted", + "replacement": "true", + "anchor": "if ( abortController . signal . aborted ) { await disposeTrackingAfterAbort ( ) } else { abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , ) }", + "function": "start", + "key": "src/powersync.ts#start:if-true:if ( abortController . signal . aborted ) { await disposeTrackingAfterAbort ( ) } else { abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M161", + "number": 161, + "source": "src/powersync.ts", + "line": 625, + "position": 21200, + "operator": "if-false", + "original": "abortController.signal.aborted", + "replacement": "false", + "anchor": "if ( abortController . signal . aborted ) { await disposeTrackingAfterAbort ( ) } else { abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , ) }", + "function": "start", + "key": "src/powersync.ts#start:if-false:if ( abortController . signal . aborted ) { await disposeTrackingAfterAbort ( ) } else { abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M162", + "number": 162, + "source": "src/powersync.ts", + "line": 626, + "position": 21250, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "await disposeTrackingAfterAbort ( )", + "function": "start", + "key": "src/powersync.ts#start:value-undefined:await disposeTrackingAfterAbort ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M163", + "number": 163, + "source": "src/powersync.ts", + "line": 628, + "position": 21305, + "operator": "delete-call", + "original": "abortController.signal.addEventListener(\n `abort`,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )", + "replacement": ";", + "anchor": "abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )", + "function": "start", + "key": "src/powersync.ts#start:delete-call:abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M164", + "number": 164, + "source": "src/powersync.ts", + "line": 631, + "position": 21406, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "void disposeTrackingAfterAbort ( )", + "function": "start", + "key": "src/powersync.ts#start:value-undefined:void disposeTrackingAfterAbort ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M165", + "number": 165, + "source": "src/powersync.ts", + "line": 633, + "position": 21463, + "operator": "field-drop", + "original": "once: true", + "replacement": "", + "anchor": "abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )", + "function": "start", + "key": "src/powersync.ts#start:field-drop:abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M166", + "number": 166, + "source": "src/powersync.ts", + "line": 633, + "position": 21469, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )", + "function": "start", + "key": "src/powersync.ts#start:bool-flip:abortController . signal . addEventListener ( `abort` , ( ) => { void disposeTrackingAfterAbort ( ) } , { once : true } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M167", + "number": 167, + "source": "src/powersync.ts", + "line": 642, + "position": 21700, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let onUnloadStarted = false", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:bool-flip:let onUnloadStarted = false" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M168", + "number": 168, + "source": "src/powersync.ts", + "line": 644, + "position": 21731, + "operator": "value-undefined", + "original": "start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: `delete`, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })", + "replacement": "undefined", + "anchor": "const startup = start ( async ( ) => { const cleanup = await restConfig . onLoad ?. ( ) onUnload = cleanup if ( abortController . signal . aborted ) { return } const appliedReceipts : Array < SyncAppliedReceipt > = [ ] const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const startup = start ( async ( ) => { const cleanup = await restConfig . onLoad ?. ( ) onUnload = cleanup if ( abortController . signal . aborted ) { return } const appliedReceipts : Array < SyncAppliedReceipt > = [ ] const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M169", + "number": 169, + "source": "src/powersync.ts", + "line": 645, + "position": 21783, + "operator": "value-undefined", + "original": "restConfig.onLoad?.()", + "replacement": "undefined", + "anchor": "const cleanup = await restConfig . onLoad ?. ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const cleanup = await restConfig . onLoad ?. ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M170", + "number": 170, + "source": "src/powersync.ts", + "line": 647, + "position": 21848, + "operator": "if-true", + "original": "abortController.signal.aborted", + "replacement": "true", + "anchor": "if ( abortController . signal . aborted ) { return }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( abortController . signal . aborted ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M171", + "number": 171, + "source": "src/powersync.ts", + "line": 647, + "position": 21848, + "operator": "if-false", + "original": "abortController.signal.aborted", + "replacement": "false", + "anchor": "if ( abortController . signal . aborted ) { return }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( abortController . signal . aborted ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M174", + "number": 174, + "source": "src/powersync.ts", + "line": 654, + "position": 22100, + "operator": "value-undefined", + "original": "establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: `delete`, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "replacement": "undefined", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M204", + "number": 204, + "source": "src/powersync.ts", + "line": 695, + "position": 23982, + "operator": "field-drop", + "original": "when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n }", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M205", + "number": 205, + "source": "src/powersync.ts", + "line": 696, + "position": 24006, + "operator": "field-drop", + "original": "[DiffTriggerOperation.INSERT]: `TRUE`", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M206", + "number": 206, + "source": "src/powersync.ts", + "line": 697, + "position": 24061, + "operator": "field-drop", + "original": "[DiffTriggerOperation.UPDATE]: `TRUE`", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~5" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M207", + "number": 207, + "source": "src/powersync.ts", + "line": 698, + "position": 24116, + "operator": "field-drop", + "original": "[DiffTriggerOperation.DELETE]: `TRUE`", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~6" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M210", + "number": 210, + "source": "src/powersync.ts", + "line": 705, + "position": 24361, + "operator": "field-drop", + "original": "writeType: (_rowId: string) => `insert`", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "writeType", + "key": "src/powersync.ts#writeType:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M211", + "number": 211, + "source": "src/powersync.ts", + "line": 706, + "position": 24416, + "operator": "field-drop", + "original": "batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )", + "replacement": "", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:field-drop:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M212", + "number": 212, + "source": "src/powersync.ts", + "line": 711, + "position": 24574, + "operator": "value-undefined", + "original": "lockContext.getAll(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )", + "replacement": "undefined", + "anchor": "await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:value-undefined:await establishTracking ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , afterRead : listed && persistence ? async ( ) => { await persistence . hydrateBaseline ( ) if ( abortController . signal . aborted ) return const gone : Array < string > = [ ] for ( const [ key , ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } listed . clear ( ) for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize , ) ) write ( { type : `delete` , key } ) const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } } : undefined , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , onInitialRow : listed ? ( value ) => { listed . add ( asPowerSyncRecord ( value ) . id ) } : undefined , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M213", + "number": 213, + "source": "src/powersync.ts", + "line": 720, + "position": 24960, + "operator": "value-undefined", + "original": "Promise.all(appliedReceipts.map(whenSyncAccepted))", + "replacement": "undefined", + "anchor": "await Promise . all ( appliedReceipts . map ( whenSyncAccepted ) )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await Promise . all ( appliedReceipts . map ( whenSyncAccepted ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M214", + "number": 214, + "source": "src/powersync.ts", + "line": 720, + "position": 24972, + "operator": "value-undefined", + "original": "appliedReceipts.map(whenSyncAccepted)", + "replacement": "undefined", + "anchor": "await Promise . all ( appliedReceipts . map ( whenSyncAccepted ) )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await Promise . all ( appliedReceipts . map ( whenSyncAccepted ) )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M215", + "number": 215, + "source": "src/powersync.ts", + "line": 721, + "position": 25021, + "operator": "delete-call", + "original": "markReady()", + "replacement": ";", + "anchor": "markReady ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:markReady ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M216", + "number": 216, + "source": "src/powersync.ts", + "line": 723, + "position": 25073, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M217", + "number": 217, + "source": "src/powersync.ts", + "line": 724, + "position": 25107, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M218", + "number": 218, + "source": "src/powersync.ts", + "line": 725, + "position": 25143, + "operator": "field-drop", + "original": "message: `Could not start syncing process for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M219", + "number": 219, + "source": "src/powersync.ts", + "line": 726, + "position": 25240, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n })~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M220", + "number": 220, + "source": "src/powersync.ts", + "line": 728, + "position": 25274, + "operator": "if-true", + "original": "!ready", + "replacement": "true", + "anchor": "if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M221", + "number": 221, + "source": "src/powersync.ts", + "line": 728, + "position": 25274, + "operator": "if-false", + "original": "!ready", + "replacement": "false", + "anchor": "if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M222", + "number": 222, + "source": "src/powersync.ts", + "line": 728, + "position": 25274, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:drop-not:if ( ! ready ) { fail ( error ) abortController . abort ( ) void disposeTrackingAfterAbort ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M223", + "number": 223, + "source": "src/powersync.ts", + "line": 729, + "position": 25296, + "operator": "delete-call", + "original": "fail(error)", + "replacement": ";", + "anchor": "fail ( error )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:fail ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M224", + "number": 224, + "source": "src/powersync.ts", + "line": 732, + "position": 25477, + "operator": "delete-call", + "original": "abortController.abort()", + "replacement": ";", + "anchor": "abortController . abort ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:abortController . abort ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M225", + "number": 225, + "source": "src/powersync.ts", + "line": 733, + "position": 25518, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "void disposeTrackingAfterAbort ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:void disposeTrackingAfterAbort ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M226", + "number": 226, + "source": "src/powersync.ts", + "line": 738, + "position": 25645, + "operator": "if-true", + "original": "onUnloadStarted || !onUnload", + "replacement": "true", + "anchor": "if ( onUnloadStarted || ! onUnload ) return", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:if-true:if ( onUnloadStarted || ! onUnload ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M227", + "number": 227, + "source": "src/powersync.ts", + "line": 738, + "position": 25645, + "operator": "if-false", + "original": "onUnloadStarted || !onUnload", + "replacement": "false", + "anchor": "if ( onUnloadStarted || ! onUnload ) return", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:if-false:if ( onUnloadStarted || ! onUnload ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M228", + "number": 228, + "source": "src/powersync.ts", + "line": 738, + "position": 25661, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( onUnloadStarted || ! onUnload ) return", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:binary-swap:if ( onUnloadStarted || ! onUnload ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M229", + "number": 229, + "source": "src/powersync.ts", + "line": 738, + "position": 25664, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( onUnloadStarted || ! onUnload ) return", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:drop-not:if ( onUnloadStarted || ! onUnload ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M230", + "number": 230, + "source": "src/powersync.ts", + "line": 739, + "position": 25710, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "onUnloadStarted = true", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:bool-flip:onUnloadStarted = true" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M231", + "number": 231, + "source": "src/powersync.ts", + "line": 742, + "position": 25793, + "operator": "value-undefined", + "original": "cleanup()", + "replacement": "undefined", + "anchor": "return cleanup ( )", + "function": "invokeOnUnload", + "key": "src/powersync.ts#invokeOnUnload:value-undefined:return cleanup ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M232", + "number": 232, + "source": "src/powersync.ts", + "line": 746, + "position": 25853, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M233", + "number": 233, + "source": "src/powersync.ts", + "line": 747, + "position": 25887, + "operator": "field-drop", + "original": "level: LogLevels.info", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M234", + "number": 234, + "source": "src/powersync.ts", + "line": 748, + "position": 25922, + "operator": "field-drop", + "original": "message: `Sync has been stopped for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M235", + "number": 235, + "source": "src/powersync.ts", + "line": 750, + "position": 26020, + "operator": "delete-call", + "original": "abortController.abort()", + "replacement": ";", + "anchor": "abortController . abort ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:delete-call:abortController . abort ( )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M236", + "number": 236, + "source": "src/powersync.ts", + "line": 751, + "position": 26079, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "const trackingDisposal = disposeTrackingAfterAbort ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const trackingDisposal = disposeTrackingAfterAbort ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M237", + "number": 237, + "source": "src/powersync.ts", + "line": 756, + "position": 26255, + "operator": "value-undefined", + "original": "invokeOnUnload()", + "replacement": "undefined", + "anchor": "await invokeOnUnload ( )", + "function": "settleOnUnload", + "key": "src/powersync.ts#settleOnUnload:value-undefined:await invokeOnUnload ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M238", + "number": 238, + "source": "src/powersync.ts", + "line": 758, + "position": 26338, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "onUnloadFailure ??= { error }", + "function": "settleOnUnload", + "key": "src/powersync.ts#settleOnUnload:field-drop:onUnloadFailure ??= { error }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M239", + "number": 239, + "source": "src/powersync.ts", + "line": 764, + "position": 26569, + "operator": "value-undefined", + "original": "Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])", + "replacement": "undefined", + "anchor": "const settlements = await Promise . allSettled ( [ startup , trackingDisposal , settleOnUnload ( ) , ] )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const settlements = await Promise . allSettled ( [ startup , trackingDisposal , settleOnUnload ( ) , ] )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M240", + "number": 240, + "source": "src/powersync.ts", + "line": 767, + "position": 26653, + "operator": "value-undefined", + "original": "settleOnUnload()", + "replacement": "undefined", + "anchor": "const settlements = await Promise . allSettled ( [ startup , trackingDisposal , settleOnUnload ( ) , ] )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:const settlements = await Promise . allSettled ( [ startup , trackingDisposal , settleOnUnload ( ) , ] )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M241", + "number": 241, + "source": "src/powersync.ts", + "line": 770, + "position": 26752, + "operator": "value-undefined", + "original": "settleOnUnload()", + "replacement": "undefined", + "anchor": "await settleOnUnload ( )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:value-undefined:await settleOnUnload ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M242", + "number": 242, + "source": "src/powersync.ts", + "line": 771, + "position": 26783, + "operator": "if-true", + "original": "disposalSettlement.status === `rejected` && onUnloadFailure", + "replacement": "true", + "anchor": "if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M243", + "number": 243, + "source": "src/powersync.ts", + "line": 771, + "position": 26783, + "operator": "if-false", + "original": "disposalSettlement.status === `rejected` && onUnloadFailure", + "replacement": "false", + "anchor": "if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M244", + "number": 244, + "source": "src/powersync.ts", + "line": 771, + "position": 26824, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M245", + "number": 245, + "source": "src/powersync.ts", + "line": 771, + "position": 26809, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:if ( disposalSettlement . status === `rejected` && onUnloadFailure ) { throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , ) }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M246", + "number": 246, + "source": "src/powersync.ts", + "line": 775, + "position": 27045, + "operator": "field-drop", + "original": "cause: disposalSettlement.reason", + "replacement": "", + "anchor": "throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , )", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:field-drop:throw new AggregateError ( [ disposalSettlement . reason , onUnloadFailure . error ] , `PowerSync tracking disposal and load-hook cleanup both failed` , { cause : disposalSettlement . reason } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M247", + "number": 247, + "source": "src/powersync.ts", + "line": 778, + "position": 27121, + "operator": "if-true", + "original": "disposalSettlement.status === `rejected`", + "replacement": "true", + "anchor": "if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M248", + "number": 248, + "source": "src/powersync.ts", + "line": 778, + "position": 27121, + "operator": "if-false", + "original": "disposalSettlement.status === `rejected`", + "replacement": "false", + "anchor": "if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M249", + "number": 249, + "source": "src/powersync.ts", + "line": 778, + "position": 27147, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:binary-swap:if ( disposalSettlement . status === `rejected` ) { throw disposalSettlement . reason }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M250", + "number": 250, + "source": "src/powersync.ts", + "line": 781, + "position": 27235, + "operator": "if-true", + "original": "onUnloadFailure", + "replacement": "true", + "anchor": "if ( onUnloadFailure ) throw onUnloadFailure . error", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-true:if ( onUnloadFailure ) throw onUnloadFailure . error" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M251", + "number": 251, + "source": "src/powersync.ts", + "line": 781, + "position": 27235, + "operator": "if-false", + "original": "onUnloadFailure", + "replacement": "false", + "anchor": "if ( onUnloadFailure ) throw onUnloadFailure . error", + "function": "runEagerSync", + "key": "src/powersync.ts#runEagerSync:if-false:if ( onUnloadFailure ) throw onUnloadFailure . error" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M252", + "number": 252, + "source": "src/powersync.ts", + "line": 817, + "position": 28419, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let stopped = false", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:bool-flip:let stopped = false" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M253", + "number": 253, + "source": "src/powersync.ts", + "line": 821, + "position": 28588, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let drainingReleases = false", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:bool-flip:let drainingReleases = false" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M254", + "number": 254, + "source": "src/powersync.ts", + "line": 824, + "position": 28764, + "operator": "value-undefined", + "original": "start()", + "replacement": "undefined", + "anchor": "const startup = start ( )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:value-undefined:const startup = start ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M255", + "number": 255, + "source": "src/powersync.ts", + "line": 825, + "position": 28785, + "operator": "value-undefined", + "original": "startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n }),\n )", + "replacement": "undefined", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:value-undefined:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M256", + "number": 256, + "source": "src/powersync.ts", + "line": 826, + "position": 28820, + "operator": "value-undefined", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n })", + "replacement": "undefined", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:value-undefined:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M257", + "number": 257, + "source": "src/powersync.ts", + "line": 827, + "position": 28854, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M258", + "number": 258, + "source": "src/powersync.ts", + "line": 828, + "position": 28890, + "operator": "field-drop", + "original": "message: `Could not start syncing process for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M259", + "number": 259, + "source": "src/powersync.ts", + "line": 829, + "position": 28987, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:void startup . catch ( ( error ) => database . logger . log ( { level : LogLevels . error , message : `Could not start syncing process for ${ viewName } into $ { trackedTableName } `,\n error,\n }),\n )~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M260", + "number": 260, + "source": "src/powersync.ts", + "line": 834, + "position": 29074, + "operator": "value-undefined", + "original": "Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)", + "replacement": "undefined", + "anchor": "const activeWhereExpressions = ( ) => Array . from ( demands . values ( ) ) . filter ( ( demand ) => demand . active ) . map ( ( demand ) => demand . options . where )", + "function": "activeWhereExpressions", + "key": "src/powersync.ts#activeWhereExpressions:value-undefined:const activeWhereExpressions = ( ) => Array . from ( demands . values ( ) ) . filter ( ( demand ) => demand . active ) . map ( ( demand ) => demand . options . where )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M261", + "number": 261, + "source": "src/powersync.ts", + "line": 834, + "position": 29085, + "operator": "value-undefined", + "original": "demands.values()", + "replacement": "undefined", + "anchor": "const activeWhereExpressions = ( ) => Array . from ( demands . values ( ) ) . filter ( ( demand ) => demand . active ) . map ( ( demand ) => demand . options . where )", + "function": "activeWhereExpressions", + "key": "src/powersync.ts#activeWhereExpressions:value-undefined:const activeWhereExpressions = ( ) => Array . from ( demands . values ( ) ) . filter ( ( demand ) => demand . active ) . map ( ( demand ) => demand . options . where )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M262", + "number": 262, + "source": "src/powersync.ts", + "line": 841, + "position": 29437, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let trackingOutage = false", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:bool-flip:let trackingOutage = false" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M263", + "number": 263, + "source": "src/powersync.ts", + "line": 843, + "position": 29523, + "operator": "loop-false", + "original": "!stopped && reconciledTrackingRevision !== trackingRevision", + "replacement": "false", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:loop-false:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M264", + "number": 264, + "source": "src/powersync.ts", + "line": 843, + "position": 29532, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:binary-swap:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M265", + "number": 265, + "source": "src/powersync.ts", + "line": 843, + "position": 29523, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M266", + "number": 266, + "source": "src/powersync.ts", + "line": 843, + "position": 29562, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:binary-swap:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { const revision = trackingRevision const isCurrent = ( ) => ! stopped && trackingRevision === revision const appliedReceipts : Array < SyncAppliedReceipt > = [ ] await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M267", + "number": 267, + "source": "src/powersync.ts", + "line": 845, + "position": 29677, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const isCurrent = ( ) => ! stopped && trackingRevision === revision", + "function": "isCurrent", + "key": "src/powersync.ts#isCurrent:binary-swap:const isCurrent = ( ) => ! stopped && trackingRevision === revision" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M268", + "number": 268, + "source": "src/powersync.ts", + "line": 845, + "position": 29668, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const isCurrent = ( ) => ! stopped && trackingRevision === revision", + "function": "isCurrent", + "key": "src/powersync.ts#isCurrent:drop-not:const isCurrent = ( ) => ! stopped && trackingRevision === revision" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M269", + "number": 269, + "source": "src/powersync.ts", + "line": 845, + "position": 29697, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const isCurrent = ( ) => ! stopped && trackingRevision === revision", + "function": "isCurrent", + "key": "src/powersync.ts#isCurrent:binary-swap:const isCurrent = ( ) => ! stopped && trackingRevision === revision~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M270", + "number": 270, + "source": "src/powersync.ts", + "line": 848, + "position": 29795, + "operator": "value-undefined", + "original": "database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: `delete`, key })\n appliedReceipts.push(commit())\n }\n })", + "replacement": "undefined", + "anchor": "await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await database . writeLock ( async ( ctx ) => { if ( ! isCurrent ( ) ) return await flushDiffRecordsWithContext ( ctx , appliedReceipts ) if ( ! isCurrent ( ) ) return await safelyDisposeTracking ( ctx ) if ( ! isCurrent ( ) ) return const active = activeWhereExpressions ( ) const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M271", + "number": 271, + "source": "src/powersync.ts", + "line": 849, + "position": 29849, + "operator": "if-true", + "original": "!isCurrent()", + "replacement": "true", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( ! isCurrent ( ) ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M272", + "number": 272, + "source": "src/powersync.ts", + "line": 849, + "position": 29849, + "operator": "if-false", + "original": "!isCurrent()", + "replacement": "false", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( ! isCurrent ( ) ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M273", + "number": 273, + "source": "src/powersync.ts", + "line": 849, + "position": 29849, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:if ( ! isCurrent ( ) ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M274", + "number": 274, + "source": "src/powersync.ts", + "line": 849, + "position": 29850, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( ! isCurrent ( ) ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M275", + "number": 275, + "source": "src/powersync.ts", + "line": 850, + "position": 29890, + "operator": "value-undefined", + "original": "flushDiffRecordsWithContext(ctx, appliedReceipts)", + "replacement": "undefined", + "anchor": "await flushDiffRecordsWithContext ( ctx , appliedReceipts )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await flushDiffRecordsWithContext ( ctx , appliedReceipts )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M276", + "number": 276, + "source": "src/powersync.ts", + "line": 851, + "position": 29958, + "operator": "if-true", + "original": "!isCurrent()", + "replacement": "true", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( ! isCurrent ( ) ) return~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M277", + "number": 277, + "source": "src/powersync.ts", + "line": 851, + "position": 29958, + "operator": "if-false", + "original": "!isCurrent()", + "replacement": "false", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( ! isCurrent ( ) ) return~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M278", + "number": 278, + "source": "src/powersync.ts", + "line": 851, + "position": 29958, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:if ( ! isCurrent ( ) ) return~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M279", + "number": 279, + "source": "src/powersync.ts", + "line": 851, + "position": 29959, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( ! isCurrent ( ) ) return~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M280", + "number": 280, + "source": "src/powersync.ts", + "line": 852, + "position": 29999, + "operator": "value-undefined", + "original": "safelyDisposeTracking(ctx)", + "replacement": "undefined", + "anchor": "await safelyDisposeTracking ( ctx )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await safelyDisposeTracking ( ctx )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M281", + "number": 281, + "source": "src/powersync.ts", + "line": 853, + "position": 30044, + "operator": "if-true", + "original": "!isCurrent()", + "replacement": "true", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( ! isCurrent ( ) ) return~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M282", + "number": 282, + "source": "src/powersync.ts", + "line": 853, + "position": 30044, + "operator": "if-false", + "original": "!isCurrent()", + "replacement": "false", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( ! isCurrent ( ) ) return~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M283", + "number": 283, + "source": "src/powersync.ts", + "line": 853, + "position": 30044, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:if ( ! isCurrent ( ) ) return~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M284", + "number": 284, + "source": "src/powersync.ts", + "line": 853, + "position": 30045, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( ! isCurrent ( ) ) return", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( ! isCurrent ( ) ) return~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M285", + "number": 285, + "source": "src/powersync.ts", + "line": 855, + "position": 30095, + "operator": "value-undefined", + "original": "activeWhereExpressions()", + "replacement": "undefined", + "anchor": "const active = activeWhereExpressions ( )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const active = activeWhereExpressions ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M286", + "number": 286, + "source": "src/powersync.ts", + "line": 859, + "position": 30348, + "operator": "value-undefined", + "original": "collection.keys()", + "replacement": "undefined", + "anchor": "const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const missing = trackingOutage ? new Set ( collection . keys ( ) ) : undefined" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M287", + "number": 287, + "source": "src/powersync.ts", + "line": 861, + "position": 30413, + "operator": "if-true", + "original": "active.length > 0", + "replacement": "true", + "anchor": "if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M288", + "number": 288, + "source": "src/powersync.ts", + "line": 861, + "position": 30413, + "operator": "if-false", + "original": "active.length > 0", + "replacement": "false", + "anchor": "if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M289", + "number": 289, + "source": "src/powersync.ts", + "line": 861, + "position": 30427, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:binary-swap:if ( active . length > 0 ) { const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) ) const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , ) const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , ) const compiledView = compileSQLite ( { where : combinedWhere } ) const newDataWhenClause = toInlinedWhereClause ( compiledNewData ) const oldDataWhenClause = toInlinedWhereClause ( compiledOldData ) const viewWhereClause = toInlinedWhereClause ( compiledView ) await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M290", + "number": 290, + "source": "src/powersync.ts", + "line": 863, + "position": 30504, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:binary-swap:const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M291", + "number": 291, + "source": "src/powersync.ts", + "line": 865, + "position": 30564, + "operator": "value-undefined", + "original": "or(active[0], active[1], ...active.slice(2))", + "replacement": "undefined", + "anchor": "const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M292", + "number": 292, + "source": "src/powersync.ts", + "line": 865, + "position": 30592, + "operator": "value-undefined", + "original": "active.slice(2)", + "replacement": "undefined", + "anchor": "const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const combinedWhere = active . length === 1 ? active [ 0 ] : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M293", + "number": 293, + "source": "src/powersync.ts", + "line": 866, + "position": 30649, + "operator": "value-undefined", + "original": "compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )", + "replacement": "undefined", + "anchor": "const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M294", + "number": 294, + "source": "src/powersync.ts", + "line": 867, + "position": 30684, + "operator": "field-drop", + "original": "where: combinedWhere", + "replacement": "", + "anchor": "const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M295", + "number": 295, + "source": "src/powersync.ts", + "line": 868, + "position": 30728, + "operator": "field-drop", + "original": "jsonColumn: 'NEW.data'", + "replacement": "", + "anchor": "const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledNewData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'NEW.data' } , )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M296", + "number": 296, + "source": "src/powersync.ts", + "line": 870, + "position": 30812, + "operator": "value-undefined", + "original": "compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )", + "replacement": "undefined", + "anchor": "const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M297", + "number": 297, + "source": "src/powersync.ts", + "line": 871, + "position": 30847, + "operator": "field-drop", + "original": "where: combinedWhere", + "replacement": "", + "anchor": "const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M298", + "number": 298, + "source": "src/powersync.ts", + "line": 872, + "position": 30891, + "operator": "field-drop", + "original": "jsonColumn: 'OLD.data'", + "replacement": "", + "anchor": "const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledOldData = compileSQLite ( { where : combinedWhere } , { jsonColumn : 'OLD.data' } , )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M299", + "number": 299, + "source": "src/powersync.ts", + "line": 874, + "position": 30972, + "operator": "value-undefined", + "original": "compileSQLite({ where: combinedWhere })", + "replacement": "undefined", + "anchor": "const compiledView = compileSQLite ( { where : combinedWhere } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const compiledView = compileSQLite ( { where : combinedWhere } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M300", + "number": 300, + "source": "src/powersync.ts", + "line": 874, + "position": 30988, + "operator": "field-drop", + "original": "where: combinedWhere", + "replacement": "", + "anchor": "const compiledView = compileSQLite ( { where : combinedWhere } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:const compiledView = compileSQLite ( { where : combinedWhere } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M301", + "number": 301, + "source": "src/powersync.ts", + "line": 875, + "position": 31054, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledNewData)", + "replacement": "undefined", + "anchor": "const newDataWhenClause = toInlinedWhereClause ( compiledNewData )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const newDataWhenClause = toInlinedWhereClause ( compiledNewData )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M302", + "number": 302, + "source": "src/powersync.ts", + "line": 876, + "position": 31134, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledOldData)", + "replacement": "undefined", + "anchor": "const oldDataWhenClause = toInlinedWhereClause ( compiledOldData )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const oldDataWhenClause = toInlinedWhereClause ( compiledOldData )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M303", + "number": 303, + "source": "src/powersync.ts", + "line": 877, + "position": 31212, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledView)", + "replacement": "undefined", + "anchor": "const viewWhereClause = toInlinedWhereClause ( compiledView )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:const viewWhereClause = toInlinedWhereClause ( compiledView )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M304", + "number": 304, + "source": "src/powersync.ts", + "line": 878, + "position": 31269, + "operator": "value-undefined", + "original": "establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "replacement": "undefined", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M305", + "number": 305, + "source": "src/powersync.ts", + "line": 880, + "position": 31328, + "operator": "field-drop", + "original": "setupContext: ctx", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M306", + "number": 306, + "source": "src/powersync.ts", + "line": 881, + "position": 31367, + "operator": "field-drop", + "original": "when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n }", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M307", + "number": 307, + "source": "src/powersync.ts", + "line": 882, + "position": 31397, + "operator": "field-drop", + "original": "[DiffTriggerOperation.INSERT]: newDataWhenClause", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M308", + "number": 308, + "source": "src/powersync.ts", + "line": 883, + "position": 31469, + "operator": "field-drop", + "original": "[DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M309", + "number": 309, + "source": "src/powersync.ts", + "line": 884, + "position": 31574, + "operator": "field-drop", + "original": "[DiffTriggerOperation.DELETE]: oldDataWhenClause", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )~5" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M310", + "number": 310, + "source": "src/powersync.ts", + "line": 886, + "position": 31667, + "operator": "field-drop", + "original": "writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "writeType", + "key": "src/powersync.ts#writeType:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M311", + "number": 311, + "source": "src/powersync.ts", + "line": 887, + "position": 31719, + "operator": "value-undefined", + "original": "collection.has(rowId)", + "replacement": "undefined", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "writeType", + "key": "src/powersync.ts#writeType:value-undefined:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M312", + "number": 312, + "source": "src/powersync.ts", + "line": 888, + "position": 31784, + "operator": "field-drop", + "original": "batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n })", + "replacement": "", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:field-drop:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M313", + "number": 313, + "source": "src/powersync.ts", + "line": 893, + "position": 31972, + "operator": "value-undefined", + "original": "lockContext\n .getAll(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n })", + "replacement": "undefined", + "anchor": "await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:value-undefined:await establishTracking ( { setupContext : ctx , when : { [ DiffTriggerOperation . INSERT ] : newDataWhenClause , [ DiffTriggerOperation . UPDATE ] : `(${ newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M314", + "number": 314, + "source": "src/powersync.ts", + "line": 899, + "position": 32290, + "operator": "delete-call", + "original": "missing?.delete(row.id)", + "replacement": ";", + "anchor": "missing ?. delete ( row . id )", + "function": "batchQuery", + "key": "src/powersync.ts#batchQuery:delete-call:missing ?. delete ( row . id )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M315", + "number": 315, + "source": "src/powersync.ts", + "line": 906, + "position": 32488, + "operator": "if-true", + "original": "!isCurrent()", + "replacement": "true", + "anchor": "if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M316", + "number": 316, + "source": "src/powersync.ts", + "line": 906, + "position": 32488, + "operator": "if-false", + "original": "!isCurrent()", + "replacement": "false", + "anchor": "if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M317", + "number": 317, + "source": "src/powersync.ts", + "line": 906, + "position": 32488, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:drop-not:if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M318", + "number": 318, + "source": "src/powersync.ts", + "line": 906, + "position": 32489, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( ! isCurrent ( ) ) await safelyDisposeTracking ( ctx ) else if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M319", + "number": 319, + "source": "src/powersync.ts", + "line": 906, + "position": 32508, + "operator": "value-undefined", + "original": "safelyDisposeTracking(ctx)", + "replacement": "undefined", + "anchor": "await safelyDisposeTracking ( ctx )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await safelyDisposeTracking ( ctx )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M320", + "number": 320, + "source": "src/powersync.ts", + "line": 907, + "position": 32558, + "operator": "if-true", + "original": "missing?.size", + "replacement": "true", + "anchor": "if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M321", + "number": 321, + "source": "src/powersync.ts", + "line": 907, + "position": 32558, + "operator": "if-false", + "original": "missing?.size", + "replacement": "false", + "anchor": "if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( missing ?. size ) { begin ( ) for ( const key of missing ) write ( { type : `delete` , key } ) appliedReceipts . push ( commit ( ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M322", + "number": 322, + "source": "src/powersync.ts", + "line": 908, + "position": 32591, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:delete-call:begin ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M323", + "number": 323, + "source": "src/powersync.ts", + "line": 909, + "position": 32642, + "operator": "delete-call", + "original": "write({ type: `delete`, key })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:delete-call:write ( { type : `delete` , key } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M324", + "number": 324, + "source": "src/powersync.ts", + "line": 909, + "position": 32650, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:write ( { type : `delete` , key } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M325", + "number": 325, + "source": "src/powersync.ts", + "line": 909, + "position": 32666, + "operator": "field-drop", + "original": "key", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:field-drop:write ( { type : `delete` , key } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M326", + "number": 326, + "source": "src/powersync.ts", + "line": 910, + "position": 32689, + "operator": "delete-call", + "original": "appliedReceipts.push(commit())", + "replacement": ";", + "anchor": "appliedReceipts . push ( commit ( ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:delete-call:appliedReceipts . push ( commit ( ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M327", + "number": 327, + "source": "src/powersync.ts", + "line": 910, + "position": 32710, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "appliedReceipts . push ( commit ( ) )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:appliedReceipts . push ( commit ( ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M328", + "number": 328, + "source": "src/powersync.ts", + "line": 913, + "position": 32769, + "operator": "value-undefined", + "original": "Promise.all(appliedReceipts)", + "replacement": "undefined", + "anchor": "await Promise . all ( appliedReceipts )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:await Promise . all ( appliedReceipts )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M329", + "number": 329, + "source": "src/powersync.ts", + "line": 914, + "position": 32814, + "operator": "if-true", + "original": "isCurrent()", + "replacement": "true", + "anchor": "if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M330", + "number": 330, + "source": "src/powersync.ts", + "line": 914, + "position": 32814, + "operator": "if-false", + "original": "isCurrent()", + "replacement": "false", + "anchor": "if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M331", + "number": 331, + "source": "src/powersync.ts", + "line": 914, + "position": 32814, + "operator": "value-undefined", + "original": "isCurrent()", + "replacement": "undefined", + "anchor": "if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:value-undefined:if ( isCurrent ( ) ) { reconciledTrackingRevision = revision if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M332", + "number": 332, + "source": "src/powersync.ts", + "line": 918, + "position": 33056, + "operator": "if-true", + "original": "trackingOutage", + "replacement": "true", + "anchor": "if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-true:if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M333", + "number": 333, + "source": "src/powersync.ts", + "line": 918, + "position": 33056, + "operator": "if-false", + "original": "trackingOutage", + "replacement": "false", + "anchor": "if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) }", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:if-false:if ( trackingOutage ) { trackingOutage = false reportHealthy ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M334", + "number": 334, + "source": "src/powersync.ts", + "line": 919, + "position": 33107, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "trackingOutage = false", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:bool-flip:trackingOutage = false" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M335", + "number": 335, + "source": "src/powersync.ts", + "line": 920, + "position": 33129, + "operator": "delete-call", + "original": "reportHealthy()", + "replacement": ";", + "anchor": "reportHealthy ( )", + "function": "reconcileTracking", + "key": "src/powersync.ts#reconcileTracking:delete-call:reportHealthy ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M336", + "number": 336, + "source": "src/powersync.ts", + "line": 929, + "position": 33430, + "operator": "loop-false", + "original": "!stopped && reconciledTrackingRevision !== trackingRevision", + "replacement": "false", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:loop-false:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M337", + "number": 337, + "source": "src/powersync.ts", + "line": 929, + "position": 33439, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:binary-swap:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M338", + "number": 338, + "source": "src/powersync.ts", + "line": 929, + "position": 33430, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:drop-not:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M339", + "number": 339, + "source": "src/powersync.ts", + "line": 929, + "position": 33469, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:binary-swap:while ( ! stopped && reconciledTrackingRevision !== trackingRevision ) { await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) ) }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M340", + "number": 340, + "source": "src/powersync.ts", + "line": 930, + "position": 33531, + "operator": "value-undefined", + "original": "reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n })", + "replacement": "undefined", + "anchor": "await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:value-undefined:await ( rebuildPromise ??= reconcileTracking ( ) . catch ( ( error ) => { if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error ) throw error } ) . finally ( ( ) => { rebuildPromise = null } ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M341", + "number": 341, + "source": "src/powersync.ts", + "line": 935, + "position": 33844, + "operator": "if-true", + "original": "stopped", + "replacement": "true", + "anchor": "if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:if-true:if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M342", + "number": 342, + "source": "src/powersync.ts", + "line": 935, + "position": 33844, + "operator": "if-false", + "original": "stopped", + "replacement": "false", + "anchor": "if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:if-false:if ( stopped ) { } else if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M343", + "number": 343, + "source": "src/powersync.ts", + "line": 937, + "position": 33929, + "operator": "if-true", + "original": "ready", + "replacement": "true", + "anchor": "if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:if-true:if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M344", + "number": 344, + "source": "src/powersync.ts", + "line": 937, + "position": 33929, + "operator": "if-false", + "original": "ready", + "replacement": "false", + "anchor": "if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:if-false:if ( ready ) { trackingOutage = true reportError ( error ) } else fail ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M345", + "number": 345, + "source": "src/powersync.ts", + "line": 938, + "position": 33973, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "trackingOutage = true", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:bool-flip:trackingOutage = true" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M346", + "number": 346, + "source": "src/powersync.ts", + "line": 939, + "position": 33996, + "operator": "delete-call", + "original": "reportError(error)", + "replacement": ";", + "anchor": "reportError ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:delete-call:reportError ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M347", + "number": 347, + "source": "src/powersync.ts", + "line": 940, + "position": 34038, + "operator": "delete-call", + "original": "fail(error)", + "replacement": ";", + "anchor": "fail ( error )", + "function": "rebuildTracking", + "key": "src/powersync.ts#rebuildTracking:delete-call:fail ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M348", + "number": 348, + "source": "src/powersync.ts", + "line": 952, + "position": 34322, + "operator": "if-true", + "original": "stopped", + "replacement": "true", + "anchor": "if ( stopped ) return", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( stopped ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M349", + "number": 349, + "source": "src/powersync.ts", + "line": 952, + "position": 34322, + "operator": "if-false", + "original": "stopped", + "replacement": "false", + "anchor": "if ( stopped ) return", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( stopped ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M350", + "number": 350, + "source": "src/powersync.ts", + "line": 958, + "position": 34612, + "operator": "if-true", + "original": "stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted", + "replacement": "true", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M351", + "number": 351, + "source": "src/powersync.ts", + "line": 958, + "position": 34612, + "operator": "if-false", + "original": "stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted", + "replacement": "false", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M352", + "number": 352, + "source": "src/powersync.ts", + "line": 959, + "position": 34664, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M353", + "number": 353, + "source": "src/powersync.ts", + "line": 958, + "position": 34620, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M354", + "number": 354, + "source": "src/powersync.ts", + "line": 959, + "position": 34635, + "operator": "value-undefined", + "original": "releasedSubsets.has(options)", + "replacement": "undefined", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted ) { return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M355", + "number": 355, + "source": "src/powersync.ts", + "line": 965, + "position": 34790, + "operator": "field-drop", + "original": "options", + "replacement": "", + "anchor": "const demand : DemandRecord = { options , active : false }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:field-drop:const demand : DemandRecord = { options , active : false }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M356", + "number": 356, + "source": "src/powersync.ts", + "line": 965, + "position": 34799, + "operator": "field-drop", + "original": "active: false", + "replacement": "", + "anchor": "const demand : DemandRecord = { options , active : false }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:field-drop:const demand : DemandRecord = { options , active : false }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M357", + "number": 357, + "source": "src/powersync.ts", + "line": 965, + "position": 34807, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "const demand : DemandRecord = { options , active : false }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:bool-flip:const demand : DemandRecord = { options , active : false }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M358", + "number": 358, + "source": "src/powersync.ts", + "line": 966, + "position": 34825, + "operator": "delete-call", + "original": "demands.set(options, demand)", + "replacement": ";", + "anchor": "demands . set ( options , demand )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:delete-call:demands . set ( options , demand )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M359", + "number": 359, + "source": "src/powersync.ts", + "line": 968, + "position": 34904, + "operator": "value-undefined", + "original": "restConfig.onLoadSubset?.(options)", + "replacement": "undefined", + "anchor": "const cleanup = await restConfig . onLoadSubset ?. ( options )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:const cleanup = await restConfig . onLoadSubset ?. ( options )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M360", + "number": 360, + "source": "src/powersync.ts", + "line": 969, + "position": 34955, + "operator": "if-true", + "original": "cleanup", + "replacement": "true", + "anchor": "if ( cleanup ) demand . cleanup = cleanup", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( cleanup ) demand . cleanup = cleanup" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M361", + "number": 361, + "source": "src/powersync.ts", + "line": 969, + "position": 34955, + "operator": "if-false", + "original": "cleanup", + "replacement": "false", + "anchor": "if ( cleanup ) demand . cleanup = cleanup", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( cleanup ) demand . cleanup = cleanup" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M362", + "number": 362, + "source": "src/powersync.ts", + "line": 971, + "position": 35029, + "operator": "delete-call", + "original": "demands.delete(options)", + "replacement": ";", + "anchor": "demands . delete ( options )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:delete-call:demands . delete ( options )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M363", + "number": 363, + "source": "src/powersync.ts", + "line": 973, + "position": 35122, + "operator": "if-true", + "original": "collector", + "replacement": "true", + "anchor": "if ( collector ) { collector . failures . push ( { order : collector . nextOrder ++ , error , } ) }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( collector ) { collector . failures . push ( { order : collector . nextOrder ++ , error , } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M364", + "number": 364, + "source": "src/powersync.ts", + "line": 973, + "position": 35122, + "operator": "if-false", + "original": "collector", + "replacement": "false", + "anchor": "if ( collector ) { collector . failures . push ( { order : collector . nextOrder ++ , error , } ) }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( collector ) { collector . failures . push ( { order : collector . nextOrder ++ , error , } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M365", + "number": 365, + "source": "src/powersync.ts", + "line": 974, + "position": 35149, + "operator": "delete-call", + "original": "collector.failures.push({\n order: collector.nextOrder++,\n error,\n })", + "replacement": ";", + "anchor": "collector . failures . push ( { order : collector . nextOrder ++ , error , } )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:delete-call:collector . failures . push ( { order : collector . nextOrder ++ , error , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M366", + "number": 366, + "source": "src/powersync.ts", + "line": 975, + "position": 35191, + "operator": "field-drop", + "original": "order: collector.nextOrder++", + "replacement": "", + "anchor": "collector . failures . push ( { order : collector . nextOrder ++ , error , } )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:field-drop:collector . failures . push ( { order : collector . nextOrder ++ , error , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M367", + "number": 367, + "source": "src/powersync.ts", + "line": 976, + "position": 35237, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "collector . failures . push ( { order : collector . nextOrder ++ , error , } )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:field-drop:collector . failures . push ( { order : collector . nextOrder ++ , error , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M368", + "number": 368, + "source": "src/powersync.ts", + "line": 985, + "position": 35473, + "operator": "if-true", + "original": "stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand", + "replacement": "true", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-true:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M369", + "number": 369, + "source": "src/powersync.ts", + "line": 985, + "position": 35473, + "operator": "if-false", + "original": "stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand", + "replacement": "false", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:if-false:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M370", + "number": 370, + "source": "src/powersync.ts", + "line": 987, + "position": 35564, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M371", + "number": 371, + "source": "src/powersync.ts", + "line": 986, + "position": 35525, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M372", + "number": 372, + "source": "src/powersync.ts", + "line": 985, + "position": 35481, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M373", + "number": 373, + "source": "src/powersync.ts", + "line": 986, + "position": 35496, + "operator": "value-undefined", + "original": "releasedSubsets.has(options)", + "replacement": "undefined", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M374", + "number": 374, + "source": "src/powersync.ts", + "line": 988, + "position": 35600, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:binary-swap:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M375", + "number": 375, + "source": "src/powersync.ts", + "line": 988, + "position": 35579, + "operator": "value-undefined", + "original": "demands.get(options)", + "replacement": "undefined", + "anchor": "if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:if ( stopped || releasedSubsets . has ( options ) || options . signal ?. aborted || demands . get ( options ) !== demand ) { cleanupDemand ( demand ) return }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M376", + "number": 376, + "source": "src/powersync.ts", + "line": 990, + "position": 35637, + "operator": "delete-call", + "original": "cleanupDemand(demand)", + "replacement": ";", + "anchor": "cleanupDemand ( demand )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:delete-call:cleanupDemand ( demand )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M377", + "number": 377, + "source": "src/powersync.ts", + "line": 994, + "position": 35717, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "demand . active = true", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:bool-flip:demand . active = true" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M378", + "number": 378, + "source": "src/powersync.ts", + "line": 996, + "position": 35767, + "operator": "value-undefined", + "original": "rebuildTracking()", + "replacement": "undefined", + "anchor": "await rebuildTracking ( )", + "function": "loadSubset", + "key": "src/powersync.ts#loadSubset:value-undefined:await rebuildTracking ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M379", + "number": 379, + "source": "src/powersync.ts", + "line": 1000, + "position": 35900, + "operator": "value-undefined", + "original": "loadSubset(options)", + "replacement": "undefined", + "anchor": "const task = loadSubset ( options )", + "function": "trackLoadSubset", + "key": "src/powersync.ts#trackLoadSubset:value-undefined:const task = loadSubset ( options )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M380", + "number": 380, + "source": "src/powersync.ts", + "line": 1001, + "position": 35930, + "operator": "delete-call", + "original": "pendingDemandLoads.add(task)", + "replacement": ";", + "anchor": "pendingDemandLoads . add ( task )", + "function": "trackLoadSubset", + "key": "src/powersync.ts#trackLoadSubset:delete-call:pendingDemandLoads . add ( task )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M381", + "number": 381, + "source": "src/powersync.ts", + "line": 1002, + "position": 35990, + "operator": "value-undefined", + "original": "pendingDemandLoads.delete(task)", + "replacement": "undefined", + "anchor": "const finish = ( ) => pendingDemandLoads . delete ( task )", + "function": "finish", + "key": "src/powersync.ts#finish:value-undefined:const finish = ( ) => pendingDemandLoads . delete ( task )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M382", + "number": 382, + "source": "src/powersync.ts", + "line": 1003, + "position": 36037, + "operator": "value-undefined", + "original": "task.then(finish, finish)", + "replacement": "undefined", + "anchor": "void task . then ( finish , finish )", + "function": "trackLoadSubset", + "key": "src/powersync.ts#trackLoadSubset:value-undefined:void task . then ( finish , finish )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M383", + "number": 383, + "source": "src/powersync.ts", + "line": 1011, + "position": 36242, + "operator": "if-true", + "original": "!compiled.where", + "replacement": "true", + "anchor": "if ( ! compiled . where ) return 'TRUE'", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:if-true:if ( ! compiled . where ) return 'TRUE'" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M384", + "number": 384, + "source": "src/powersync.ts", + "line": 1011, + "position": 36242, + "operator": "if-false", + "original": "!compiled.where", + "replacement": "false", + "anchor": "if ( ! compiled . where ) return 'TRUE'", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:if-false:if ( ! compiled . where ) return 'TRUE'" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M385", + "number": 385, + "source": "src/powersync.ts", + "line": 1011, + "position": 36242, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! compiled . where ) return 'TRUE'", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:drop-not:if ( ! compiled . where ) return 'TRUE'" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M386", + "number": 386, + "source": "src/powersync.ts", + "line": 1012, + "position": 36300, + "operator": "value-undefined", + "original": "compiled.where.split('?')", + "replacement": "undefined", + "anchor": "const sqlParts = compiled . where . split ( '?' )", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:value-undefined:const sqlParts = compiled . where . split ( '?' )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M387", + "number": 387, + "source": "src/powersync.ts", + "line": 1013, + "position": 36343, + "operator": "value-undefined", + "original": "sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )", + "replacement": "undefined", + "anchor": "return sanitizeSQL ( sqlParts as unknown as TemplateStringsArray , ... compiled . params , )", + "function": "toInlinedWhereClause", + "key": "src/powersync.ts#toInlinedWhereClause:value-undefined:return sanitizeSQL ( sqlParts as unknown as TemplateStringsArray , ... compiled . params , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M388", + "number": 388, + "source": "src/powersync.ts", + "line": 1020, + "position": 36549, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `Could not clean up subset hook for ${viewName}`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })", + "function": "reportDemandCleanupFailure", + "key": "src/powersync.ts#reportDemandCleanupFailure:delete-call:database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M389", + "number": 389, + "source": "src/powersync.ts", + "line": 1021, + "position": 36583, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })", + "function": "reportDemandCleanupFailure", + "key": "src/powersync.ts#reportDemandCleanupFailure:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M390", + "number": 390, + "source": "src/powersync.ts", + "line": 1022, + "position": 36619, + "operator": "field-drop", + "original": "message: `Could not clean up subset hook for ${viewName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })", + "function": "reportDemandCleanupFailure", + "key": "src/powersync.ts#reportDemandCleanupFailure:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M391", + "number": 391, + "source": "src/powersync.ts", + "line": 1023, + "position": 36690, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })", + "function": "reportDemandCleanupFailure", + "key": "src/powersync.ts#reportDemandCleanupFailure:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not clean up subset hook for ${ viewName } `,\n error,\n })~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M392", + "number": 392, + "source": "src/powersync.ts", + "line": 1028, + "position": 36795, + "operator": "delete-call", + "original": "demands.delete(demand.options)", + "replacement": ";", + "anchor": "demands . delete ( demand . options )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:demands . delete ( demand . options )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M393", + "number": 393, + "source": "src/powersync.ts", + "line": 1034, + "position": 37081, + "operator": "value-undefined", + "original": "cleanup?.()", + "replacement": "undefined", + "anchor": "const result : unknown = cleanup ?. ( )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:const result : unknown = cleanup ?. ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M394", + "number": 394, + "source": "src/powersync.ts", + "line": 1035, + "position": 37109, + "operator": "if-true", + "original": "!isPromiseLike(result)", + "replacement": "true", + "anchor": "if ( ! isPromiseLike ( result ) ) return", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-true:if ( ! isPromiseLike ( result ) ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M395", + "number": 395, + "source": "src/powersync.ts", + "line": 1035, + "position": 37109, + "operator": "if-false", + "original": "!isPromiseLike(result)", + "replacement": "false", + "anchor": "if ( ! isPromiseLike ( result ) ) return", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-false:if ( ! isPromiseLike ( result ) ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M396", + "number": 396, + "source": "src/powersync.ts", + "line": 1035, + "position": 37109, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! isPromiseLike ( result ) ) return", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:drop-not:if ( ! isPromiseLike ( result ) ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M397", + "number": 397, + "source": "src/powersync.ts", + "line": 1035, + "position": 37110, + "operator": "value-undefined", + "original": "isPromiseLike(result)", + "replacement": "undefined", + "anchor": "if ( ! isPromiseLike ( result ) ) return", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:if ( ! isPromiseLike ( result ) ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M398", + "number": 398, + "source": "src/powersync.ts", + "line": 1037, + "position": 37166, + "operator": "value-undefined", + "original": "Promise.resolve(result)", + "replacement": "undefined", + "anchor": "const task = Promise . resolve ( result )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:const task = Promise . resolve ( result )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M399", + "number": 399, + "source": "src/powersync.ts", + "line": 1038, + "position": 37202, + "operator": "delete-call", + "original": "pendingDemandCleanups.add(task)", + "replacement": ";", + "anchor": "pendingDemandCleanups . add ( task )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:pendingDemandCleanups . add ( task )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M400", + "number": 400, + "source": "src/powersync.ts", + "line": 1039, + "position": 37267, + "operator": "value-undefined", + "original": "pendingDemandCleanups.delete(task)", + "replacement": "undefined", + "anchor": "const finish = ( ) => pendingDemandCleanups . delete ( task )", + "function": "finish", + "key": "src/powersync.ts#finish:value-undefined:const finish = ( ) => pendingDemandCleanups . delete ( task )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M401", + "number": 401, + "source": "src/powersync.ts", + "line": 1040, + "position": 37319, + "operator": "value-undefined", + "original": "task.then(finish, finish)", + "replacement": "undefined", + "anchor": "void task . then ( finish , finish )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:void task . then ( finish , finish )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M402", + "number": 402, + "source": "src/powersync.ts", + "line": 1041, + "position": 37361, + "operator": "if-true", + "original": "collector", + "replacement": "true", + "anchor": "if ( collector ) { collector . tasks . push ( { order : order ! , promise : task } ) collector . ownedTasks . add ( task ) void task . catch ( ( ) => undefined ) } else { void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-true:if ( collector ) { collector . tasks . push ( { order : order ! , promise : task } ) collector . ownedTasks . add ( task ) void task . catch ( ( ) => undefined ) } else { void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M403", + "number": 403, + "source": "src/powersync.ts", + "line": 1041, + "position": 37361, + "operator": "if-false", + "original": "collector", + "replacement": "false", + "anchor": "if ( collector ) { collector . tasks . push ( { order : order ! , promise : task } ) collector . ownedTasks . add ( task ) void task . catch ( ( ) => undefined ) } else { void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-false:if ( collector ) { collector . tasks . push ( { order : order ! , promise : task } ) collector . ownedTasks . add ( task ) void task . catch ( ( ) => undefined ) } else { void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M404", + "number": 404, + "source": "src/powersync.ts", + "line": 1042, + "position": 37388, + "operator": "delete-call", + "original": "collector.tasks.push({ order: order!, promise: task })", + "replacement": ";", + "anchor": "collector . tasks . push ( { order : order ! , promise : task } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:collector . tasks . push ( { order : order ! , promise : task } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M405", + "number": 405, + "source": "src/powersync.ts", + "line": 1042, + "position": 37411, + "operator": "field-drop", + "original": "order: order!", + "replacement": "", + "anchor": "collector . tasks . push ( { order : order ! , promise : task } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:field-drop:collector . tasks . push ( { order : order ! , promise : task } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M406", + "number": 406, + "source": "src/powersync.ts", + "line": 1042, + "position": 37426, + "operator": "field-drop", + "original": "promise: task", + "replacement": "", + "anchor": "collector . tasks . push ( { order : order ! , promise : task } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:field-drop:collector . tasks . push ( { order : order ! , promise : task } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M407", + "number": 407, + "source": "src/powersync.ts", + "line": 1043, + "position": 37457, + "operator": "delete-call", + "original": "collector.ownedTasks.add(task)", + "replacement": ";", + "anchor": "collector . ownedTasks . add ( task )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:collector . ownedTasks . add ( task )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M408", + "number": 408, + "source": "src/powersync.ts", + "line": 1044, + "position": 37507, + "operator": "value-undefined", + "original": "task.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void task . catch ( ( ) => undefined )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:void task . catch ( ( ) => undefined )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M409", + "number": 409, + "source": "src/powersync.ts", + "line": 1046, + "position": 37575, + "operator": "value-undefined", + "original": "task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })", + "replacement": "undefined", + "anchor": "void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:void task . catch ( ( error ) => { if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) } } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M410", + "number": 410, + "source": "src/powersync.ts", + "line": 1047, + "position": 37619, + "operator": "if-true", + "original": "!demandCleanupCollector?.ownedTasks.has(task)", + "replacement": "true", + "anchor": "if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-true:if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M411", + "number": 411, + "source": "src/powersync.ts", + "line": 1047, + "position": 37619, + "operator": "if-false", + "original": "!demandCleanupCollector?.ownedTasks.has(task)", + "replacement": "false", + "anchor": "if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-false:if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M412", + "number": 412, + "source": "src/powersync.ts", + "line": 1047, + "position": 37619, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:drop-not:if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M413", + "number": 413, + "source": "src/powersync.ts", + "line": 1047, + "position": 37620, + "operator": "value-undefined", + "original": "demandCleanupCollector?.ownedTasks.has(task)", + "replacement": "undefined", + "anchor": "if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:value-undefined:if ( ! demandCleanupCollector ?. ownedTasks . has ( task ) ) { reportDemandCleanupFailure ( error ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M414", + "number": 414, + "source": "src/powersync.ts", + "line": 1048, + "position": 37686, + "operator": "delete-call", + "original": "reportDemandCleanupFailure(error)", + "replacement": ";", + "anchor": "reportDemandCleanupFailure ( error )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:reportDemandCleanupFailure ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M415", + "number": 415, + "source": "src/powersync.ts", + "line": 1053, + "position": 37813, + "operator": "if-true", + "original": "collector", + "replacement": "true", + "anchor": "if ( collector ) { collector . failures . push ( { order : order ! , error } ) } else { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-true:if ( collector ) { collector . failures . push ( { order : order ! , error } ) } else { reportDemandCleanupFailure ( error ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M416", + "number": 416, + "source": "src/powersync.ts", + "line": 1053, + "position": 37813, + "operator": "if-false", + "original": "collector", + "replacement": "false", + "anchor": "if ( collector ) { collector . failures . push ( { order : order ! , error } ) } else { reportDemandCleanupFailure ( error ) }", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:if-false:if ( collector ) { collector . failures . push ( { order : order ! , error } ) } else { reportDemandCleanupFailure ( error ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M417", + "number": 417, + "source": "src/powersync.ts", + "line": 1054, + "position": 37840, + "operator": "delete-call", + "original": "collector.failures.push({ order: order!, error })", + "replacement": ";", + "anchor": "collector . failures . push ( { order : order ! , error } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:collector . failures . push ( { order : order ! , error } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M418", + "number": 418, + "source": "src/powersync.ts", + "line": 1054, + "position": 37866, + "operator": "field-drop", + "original": "order: order!", + "replacement": "", + "anchor": "collector . failures . push ( { order : order ! , error } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:field-drop:collector . failures . push ( { order : order ! , error } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M419", + "number": 419, + "source": "src/powersync.ts", + "line": 1054, + "position": 37881, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "collector . failures . push ( { order : order ! , error } )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:field-drop:collector . failures . push ( { order : order ! , error } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M420", + "number": 420, + "source": "src/powersync.ts", + "line": 1056, + "position": 37925, + "operator": "delete-call", + "original": "reportDemandCleanupFailure(error)", + "replacement": ";", + "anchor": "reportDemandCleanupFailure ( error )", + "function": "cleanupDemand", + "key": "src/powersync.ts#cleanupDemand:delete-call:reportDemandCleanupFailure ( error )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M421", + "number": 421, + "source": "src/powersync.ts", + "line": 1067, + "position": 38237, + "operator": "value-undefined", + "original": "collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error)", + "replacement": "undefined", + "anchor": "const errors = [ ... primaryFailures , ... collector . failures . slice ( ) . sort ( ( left , right ) => left . order - right . order ) . map ( ( { error } ) => error ) , ]", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:value-undefined:const errors = [ ... primaryFailures , ... collector . failures . slice ( ) . sort ( ( left , right ) => left . order - right . order ) . map ( ( { error } ) => error ) , ]" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M422", + "number": 422, + "source": "src/powersync.ts", + "line": 1072, + "position": 38410, + "operator": "if-true", + "original": "errors.length === 1", + "replacement": "true", + "anchor": "if ( errors . length === 1 ) throw errors [ 0 ]", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:if-true:if ( errors . length === 1 ) throw errors [ 0 ]" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M423", + "number": 423, + "source": "src/powersync.ts", + "line": 1072, + "position": 38410, + "operator": "if-false", + "original": "errors.length === 1", + "replacement": "false", + "anchor": "if ( errors . length === 1 ) throw errors [ 0 ]", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:if-false:if ( errors . length === 1 ) throw errors [ 0 ]" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M424", + "number": 424, + "source": "src/powersync.ts", + "line": 1072, + "position": 38424, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( errors . length === 1 ) throw errors [ 0 ]", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:binary-swap:if ( errors . length === 1 ) throw errors [ 0 ]" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M425", + "number": 425, + "source": "src/powersync.ts", + "line": 1073, + "position": 38461, + "operator": "if-true", + "original": "errors.length > 1", + "replacement": "true", + "anchor": "if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:if-true:if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M426", + "number": 426, + "source": "src/powersync.ts", + "line": 1073, + "position": 38461, + "operator": "if-false", + "original": "errors.length > 1", + "replacement": "false", + "anchor": "if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:if-false:if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M427", + "number": 427, + "source": "src/powersync.ts", + "line": 1073, + "position": 38475, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:binary-swap:if ( errors . length > 1 ) { throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M428", + "number": 428, + "source": "src/powersync.ts", + "line": 1075, + "position": 38571, + "operator": "field-drop", + "original": "cause: errors[0]", + "replacement": "", + "anchor": "throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } )", + "function": "throwDemandCleanupFailures", + "key": "src/powersync.ts#throwDemandCleanupFailures:field-drop:throw new AggregateError ( errors , `PowerSync cleanup failed` , { cause : errors [ 0 ] , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M429", + "number": 429, + "source": "src/powersync.ts", + "line": 1083, + "position": 38778, + "operator": "value-undefined", + "original": "compileSQLite({ where: options.where })", + "replacement": "undefined", + "anchor": "const compiledDeparting = compileSQLite ( { where : options . where } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const compiledDeparting = compileSQLite ( { where : options . where } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M430", + "number": 430, + "source": "src/powersync.ts", + "line": 1083, + "position": 38794, + "operator": "field-drop", + "original": "where: options.where", + "replacement": "", + "anchor": "const compiledDeparting = compileSQLite ( { where : options . where } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:field-drop:const compiledDeparting = compileSQLite ( { where : options . where } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M431", + "number": 431, + "source": "src/powersync.ts", + "line": 1084, + "position": 38854, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledDeparting)", + "replacement": "undefined", + "anchor": "const departingWhereSQL = toInlinedWhereClause ( compiledDeparting )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const departingWhereSQL = toInlinedWhereClause ( compiledDeparting )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M432", + "number": 432, + "source": "src/powersync.ts", + "line": 1087, + "position": 38980, + "operator": "if-true", + "original": "stopped", + "replacement": "true", + "anchor": "if ( stopped ) return", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( stopped ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M433", + "number": 433, + "source": "src/powersync.ts", + "line": 1087, + "position": 38980, + "operator": "if-false", + "original": "stopped", + "replacement": "false", + "anchor": "if ( stopped ) return", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( stopped ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M434", + "number": 434, + "source": "src/powersync.ts", + "line": 1089, + "position": 39069, + "operator": "value-undefined", + "original": "activeWhereExpressions()", + "replacement": "undefined", + "anchor": "const active = activeWhereExpressions ( )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const active = activeWhereExpressions ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M435", + "number": 435, + "source": "src/powersync.ts", + "line": 1091, + "position": 39146, + "operator": "if-true", + "original": "active.length === 0", + "replacement": "true", + "anchor": "if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M436", + "number": 436, + "source": "src/powersync.ts", + "line": 1091, + "position": 39146, + "operator": "if-false", + "original": "active.length === 0", + "replacement": "false", + "anchor": "if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M437", + "number": 437, + "source": "src/powersync.ts", + "line": 1091, + "position": 39160, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:binary-swap:if ( active . length === 0 ) { evictionSQL = `SELECT id FROM ${ viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M438", + "number": 438, + "source": "src/powersync.ts", + "line": 1095, + "position": 39344, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:binary-swap:const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M439", + "number": 439, + "source": "src/powersync.ts", + "line": 1097, + "position": 39401, + "operator": "value-undefined", + "original": "or(active[0], active[1], ...active.slice(2))", + "replacement": "undefined", + "anchor": "const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M440", + "number": 440, + "source": "src/powersync.ts", + "line": 1097, + "position": 39429, + "operator": "value-undefined", + "original": "active.slice(2)", + "replacement": "undefined", + "anchor": "const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const combinedRemaining = active . length === 1 ? active [ 0 ] ! : or ( active [ 0 ] , active [ 1 ] , ... active . slice ( 2 ) )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M441", + "number": 441, + "source": "src/powersync.ts", + "line": 1098, + "position": 39486, + "operator": "value-undefined", + "original": "compileSQLite({\n where: combinedRemaining,\n })", + "replacement": "undefined", + "anchor": "const compiledRemaining = compileSQLite ( { where : combinedRemaining , } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const compiledRemaining = compileSQLite ( { where : combinedRemaining , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M442", + "number": 442, + "source": "src/powersync.ts", + "line": 1099, + "position": 39518, + "operator": "field-drop", + "original": "where: combinedRemaining", + "replacement": "", + "anchor": "const compiledRemaining = compileSQLite ( { where : combinedRemaining , } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:field-drop:const compiledRemaining = compileSQLite ( { where : combinedRemaining , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M443", + "number": 443, + "source": "src/powersync.ts", + "line": 1101, + "position": 39601, + "operator": "value-undefined", + "original": "toInlinedWhereClause(compiledRemaining)", + "replacement": "undefined", + "anchor": "const remainingWhereSQL = toInlinedWhereClause ( compiledRemaining )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:const remainingWhereSQL = toInlinedWhereClause ( compiledRemaining )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M444", + "number": 444, + "source": "src/powersync.ts", + "line": 1105, + "position": 39805, + "operator": "value-undefined", + "original": "database.getAll<{ id: string }>(evictionSQL)", + "replacement": "undefined", + "anchor": "rowsToEvict = await database . getAll < { id : string } > ( evictionSQL )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:rowsToEvict = await database . getAll < { id : string } > ( evictionSQL )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M445", + "number": 445, + "source": "src/powersync.ts", + "line": 1107, + "position": 39986, + "operator": "if-true", + "original": "stopped", + "replacement": "true", + "anchor": "if ( stopped ) return", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( stopped ) return~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M446", + "number": 446, + "source": "src/powersync.ts", + "line": 1107, + "position": 39986, + "operator": "if-false", + "original": "stopped", + "replacement": "false", + "anchor": "if ( stopped ) return", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( stopped ) return~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M447", + "number": 447, + "source": "src/powersync.ts", + "line": 1108, + "position": 40018, + "operator": "if-true", + "original": "trackingRevision === revision", + "replacement": "true", + "anchor": "if ( trackingRevision === revision ) break", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( trackingRevision === revision ) break" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M448", + "number": 448, + "source": "src/powersync.ts", + "line": 1108, + "position": 40018, + "operator": "if-false", + "original": "trackingRevision === revision", + "replacement": "false", + "anchor": "if ( trackingRevision === revision ) break", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( trackingRevision === revision ) break" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M449", + "number": 449, + "source": "src/powersync.ts", + "line": 1108, + "position": 40035, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( trackingRevision === revision ) break", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:binary-swap:if ( trackingRevision === revision ) break" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M450", + "number": 450, + "source": "src/powersync.ts", + "line": 1110, + "position": 40081, + "operator": "if-true", + "original": "rowsToEvict.length > 0", + "replacement": "true", + "anchor": "if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-true:if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M451", + "number": 451, + "source": "src/powersync.ts", + "line": 1110, + "position": 40081, + "operator": "if-false", + "original": "rowsToEvict.length > 0", + "replacement": "false", + "anchor": "if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:if-false:if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M452", + "number": 452, + "source": "src/powersync.ts", + "line": 1110, + "position": 40100, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:binary-swap:if ( rowsToEvict . length > 0 ) { begin ( ) for ( const { id } of rowsToEvict ) { write ( { type : `delete` , key : id } ) } void commit ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M453", + "number": 453, + "source": "src/powersync.ts", + "line": 1111, + "position": 40119, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:delete-call:begin ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M454", + "number": 454, + "source": "src/powersync.ts", + "line": 1113, + "position": 40189, + "operator": "delete-call", + "original": "write({ type: `delete`, key: id })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key : id } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:delete-call:write ( { type : `delete` , key : id } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M455", + "number": 455, + "source": "src/powersync.ts", + "line": 1113, + "position": 40197, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key : id } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:field-drop:write ( { type : `delete` , key : id } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M456", + "number": 456, + "source": "src/powersync.ts", + "line": 1113, + "position": 40213, + "operator": "field-drop", + "original": "key: id", + "replacement": "", + "anchor": "write ( { type : `delete` , key : id } )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:field-drop:write ( { type : `delete` , key : id } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M457", + "number": 457, + "source": "src/powersync.ts", + "line": 1115, + "position": 40255, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "void commit ( )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:void commit ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M458", + "number": 458, + "source": "src/powersync.ts", + "line": 1117, + "position": 40292, + "operator": "value-undefined", + "original": "rebuildTracking()", + "replacement": "undefined", + "anchor": "await rebuildTracking ( )", + "function": "performPhysicalRelease", + "key": "src/powersync.ts#performPhysicalRelease:value-undefined:await rebuildTracking ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M459", + "number": 459, + "source": "src/powersync.ts", + "line": 1121, + "position": 40392, + "operator": "if-true", + "original": "stopped || drainingReleases || releaseRetryTimer", + "replacement": "true", + "anchor": "if ( stopped || drainingReleases || releaseRetryTimer ) return", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:if-true:if ( stopped || drainingReleases || releaseRetryTimer ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M460", + "number": 460, + "source": "src/powersync.ts", + "line": 1121, + "position": 40392, + "operator": "if-false", + "original": "stopped || drainingReleases || releaseRetryTimer", + "replacement": "false", + "anchor": "if ( stopped || drainingReleases || releaseRetryTimer ) return", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:if-false:if ( stopped || drainingReleases || releaseRetryTimer ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M461", + "number": 461, + "source": "src/powersync.ts", + "line": 1121, + "position": 40420, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || drainingReleases || releaseRetryTimer ) return", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:binary-swap:if ( stopped || drainingReleases || releaseRetryTimer ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M462", + "number": 462, + "source": "src/powersync.ts", + "line": 1121, + "position": 40400, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || drainingReleases || releaseRetryTimer ) return", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:binary-swap:if ( stopped || drainingReleases || releaseRetryTimer ) return~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M463", + "number": 463, + "source": "src/powersync.ts", + "line": 1122, + "position": 40463, + "operator": "if-true", + "original": "delay > 0", + "replacement": "true", + "anchor": "if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:if-true:if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M464", + "number": 464, + "source": "src/powersync.ts", + "line": 1122, + "position": 40463, + "operator": "if-false", + "original": "delay > 0", + "replacement": "false", + "anchor": "if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:if-false:if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M465", + "number": 465, + "source": "src/powersync.ts", + "line": 1122, + "position": 40469, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:binary-swap:if ( delay > 0 ) { releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay ) return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M466", + "number": 466, + "source": "src/powersync.ts", + "line": 1123, + "position": 40508, + "operator": "value-undefined", + "original": "setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)", + "replacement": "undefined", + "anchor": "releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay )", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:value-undefined:releaseRetryTimer = setTimeout ( ( ) => { releaseRetryTimer = undefined void drainReleases ( ) } , delay )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M467", + "number": 467, + "source": "src/powersync.ts", + "line": 1125, + "position": 40590, + "operator": "value-undefined", + "original": "drainReleases()", + "replacement": "undefined", + "anchor": "void drainReleases ( )", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:value-undefined:void drainReleases ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M468", + "number": 468, + "source": "src/powersync.ts", + "line": 1129, + "position": 40674, + "operator": "value-undefined", + "original": "drainReleases()", + "replacement": "undefined", + "anchor": "void drainReleases ( )", + "function": "scheduleReleaseDrain", + "key": "src/powersync.ts#scheduleReleaseDrain:value-undefined:void drainReleases ( )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M469", + "number": 469, + "source": "src/powersync.ts", + "line": 1133, + "position": 40771, + "operator": "if-true", + "original": "stopped || drainingReleases", + "replacement": "true", + "anchor": "if ( stopped || drainingReleases ) return", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:if-true:if ( stopped || drainingReleases ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M470", + "number": 470, + "source": "src/powersync.ts", + "line": 1133, + "position": 40771, + "operator": "if-false", + "original": "stopped || drainingReleases", + "replacement": "false", + "anchor": "if ( stopped || drainingReleases ) return", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:if-false:if ( stopped || drainingReleases ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M471", + "number": 471, + "source": "src/powersync.ts", + "line": 1133, + "position": 40779, + "operator": "binary-swap", + "original": "||", + "replacement": "&&", + "anchor": "if ( stopped || drainingReleases ) return", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:if ( stopped || drainingReleases ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M472", + "number": 472, + "source": "src/powersync.ts", + "line": 1134, + "position": 40836, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "drainingReleases = true", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:bool-flip:drainingReleases = true" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M473", + "number": 473, + "source": "src/powersync.ts", + "line": 1139, + "position": 41090, + "operator": "loop-false", + "original": "!stopped && index < attempts", + "replacement": "false", + "anchor": "for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:loop-false:for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M474", + "number": 474, + "source": "src/powersync.ts", + "line": 1139, + "position": 41099, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M475", + "number": 475, + "source": "src/powersync.ts", + "line": 1139, + "position": 41090, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:drop-not:for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M476", + "number": 476, + "source": "src/powersync.ts", + "line": 1139, + "position": 41108, + "operator": "binary-swap", + "original": "<", + "replacement": "<=", + "anchor": "for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:for ( let index = 0 ; ! stopped && index < attempts ; index ++ ) { const pending = pendingReleases . shift ( ) ! try { await performPhysicalRelease ( pending . options ) } catch ( error ) { pending . failures ++ pendingReleases . push ( pending ) const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , ) retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay ) database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })\n }\n }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M477", + "number": 477, + "source": "src/powersync.ts", + "line": 1140, + "position": 41161, + "operator": "value-undefined", + "original": "pendingReleases.shift()", + "replacement": "undefined", + "anchor": "const pending = pendingReleases . shift ( ) !", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:value-undefined:const pending = pendingReleases . shift ( ) !" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M478", + "number": 478, + "source": "src/powersync.ts", + "line": 1142, + "position": 41228, + "operator": "value-undefined", + "original": "performPhysicalRelease(pending.options)", + "replacement": "undefined", + "anchor": "await performPhysicalRelease ( pending . options )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:value-undefined:await performPhysicalRelease ( pending . options )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M479", + "number": 479, + "source": "src/powersync.ts", + "line": 1145, + "position": 41351, + "operator": "delete-call", + "original": "pendingReleases.push(pending)", + "replacement": ";", + "anchor": "pendingReleases . push ( pending )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:delete-call:pendingReleases . push ( pending )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M480", + "number": 480, + "source": "src/powersync.ts", + "line": 1146, + "position": 41411, + "operator": "value-undefined", + "original": "Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )", + "replacement": "undefined", + "anchor": "const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:value-undefined:const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M481", + "number": 481, + "source": "src/powersync.ts", + "line": 1147, + "position": 41469, + "operator": "arithmetic-swap", + "original": "-", + "replacement": "+", + "anchor": "const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:arithmetic-swap:const delay = Math . min ( 1000 * 2 ** ( pending . failures - 1 ) , 30000 , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M482", + "number": 482, + "source": "src/powersync.ts", + "line": 1151, + "position": 41576, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M483", + "number": 483, + "source": "src/powersync.ts", + "line": 1151, + "position": 41592, + "operator": "value-undefined", + "original": "Math.min(retryDelay, delay)", + "replacement": "undefined", + "anchor": "retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:value-undefined:retryDelay = retryDelay === 0 ? delay : Math . min ( retryDelay , delay )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M484", + "number": 484, + "source": "src/powersync.ts", + "line": 1152, + "position": 41636, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.error,\n message: `Could not release subset tracking for ${viewName}; retrying`,\n error,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:delete-call:database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M485", + "number": 485, + "source": "src/powersync.ts", + "line": 1153, + "position": 41676, + "operator": "field-drop", + "original": "level: LogLevels.error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M486", + "number": 486, + "source": "src/powersync.ts", + "line": 1154, + "position": 41718, + "operator": "field-drop", + "original": "message: `Could not release subset tracking for ${viewName}; retrying`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M487", + "number": 487, + "source": "src/powersync.ts", + "line": 1155, + "position": 41808, + "operator": "field-drop", + "original": "error", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:field-drop:database . logger . log ( { level : LogLevels . error , message : `Could not release subset tracking for ${ viewName } ; retrying `,\n error,\n })~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M488", + "number": 488, + "source": "src/powersync.ts", + "line": 1160, + "position": 41917, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "drainingReleases = false", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:bool-flip:drainingReleases = false" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M489", + "number": 489, + "source": "src/powersync.ts", + "line": 1162, + "position": 41949, + "operator": "if-true", + "original": "pendingReleases.length > 0", + "replacement": "true", + "anchor": "if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:if-true:if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M490", + "number": 490, + "source": "src/powersync.ts", + "line": 1162, + "position": 41949, + "operator": "if-false", + "original": "pendingReleases.length > 0", + "replacement": "false", + "anchor": "if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:if-false:if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M491", + "number": 491, + "source": "src/powersync.ts", + "line": 1162, + "position": 41972, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:binary-swap:if ( pendingReleases . length > 0 ) scheduleReleaseDrain ( retryDelay )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M492", + "number": 492, + "source": "src/powersync.ts", + "line": 1162, + "position": 41977, + "operator": "delete-call", + "original": "scheduleReleaseDrain(retryDelay)", + "replacement": ";", + "anchor": "scheduleReleaseDrain ( retryDelay )", + "function": "drainReleases", + "key": "src/powersync.ts#drainReleases:delete-call:scheduleReleaseDrain ( retryDelay )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M493", + "number": 493, + "source": "src/powersync.ts", + "line": 1166, + "position": 42100, + "operator": "delete-call", + "original": "releasedSubsets.add(options)", + "replacement": ";", + "anchor": "releasedSubsets . add ( options )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:releasedSubsets . add ( options )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M494", + "number": 494, + "source": "src/powersync.ts", + "line": 1167, + "position": 42154, + "operator": "value-undefined", + "original": "demands.get(options)", + "replacement": "undefined", + "anchor": "const demand = demands . get ( options )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:value-undefined:const demand = demands . get ( options )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M495", + "number": 495, + "source": "src/powersync.ts", + "line": 1168, + "position": 42189, + "operator": "if-true", + "original": "!demand", + "replacement": "true", + "anchor": "if ( ! demand ) return", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-true:if ( ! demand ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M496", + "number": 496, + "source": "src/powersync.ts", + "line": 1168, + "position": 42189, + "operator": "if-false", + "original": "!demand", + "replacement": "false", + "anchor": "if ( ! demand ) return", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-false:if ( ! demand ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M497", + "number": 497, + "source": "src/powersync.ts", + "line": 1168, + "position": 42189, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! demand ) return", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:drop-not:if ( ! demand ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M498", + "number": 498, + "source": "src/powersync.ts", + "line": 1171, + "position": 42262, + "operator": "if-true", + "original": "wasActive", + "replacement": "true", + "anchor": "if ( wasActive ) trackingRevision ++", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-true:if ( wasActive ) trackingRevision ++" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M499", + "number": 499, + "source": "src/powersync.ts", + "line": 1171, + "position": 42262, + "operator": "if-false", + "original": "wasActive", + "replacement": "false", + "anchor": "if ( wasActive ) trackingRevision ++", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-false:if ( wasActive ) trackingRevision ++" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M500", + "number": 500, + "source": "src/powersync.ts", + "line": 1172, + "position": 42302, + "operator": "delete-call", + "original": "cleanupDemand(demand)", + "replacement": ";", + "anchor": "cleanupDemand ( demand )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:cleanupDemand ( demand )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M501", + "number": 501, + "source": "src/powersync.ts", + "line": 1174, + "position": 42339, + "operator": "if-true", + "original": "wasActive", + "replacement": "true", + "anchor": "if ( wasActive ) { pendingReleases . push ( { options , failures : 0 } ) clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined scheduleReleaseDrain ( ) }", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-true:if ( wasActive ) { pendingReleases . push ( { options , failures : 0 } ) clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined scheduleReleaseDrain ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M502", + "number": 502, + "source": "src/powersync.ts", + "line": 1174, + "position": 42339, + "operator": "if-false", + "original": "wasActive", + "replacement": "false", + "anchor": "if ( wasActive ) { pendingReleases . push ( { options , failures : 0 } ) clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined scheduleReleaseDrain ( ) }", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:if-false:if ( wasActive ) { pendingReleases . push ( { options , failures : 0 } ) clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined scheduleReleaseDrain ( ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M503", + "number": 503, + "source": "src/powersync.ts", + "line": 1175, + "position": 42364, + "operator": "delete-call", + "original": "pendingReleases.push({ options, failures: 0 })", + "replacement": ";", + "anchor": "pendingReleases . push ( { options , failures : 0 } )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:pendingReleases . push ( { options , failures : 0 } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M504", + "number": 504, + "source": "src/powersync.ts", + "line": 1175, + "position": 42387, + "operator": "field-drop", + "original": "options", + "replacement": "", + "anchor": "pendingReleases . push ( { options , failures : 0 } )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:field-drop:pendingReleases . push ( { options , failures : 0 } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M505", + "number": 505, + "source": "src/powersync.ts", + "line": 1175, + "position": 42396, + "operator": "field-drop", + "original": "failures: 0", + "replacement": "", + "anchor": "pendingReleases . push ( { options , failures : 0 } )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:field-drop:pendingReleases . push ( { options , failures : 0 } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M506", + "number": 506, + "source": "src/powersync.ts", + "line": 1177, + "position": 42492, + "operator": "delete-call", + "original": "clearTimeout(releaseRetryTimer)", + "replacement": ";", + "anchor": "clearTimeout ( releaseRetryTimer )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:clearTimeout ( releaseRetryTimer )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M507", + "number": 507, + "source": "src/powersync.ts", + "line": 1179, + "position": 42578, + "operator": "delete-call", + "original": "scheduleReleaseDrain()", + "replacement": ";", + "anchor": "scheduleReleaseDrain ( )", + "function": "unloadSubset", + "key": "src/powersync.ts#unloadSubset:delete-call:scheduleReleaseDrain ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M508", + "number": 508, + "source": "src/powersync.ts", + "line": 1183, + "position": 42632, + "operator": "delete-call", + "original": "markReady()", + "replacement": ";", + "anchor": "markReady ( )", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:delete-call:markReady ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M509", + "number": 509, + "source": "src/powersync.ts", + "line": 1186, + "position": 42672, + "operator": "field-drop", + "original": "cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === `rejected`) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === `rejected`\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n }", + "replacement": "", + "anchor": "return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M510", + "number": 510, + "source": "src/powersync.ts", + "line": 1187, + "position": 42711, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "stopped = true", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:bool-flip:stopped = true" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M511", + "number": 511, + "source": "src/powersync.ts", + "line": 1188, + "position": 42728, + "operator": "delete-call", + "original": "clearTimeout(releaseRetryTimer)", + "replacement": ";", + "anchor": "clearTimeout ( releaseRetryTimer )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:clearTimeout ( releaseRetryTimer )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M512", + "number": 512, + "source": "src/powersync.ts", + "line": 1190, + "position": 42814, + "operator": "delete-call", + "original": "database.logger.log({\n level: LogLevels.info,\n message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n })", + "replacement": ";", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M513", + "number": 513, + "source": "src/powersync.ts", + "line": 1191, + "position": 42850, + "operator": "field-drop", + "original": "level: LogLevels.info", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M514", + "number": 514, + "source": "src/powersync.ts", + "line": 1192, + "position": 42887, + "operator": "field-drop", + "original": "message: `Sync has been stopped for ${viewName} into ${trackedTableName}`", + "replacement": "", + "anchor": "database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M515", + "number": 515, + "source": "src/powersync.ts", + "line": 1194, + "position": 42989, + "operator": "delete-call", + "original": "abortController.abort()", + "replacement": ";", + "anchor": "abortController . abort ( )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:abortController . abort ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M516", + "number": 516, + "source": "src/powersync.ts", + "line": 1195, + "position": 43050, + "operator": "value-undefined", + "original": "disposeTrackingAfterAbort()", + "replacement": "undefined", + "anchor": "const trackingDisposal = disposeTrackingAfterAbort ( )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const trackingDisposal = disposeTrackingAfterAbort ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M517", + "number": 517, + "source": "src/powersync.ts", + "line": 1199, + "position": 43265, + "operator": "field-drop", + "original": "tasks: pendingTasks.map((promise, order) => ({ order, promise }))", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M518", + "number": 518, + "source": "src/powersync.ts", + "line": 1199, + "position": 43272, + "operator": "value-undefined", + "original": "pendingTasks.map((promise, order) => ({ order, promise }))", + "replacement": "undefined", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M519", + "number": 519, + "source": "src/powersync.ts", + "line": 1199, + "position": 43312, + "operator": "field-drop", + "original": "order", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M520", + "number": 520, + "source": "src/powersync.ts", + "line": 1199, + "position": 43319, + "operator": "field-drop", + "original": "promise", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M521", + "number": 521, + "source": "src/powersync.ts", + "line": 1200, + "position": 43346, + "operator": "field-drop", + "original": "ownedTasks: new Set(pendingTasks)", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M522", + "number": 522, + "source": "src/powersync.ts", + "line": 1201, + "position": 43395, + "operator": "field-drop", + "original": "failures: []", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~5" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M523", + "number": 523, + "source": "src/powersync.ts", + "line": 1202, + "position": 43423, + "operator": "field-drop", + "original": "nextOrder: pendingTasks.length", + "replacement": "", + "anchor": "const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:const collector : DemandCleanupCollector = { tasks : pendingTasks . map ( ( promise , order ) => ( { order , promise } ) ) , ownedTasks : new Set ( pendingTasks ) , failures : [ ] , nextOrder : pendingTasks . length , }~6" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M524", + "number": 524, + "source": "src/powersync.ts", + "line": 1205, + "position": 43549, + "operator": "value-undefined", + "original": "demands.values()", + "replacement": "undefined", + "anchor": "for ( const demand of demands . values ( ) ) { cleanupDemand ( demand ) }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:for ( const demand of demands . values ( ) ) { cleanupDemand ( demand ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M525", + "number": 525, + "source": "src/powersync.ts", + "line": 1206, + "position": 43583, + "operator": "delete-call", + "original": "cleanupDemand(demand)", + "replacement": ";", + "anchor": "cleanupDemand ( demand )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:cleanupDemand ( demand )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M526", + "number": 526, + "source": "src/powersync.ts", + "line": 1210, + "position": 43678, + "operator": "value-undefined", + "original": "(async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === `rejected`) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === `rejected`\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })", + "replacement": "undefined", + "anchor": "return ( async ( ) => { const ownershipSettlements = await Promise . allSettled ( [ trackingDisposal , ... pendingLoads , ] ) const disposalSettlement = ownershipSettlements [ 0 ] let settledTasks = 0 while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } } throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , ) } ) ( ) . finally ( ( ) => { if ( demandCleanupCollector === collector ) { demandCleanupCollector = null } } )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:return ( async ( ) => { const ownershipSettlements = await Promise . allSettled ( [ trackingDisposal , ... pendingLoads , ] ) const disposalSettlement = ownershipSettlements [ 0 ] let settledTasks = 0 while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } } throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , ) } ) ( ) . finally ( ( ) => { if ( demandCleanupCollector === collector ) { demandCleanupCollector = null } } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M527", + "number": 527, + "source": "src/powersync.ts", + "line": 1211, + "position": 43742, + "operator": "value-undefined", + "original": "Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])", + "replacement": "undefined", + "anchor": "const ownershipSettlements = await Promise . allSettled ( [ trackingDisposal , ... pendingLoads , ] )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const ownershipSettlements = await Promise . allSettled ( [ trackingDisposal , ... pendingLoads , ] )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M528", + "number": 528, + "source": "src/powersync.ts", + "line": 1218, + "position": 43969, + "operator": "loop-false", + "original": "settledTasks < collector.tasks.length", + "replacement": "false", + "anchor": "while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:loop-false:while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M529", + "number": 529, + "source": "src/powersync.ts", + "line": 1218, + "position": 43982, + "operator": "binary-swap", + "original": "<", + "replacement": "<=", + "anchor": "while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:binary-swap:while ( settledTasks < collector . tasks . length ) { const tasks = collector . tasks . slice ( settledTasks ) settledTasks = collector . tasks . length const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , ) for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M530", + "number": 530, + "source": "src/powersync.ts", + "line": 1219, + "position": 44040, + "operator": "value-undefined", + "original": "collector.tasks.slice(settledTasks)", + "replacement": "undefined", + "anchor": "const tasks = collector . tasks . slice ( settledTasks )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const tasks = collector . tasks . slice ( settledTasks )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M531", + "number": 531, + "source": "src/powersync.ts", + "line": 1221, + "position": 44169, + "operator": "value-undefined", + "original": "Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )", + "replacement": "undefined", + "anchor": "const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M532", + "number": 532, + "source": "src/powersync.ts", + "line": 1222, + "position": 44207, + "operator": "value-undefined", + "original": "tasks.map(({ promise }) => promise)", + "replacement": "undefined", + "anchor": "const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:const outcomes = await Promise . allSettled ( tasks . map ( ( { promise } ) => promise ) , )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M533", + "number": 533, + "source": "src/powersync.ts", + "line": 1224, + "position": 44309, + "operator": "value-undefined", + "original": "outcomes.entries()", + "replacement": "undefined", + "anchor": "for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:value-undefined:for ( const [ index , outcome ] of outcomes . entries ( ) ) { if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M534", + "number": 534, + "source": "src/powersync.ts", + "line": 1225, + "position": 44353, + "operator": "if-true", + "original": "outcome.status === `rejected`", + "replacement": "true", + "anchor": "if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:if-true:if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M535", + "number": 535, + "source": "src/powersync.ts", + "line": 1225, + "position": 44353, + "operator": "if-false", + "original": "outcome.status === `rejected`", + "replacement": "false", + "anchor": "if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:if-false:if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M536", + "number": 536, + "source": "src/powersync.ts", + "line": 1225, + "position": 44368, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:binary-swap:if ( outcome . status === `rejected` ) { collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M537", + "number": 537, + "source": "src/powersync.ts", + "line": 1226, + "position": 44406, + "operator": "delete-call", + "original": "collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })", + "replacement": ";", + "anchor": "collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M538", + "number": 538, + "source": "src/powersync.ts", + "line": 1227, + "position": 44454, + "operator": "field-drop", + "original": "order: tasks[index]!.order", + "replacement": "", + "anchor": "collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M539", + "number": 539, + "source": "src/powersync.ts", + "line": 1228, + "position": 44504, + "operator": "field-drop", + "original": "error: outcome.reason", + "replacement": "", + "anchor": "collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:field-drop:collector . failures . push ( { order : tasks [ index ] ! . order , error : outcome . reason , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M540", + "number": 540, + "source": "src/powersync.ts", + "line": 1233, + "position": 44618, + "operator": "delete-call", + "original": "throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === `rejected`\n ? [disposalSettlement.reason]\n : [],\n )", + "replacement": ";", + "anchor": "throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:delete-call:throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M541", + "number": 541, + "source": "src/powersync.ts", + "line": 1235, + "position": 44715, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , )", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:binary-swap:throwDemandCleanupFailures ( collector , disposalSettlement . status === `rejected` ? [ disposalSettlement . reason ] : [ ] , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M542", + "number": 542, + "source": "src/powersync.ts", + "line": 1240, + "position": 44869, + "operator": "if-true", + "original": "demandCleanupCollector === collector", + "replacement": "true", + "anchor": "if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:if-true:if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M543", + "number": 543, + "source": "src/powersync.ts", + "line": 1240, + "position": 44869, + "operator": "if-false", + "original": "demandCleanupCollector === collector", + "replacement": "false", + "anchor": "if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:if-false:if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M544", + "number": 544, + "source": "src/powersync.ts", + "line": 1240, + "position": 44892, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }", + "function": "cleanup", + "key": "src/powersync.ts#cleanup:binary-swap:if ( demandCleanupCollector === collector ) { demandCleanupCollector = null }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M545", + "number": 545, + "source": "src/powersync.ts", + "line": 1245, + "position": 45009, + "operator": "field-drop", + "original": "loadSubset: trackLoadSubset", + "replacement": "", + "anchor": "return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M546", + "number": 546, + "source": "src/powersync.ts", + "line": 1246, + "position": 45048, + "operator": "field-drop", + "original": "unloadSubset", + "replacement": "", + "anchor": "return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }", + "function": "runOnDemandSync", + "key": "src/powersync.ts#runOnDemandSync:field-drop:return { cleanup : ( ) => { stopped = true clearTimeout ( releaseRetryTimer ) releaseRetryTimer = undefined database . logger . log ( { level : LogLevels . info , message : `Sync has been stopped for ${ viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M547", + "number": 547, + "source": "src/powersync.ts", + "line": 1251, + "position": 45134, + "operator": "field-drop", + "original": "getSyncMetadata: undefined", + "replacement": "", + "anchor": "const sync : SyncConfig < OutputType , string > = { sync : ( params ) => { const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const { begin , write , collection , commit , fail , reportError , reportHealthy , } = params let ready = false const markReady = ( ) => { ready = true params . markReady ( ) } const abortController = new AbortController ( ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let disposeTracking : ( ( options ? : { context ? : LockContext } ) => Promise < void > ) | null = null let trackingSetup : Promise < void > | null = null let activeTrackingDisposal : Promise < void > | null = null let abortTrackingDisposal : Promise < void > | null = null if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) } async function safelyDisposeTracking ( context ? : LockContext , ) : Promise < void > { const setup = trackingSetup if ( setup ) { await setup . catch ( ( ) => undefined ) } const dispose = disposeTracking if ( ! dispose ) { await activeTrackingDisposal return } disposeTracking = null let disposal : Promise < void > try { disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) ) } catch ( error ) { disposal = Promise . reject ( error ) } activeTrackingDisposal = disposal try { await disposal } finally { if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null } } } function disposeTrackingAfterAbort ( ) : Promise < void > { const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) ) void disposal . catch ( ( ) => undefined ) return disposal } async function establishTracking ( options : Parameters < typeof createDiffTrigger > [ 0 ] , appliedReceipts : Array < SyncAppliedReceipt > , ) : Promise < void > { const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( ) trackingSetup = setup try { await setup } finally { if ( trackingSetup === setup ) { trackingSetup = null } } } async function createDiffTrigger ( options : { setupContext ? : LockContext snapshot ? : boolean afterRead ? : ( ) => Promise < void > onInitialRow ? : ( value : OutputType ) => void when : Record < DiffTriggerOperation , string > writeType : ( rowId : string ) => OperationType batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => Promise < Array < TableType >> } , appliedReceipts : Array < SyncAppliedReceipt > , ) { const { setupContext , snapshot , afterRead , onInitialRow , when , writeType , batchQuery , } = options return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n ` SELECT * FROM $ { trackedTableName } ORDER BY operation_id ASC `,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(` DELETE FROM $ { trackedTableName } `)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync is starting for $ { viewName } into $ { trackedTableName } `,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n ` abort `,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: ` delete `, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: ` TRUE `,\n [DiffTriggerOperation.UPDATE]: ` TRUE `,\n [DiffTriggerOperation.DELETE]: ` TRUE `,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL` SELECT * FROM $ { viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === ` rejected ` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n ` PowerSync tracking disposal and load - hook cleanup both failed `,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === ` rejected `) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: ` ( $ { newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not clean up subset hook for $ { viewName } `,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, ` PowerSync cleanup failed `, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: ` delete `, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not release subset tracking for $ { viewName } ; retrying `,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const sync : SyncConfig < OutputType , string > = { sync : ( params ) => { const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const { begin , write , collection , commit , fail , reportError , reportHealthy , } = params let ready = false const markReady = ( ) => { ready = true params . markReady ( ) } const abortController = new AbortController ( ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let disposeTracking : ( ( options ? : { context ? : LockContext } ) => Promise < void > ) | null = null let trackingSetup : Promise < void > | null = null let activeTrackingDisposal : Promise < void > | null = null let abortTrackingDisposal : Promise < void > | null = null if ( syncMode === `eager` ) { return runEagerSync ( ) } else { return runOnDemandSync ( ) } async function safelyDisposeTracking ( context ? : LockContext , ) : Promise < void > { const setup = trackingSetup if ( setup ) { await setup . catch ( ( ) => undefined ) } const dispose = disposeTracking if ( ! dispose ) { await activeTrackingDisposal return } disposeTracking = null let disposal : Promise < void > try { disposal = Promise . resolve ( dispose ( context ? { context } : undefined ) ) } catch ( error ) { disposal = Promise . reject ( error ) } activeTrackingDisposal = disposal try { await disposal } finally { if ( activeTrackingDisposal === disposal ) { activeTrackingDisposal = null } } } function disposeTrackingAfterAbort ( ) : Promise < void > { const disposal = ( abortTrackingDisposal ??= safelyDisposeTracking ( ) ) void disposal . catch ( ( ) => undefined ) return disposal } async function establishTracking ( options : Parameters < typeof createDiffTrigger > [ 0 ] , appliedReceipts : Array < SyncAppliedReceipt > , ) : Promise < void > { const setup = ( async ( ) => { const dispose = await createDiffTrigger ( options , appliedReceipts ) disposeTracking = dispose } ) ( ) trackingSetup = setup try { await setup } finally { if ( trackingSetup === setup ) { trackingSetup = null } } } async function createDiffTrigger ( options : { setupContext ? : LockContext snapshot ? : boolean afterRead ? : ( ) => Promise < void > onInitialRow ? : ( value : OutputType ) => void when : Record < DiffTriggerOperation , string > writeType : ( rowId : string ) => OperationType batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => Promise < Array < TableType >> } , appliedReceipts : Array < SyncAppliedReceipt > , ) { const { setupContext , snapshot , afterRead , onInitialRow , when , writeType , batchQuery , } = options return await database . triggers . createDiffTrigger ( { source : viewName , destination : trackedTableName , columns : table . columns . map ( ( column ) => column . name ) , setupContext , when , hooks : { beforeCreate : async ( context ) => { const batches : Array < Array < TableType >> = [ ] const apply = ( ) => { begin ( { snapshot } ) for ( const row of batches . splice ( 0 ) . flat ( ) ) { const type = writeType ( row . id ) const value = deserializeSyncRow ( row ) onInitialRow ?. ( value ) write ( { type , value } ) } const applied = commit ( ) if ( applied !== true ) void applied . catch ( ( ) => undefined ) appliedReceipts . push ( applied ) } let currentBatchCount = syncBatchSize let cursor = 0 while ( currentBatchCount == syncBatchSize ) { const batchItems = await batchQuery ( context , syncBatchSize , cursor , ) currentBatchCount = batchItems . length cursor += currentBatchCount batches . push ( batchItems ) if ( ! snapshot ) apply ( ) } if ( snapshot ) apply ( ) if ( afterRead ) await afterRead ( ) database . logger . log ( { level : LogLevels . info , message : `Sync is ready for ${ viewName } into $ { trackedTableName } `,\n })\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array,\n ): Promise {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll(\n ` SELECT * FROM $ { trackedTableName } ORDER BY operation_id ASC `,\n )\n const pendingOperations: Array = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(` DELETE FROM $ { trackedTableName } `)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.log({\n level: LogLevels.error,\n message: ` An error has been detected in the sync handler `,\n error,\n })\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise) {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync is starting for $ { viewName } into $ { trackedTableName } `,\n })\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await disposeTrackingAfterAbort()\n } else {\n abortController.signal.addEventListener(\n ` abort `,\n () => {\n void disposeTrackingAfterAbort()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n let onUnloadStarted = false\n\n const startup = start(async () => {\n const cleanup = await restConfig.onLoad?.()\n onUnload = cleanup\n if (abortController.signal.aborted) {\n return\n }\n\n const appliedReceipts: Array = []\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n await establishTracking(\n {\n // Retained reloads replace atomically; fresh persisted startup\n // keeps source batches and reconciles only restored omissions.\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n afterRead:\n listed && persistence\n ? async () => {\n await persistence.hydrateBaseline()\n if (abortController.signal.aborted) return\n // Copy omitted keys before committing: a synchronous\n // commit can change the accepted-row iterator's backing map.\n // Source rows remain batched; only omitted keys are retained.\n const gone: Array = []\n for (const [\n key,\n ] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n listed.clear()\n for (\n let first = 0;\n first < gone.length;\n first += syncBatchSize\n ) {\n begin()\n for (const key of gone.slice(\n first,\n first + syncBatchSize,\n ))\n write({ type: ` delete `, key })\n const applied = commit()\n if (applied !== true)\n void applied.catch(() => undefined)\n appliedReceipts.push(applied)\n }\n }\n : undefined,\n when: {\n [DiffTriggerOperation.INSERT]: ` TRUE `,\n [DiffTriggerOperation.UPDATE]: ` TRUE `,\n [DiffTriggerOperation.DELETE]: ` TRUE `,\n },\n onInitialRow: listed\n ? (value) => {\n listed.add(asPowerSyncRecord(value).id)\n }\n : undefined,\n writeType: (_rowId: string) => ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll(\n sanitizeSQL` SELECT * FROM $ { viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n // Readiness counts accepted rows. A mutation handler waits for it,\n // and its own persisting transaction holds the startup rows.\n await Promise.all(appliedReceipts.map(whenSyncAccepted))\n markReady()\n }).catch((error) => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n })\n if (!ready) {\n fail(error)\n // A failed first load ends this sync run, so its change watcher\n // and any diff trigger it set up are released now, not at cleanup.\n abortController.abort()\n void disposeTrackingAfterAbort()\n }\n })\n\n const invokeOnUnload = (): void | Promise => {\n if (onUnloadStarted || !onUnload) return\n onUnloadStarted = true\n const cleanup = onUnload\n onUnload = null\n return cleanup()\n }\n\n return async () => {\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n\n let onUnloadFailure: { error: unknown } | undefined\n const settleOnUnload = async () => {\n try {\n await invokeOnUnload()\n } catch (error) {\n onUnloadFailure ??= { error }\n }\n }\n\n // Invoke an already-installed callback in this stack, but also wait\n // for startup to publish a callback acquired concurrently with abort.\n const settlements = await Promise.allSettled([\n startup,\n trackingDisposal,\n settleOnUnload(),\n ])\n const disposalSettlement = settlements[1]\n await settleOnUnload()\n if (disposalSettlement.status === ` rejected ` && onUnloadFailure) {\n throw new AggregateError(\n [disposalSettlement.reason, onUnloadFailure.error],\n ` PowerSync tracking disposal and load - hook cleanup both failed `,\n { cause: disposalSettlement.reason },\n )\n }\n if (disposalSettlement.status === ` rejected `) {\n throw disposalSettlement.reason\n }\n if (onUnloadFailure) throw onUnloadFailure.error\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n type DemandCleanupTask = {\n order: number\n promise: Promise\n }\n type DemandCleanupFailure = {\n order: number\n error: unknown\n }\n type DemandCleanupCollector = {\n tasks: Array\n ownedTasks: Set>\n failures: Array\n nextOrder: number\n }\n\n const demands = new Map()\n const releasedSubsets = new WeakSet()\n const pendingReleases: Array = []\n const pendingDemandLoads = new Set>()\n const pendingDemandCleanups = new Set>()\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType | undefined\n let demandCleanupCollector: DemandCleanupCollector | null = null\n const startup = start()\n void startup.catch((error) =>\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not start syncing process for $ { viewName } into $ { trackedTableName } `,\n error,\n }),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n // Set while a failed rebuild may have left the source untracked.\n let trackingOutage = false\n const reconcileTracking = async (): Promise => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n // Tracking was absent during an outage. Positive baseline rows\n // alone cannot reveal deletes or predicate exits from that gap.\n const missing = trackingOutage\n ? new Set(collection.keys())\n : undefined\n if (active.length > 0) {\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: ` ( $ { newDataWhenClause } ) OR ( $ { oldDataWhenClause } ) `,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? ` update ` : ` insert `,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext\n .getAll(\n ` SELECT * FROM $ { viewName } WHERE $ { viewWhereClause } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n )\n .then((rows) => {\n for (const row of rows) missing?.delete(row.id)\n return rows\n }),\n },\n appliedReceipts,\n )\n }\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n else if (missing?.size) {\n begin()\n for (const key of missing) write({ type: ` delete `, key })\n appliedReceipts.push(commit())\n }\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n // Replacing the trigger alone does not restore change tracking:\n // its baseline writes must also apply before sync works again.\n if (trackingOutage) {\n trackingOutage = false\n reportHealthy()\n }\n }\n }\n }\n\n const rebuildTracking = async (): Promise => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger,\n // so rows can grow stale. Later demand rebuilds again, so after\n // Collection readiness this is a failureReason, not fail().\n if (stopped) {\n // Cleanup owns the outcome.\n } else if (ready) {\n trackingOutage = true\n reportError(error)\n } else fail(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n const collector = demandCleanupCollector\n if (collector) {\n collector.failures.push({\n order: collector.nextOrder++,\n error,\n })\n }\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n cleanupDemand(demand)\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const trackLoadSubset = (options: LoadSubsetOptions): Promise => {\n const task = loadSubset(options)\n pendingDemandLoads.add(task)\n const finish = () => pendingDemandLoads.delete(task)\n void task.then(finish, finish)\n return task\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const reportDemandCleanupFailure = (error: unknown): void => {\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not clean up subset hook for $ { viewName } `,\n error,\n })\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n const collector = demandCleanupCollector\n const order = collector ? collector.nextOrder++ : undefined\n try {\n const cleanup = demand.cleanup\n demand.cleanup = undefined\n const result: unknown = cleanup?.()\n if (!isPromiseLike(result)) return\n\n const task = Promise.resolve(result)\n pendingDemandCleanups.add(task)\n const finish = () => pendingDemandCleanups.delete(task)\n void task.then(finish, finish)\n if (collector) {\n collector.tasks.push({ order: order!, promise: task })\n collector.ownedTasks.add(task)\n void task.catch(() => undefined)\n } else {\n void task.catch((error) => {\n if (!demandCleanupCollector?.ownedTasks.has(task)) {\n reportDemandCleanupFailure(error)\n }\n })\n }\n } catch (error) {\n if (collector) {\n collector.failures.push({ order: order!, error })\n } else {\n reportDemandCleanupFailure(error)\n }\n }\n }\n\n const throwDemandCleanupFailures = (\n collector: DemandCleanupCollector,\n primaryFailures: ReadonlyArray = [],\n ): void => {\n const errors = [\n ...primaryFailures,\n ...collector.failures\n .slice()\n .sort((left, right) => left.order - right.order)\n .map(({ error }) => error),\n ]\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, ` PowerSync cleanup failed `, {\n cause: errors[0],\n })\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE $ { departingWhereSQL } `\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = ` SELECT id FROM $ { viewName } WHERE ( $ { departingWhereSQL } ) AND NOT ( $ { remainingWhereSQL } ) `\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: ` delete `, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.log({\n level: LogLevels.error,\n message: ` Could not release subset tracking for $ { viewName } ; retrying `,\n error,\n })\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.log({\n level: LogLevels.info,\n message: ` Sync has been stopped for $ { viewName } into $ { trackedTableName } `,\n })\n abortController.abort()\n const trackingDisposal = disposeTrackingAfterAbort()\n const pendingLoads = [...pendingDemandLoads]\n const pendingTasks = [...pendingDemandCleanups]\n const collector: DemandCleanupCollector = {\n tasks: pendingTasks.map((promise, order) => ({ order, promise })),\n ownedTasks: new Set(pendingTasks),\n failures: [],\n nextOrder: pendingTasks.length,\n }\n demandCleanupCollector = collector\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n\n return (async () => {\n const ownershipSettlements = await Promise.allSettled([\n trackingDisposal,\n ...pendingLoads,\n ])\n const disposalSettlement = ownershipSettlements[0]\n\n let settledTasks = 0\n while (settledTasks < collector.tasks.length) {\n const tasks = collector.tasks.slice(settledTasks)\n settledTasks = collector.tasks.length\n const outcomes = await Promise.allSettled(\n tasks.map(({ promise }) => promise),\n )\n for (const [index, outcome] of outcomes.entries()) {\n if (outcome.status === ` rejected `) {\n collector.failures.push({\n order: tasks[index]!.order,\n error: outcome.reason,\n })\n }\n }\n }\n throwDemandCleanupFailures(\n collector,\n disposalSettlement.status === ` rejected `\n ? [disposalSettlement.reason]\n : [],\n )\n })().finally(() => {\n if (demandCleanupCollector === collector) {\n demandCleanupCollector = null\n }\n })\n },\n loadSubset: trackLoadSubset,\n unloadSubset,\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M548", + "number": 548, + "source": "src/powersync.ts", + "line": 1265, + "position": 45545, + "operator": "field-drop", + "original": "deserializationSchema", + "replacement": "", + "anchor": "const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M549", + "number": 549, + "source": "src/powersync.ts", + "line": 1266, + "position": 45572, + "operator": "field-drop", + "original": "serializer", + "replacement": "", + "anchor": "const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M550", + "number": 550, + "source": "src/powersync.ts", + "line": 1267, + "position": 45588, + "operator": "field-drop", + "original": "onDeserializationError", + "replacement": "", + "anchor": "const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const { onLoad : _onLoad , onLoadSubset : _onLoadSubset , deserializationSchema : _deserializationSchema , serializer : _serializer , onDeserializationError : _onDeserializationError , ... collectionConfig } = { deserializationSchema , serializer , onDeserializationError , ... restConfig , }~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M551", + "number": 551, + "source": "src/powersync.ts", + "line": 1277, + "position": 45770, + "operator": "field-drop", + "original": "schema", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M552", + "number": 552, + "source": "src/powersync.ts", + "line": 1278, + "position": 45782, + "operator": "field-drop", + "original": "getKey", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M553", + "number": 553, + "source": "src/powersync.ts", + "line": 1280, + "position": 45885, + "operator": "field-drop", + "original": "startSync: true", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M554", + "number": 554, + "source": "src/powersync.ts", + "line": 1280, + "position": 45896, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:bool-flip:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M555", + "number": 555, + "source": "src/powersync.ts", + "line": 1281, + "position": 45906, + "operator": "field-drop", + "original": "syncMode", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M556", + "number": 556, + "source": "src/powersync.ts", + "line": 1282, + "position": 45920, + "operator": "field-drop", + "original": "sync", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~5" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M557", + "number": 557, + "source": "src/powersync.ts", + "line": 1283, + "position": 45930, + "operator": "field-drop", + "original": "onInsert: async (params) => {\n // The transaction here should only ever contain a single insert mutation\n return await transactor.applyTransaction(params.transaction)\n }", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "onInsert", + "key": "src/powersync.ts#onInsert:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M558", + "number": 558, + "source": "src/powersync.ts", + "line": 1285, + "position": 46059, + "operator": "value-undefined", + "original": "transactor.applyTransaction(params.transaction)", + "replacement": "undefined", + "anchor": "return await transactor . applyTransaction ( params . transaction )", + "function": "onInsert", + "key": "src/powersync.ts#onInsert:value-undefined:return await transactor . applyTransaction ( params . transaction )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M559", + "number": 559, + "source": "src/powersync.ts", + "line": 1287, + "position": 46118, + "operator": "field-drop", + "original": "onUpdate: async (params) => {\n // The transaction here should only ever contain a single update mutation\n return await transactor.applyTransaction(params.transaction)\n }", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "onUpdate", + "key": "src/powersync.ts#onUpdate:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M560", + "number": 560, + "source": "src/powersync.ts", + "line": 1289, + "position": 46247, + "operator": "value-undefined", + "original": "transactor.applyTransaction(params.transaction)", + "replacement": "undefined", + "anchor": "return await transactor . applyTransaction ( params . transaction )", + "function": "onUpdate", + "key": "src/powersync.ts#onUpdate:value-undefined:return await transactor . applyTransaction ( params . transaction )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M561", + "number": 561, + "source": "src/powersync.ts", + "line": 1291, + "position": 46306, + "operator": "field-drop", + "original": "onDelete: async (params) => {\n // The transaction here should only ever contain a single delete mutation\n return await transactor.applyTransaction(params.transaction)\n }", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "onDelete", + "key": "src/powersync.ts#onDelete:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M562", + "number": 562, + "source": "src/powersync.ts", + "line": 1293, + "position": 46435, + "operator": "value-undefined", + "original": "transactor.applyTransaction(params.transaction)", + "replacement": "undefined", + "anchor": "return await transactor . applyTransaction ( params . transaction )", + "function": "onDelete", + "key": "src/powersync.ts#onDelete:value-undefined:return await transactor . applyTransaction ( params . transaction )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M563", + "number": 563, + "source": "src/powersync.ts", + "line": 1295, + "position": 46494, + "operator": "field-drop", + "original": "utils: {\n getMeta: () => ({\n tableName: viewName,\n trackedTableName,\n metadataIsTracked,\n serializeValue: (value) =>\n serializeForSQLite(value, table, serializer),\n }),\n }", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "createPowerSyncCollectionConfig", + "key": "src/powersync.ts#createPowerSyncCollectionConfig:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~6" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M564", + "number": 564, + "source": "src/powersync.ts", + "line": 1296, + "position": 46509, + "operator": "field-drop", + "original": "getMeta: () => ({\n tableName: viewName,\n trackedTableName,\n metadataIsTracked,\n serializeValue: (value) =>\n serializeForSQLite(value, table, serializer),\n })", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "getMeta", + "key": "src/powersync.ts#getMeta:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M565", + "number": 565, + "source": "src/powersync.ts", + "line": 1297, + "position": 46535, + "operator": "field-drop", + "original": "tableName: viewName", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "getMeta", + "key": "src/powersync.ts#getMeta:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M566", + "number": 566, + "source": "src/powersync.ts", + "line": 1298, + "position": 46564, + "operator": "field-drop", + "original": "trackedTableName", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "getMeta", + "key": "src/powersync.ts#getMeta:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M567", + "number": 567, + "source": "src/powersync.ts", + "line": 1299, + "position": 46590, + "operator": "field-drop", + "original": "metadataIsTracked", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "getMeta", + "key": "src/powersync.ts#getMeta:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M568", + "number": 568, + "source": "src/powersync.ts", + "line": 1300, + "position": 46617, + "operator": "field-drop", + "original": "serializeValue: (value) =>\n serializeForSQLite(value, table, serializer)", + "replacement": "", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "serializeValue", + "key": "src/powersync.ts#serializeValue:field-drop:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "powersync.ts:M569", + "number": 569, + "source": "src/powersync.ts", + "line": 1301, + "position": 46654, + "operator": "value-undefined", + "original": "serializeForSQLite(value, table, serializer)", + "replacement": "undefined", + "anchor": "const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }", + "function": "serializeValue", + "key": "src/powersync.ts#serializeValue:value-undefined:const outputConfig : EnhancedPowerSyncCollectionConfig < TTable , OutputType , TSchema > = { ... collectionConfig , schema , getKey , startSync : true , syncMode , sync , onInsert : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onUpdate : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , onDelete : async ( params ) => { return await transactor . applyTransaction ( params . transaction ) } , utils : { getMeta : ( ) => ( { tableName : viewName , trackedTableName , metadataIsTracked , serializeValue : ( value ) => serializeForSQLite < TTable > ( value , table , serializer ) , } ) , } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + } + ], + "pilot": { + "run": { + "id": "90aa5a0b5c54", + "inputs": "1 source files, 2 oracle files, 0 ordinary test files, 4 authored mutants, seed 2367, pool forks; a partial run (--only or --sample)" + }, + "options": { + "pkg": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection", + "envPrefix": "TANSTACK_DB_ORACLE", + "daemon": false, + "ordinary": false, + "contexts": 1, + "sources": [ + "src/powersync.ts" + ], + "seed": "2367", + "out": "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/pilot-powersync", + "only": [ + "authored:forced-snapshot-startup", + "authored:skip-restored-omissions", + "authored:provider-id-used-as-collection-key", + "authored:unbounded-omission-batch" + ], + "testTimeout": 15000, + "hangLimit": 10000, + "firstLimit": 2000, + "pool": "forks", + "mutants": "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/controls-powersync" + }, + "excluded": [], + "nondeterministic": [], + "mutantSeconds": 79.553, + "evidence": { + "reused": 0, + "computed": 12, + "cut": 0, + "idle": 0, + "spin": 0, + "rerun": 0, + "unchanged": 0 + }, + "results": [ + { + "id": "authored:forced-snapshot-startup", + "authored": true, + "authoredId": "forced-snapshot-startup", + "source": "src/powersync.ts", + "operator": "authored", + "function": "sync", + "intent": "Restore unconditional snapshot replacement for persisted first startup; accepted public/durable result may be right while the independent startup-work law rejects it.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "original": "const replacesRetainedRows =\n collection.health.reloading &&\n !collection._state.acceptedSyncedEntries().next().done", + "replacement": "const replacesRetainedRows = persistence !== null", + "line": 357, + "outcome": "killed", + "by": [ + "persisted startup conformance: PowerSync > adapter.powersync.persisted-startup through controlled > adapter.powersync.persisted-startup > fixed seed 2367" + ], + "laws": [ + "startup batching" + ], + "randomAlso": false, + "seconds": 48.057466708, + "key": "src/powersync.ts#authored:forced-snapshot-startup" + }, + { + "id": "authored:skip-restored-omissions", + "authored": true, + "authoredId": "skip-restored-omissions", + "source": "src/powersync.ts", + "operator": "authored", + "function": "runEagerSync", + "intent": "Keep the positive source result but omit the restored-row reconciliation, reproducing the obsolete persisted-row class under ordinary batched startup.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "original": "if (!listed.has(key)) gone.push(key)", + "replacement": "if (false) gone.push(key)", + "line": 674, + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.powersync.controlled > fixed seed 2367", + "persisted startup conformance: PowerSync > adapter.powersync.persisted-startup through controlled > adapter.powersync.persisted-startup > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle", + "persisted startup" + ], + "randomAlso": false, + "seconds": 2.360280834, + "key": "src/powersync.ts#authored:skip-restored-omissions" + }, + { + "id": "authored:provider-id-used-as-collection-key", + "authored": true, + "authoredId": "provider-id-used-as-collection-key", + "source": "src/powersync.ts", + "operator": "authored", + "function": "apply", + "intent": "Track provider raw IDs while writing deserialized output keys. The prefixed-ID receiving axis must reject deletion of legitimate source rows.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "original": "onInitialRow?.(value)", + "replacement": "onInitialRow?.(row as unknown as OutputType)", + "line": 484, + "outcome": "killed", + "by": [ + "persisted startup conformance: PowerSync > adapter.powersync.persisted-startup through controlled > adapter.powersync.persisted-startup > fixed seed 2367" + ], + "laws": [ + "persisted startup" + ], + "randomAlso": false, + "seconds": 12.883021166, + "key": "src/powersync.ts#authored:provider-id-used-as-collection-key" + }, + { + "id": "authored:unbounded-omission-batch", + "authored": true, + "authoredId": "unbounded-omission-batch", + "source": "src/powersync.ts", + "operator": "authored", + "function": "runEagerSync", + "intent": "The first omission commit consumes every remaining key rather than the configured batch bound; three-omission histories distinguish it.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "original": "gone.slice(\n first,\n first + syncBatchSize,\n )", + "replacement": "gone.slice(first)", + "line": 683, + "outcome": "killed", + "by": [ + "persisted startup conformance: PowerSync > adapter.powersync.persisted-startup through controlled > adapter.powersync.persisted-startup > fixed seed 2367" + ], + "laws": [ + "startup batching" + ], + "randomAlso": false, + "seconds": 14.276600333, + "key": "src/powersync.ts#authored:unbounded-omission-batch" + } + ] + }, + "authoredControls": [ + { + "id": "forced-snapshot-startup", + "source": "src/powersync.ts", + "find": "const replacesRetainedRows =\n collection.health.reloading &&\n !collection._state.acceptedSyncedEntries().next().done", + "replace": "const replacesRetainedRows = persistence !== null", + "intent": "Restore unconditional snapshot replacement for persisted first startup; accepted public/durable result may be right while the independent startup-work law rejects it.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "sourceSha256": "c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068", + "position": 11893, + "line": 357, + "transformedSha256": "b5162fdfdf9898cdc92011fdf7fdfb00206cc466827d7ea1f22f29dedb5dca9e" + }, + { + "id": "skip-restored-omissions", + "source": "src/powersync.ts", + "find": "if (!listed.has(key)) gone.push(key)", + "replace": "if (false) gone.push(key)", + "intent": "Keep the positive source result but omit the restored-row reconciliation, reproducing the obsolete persisted-row class under ordinary batched startup.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "sourceSha256": "c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068", + "position": 23145, + "line": 674, + "transformedSha256": "ba63a4db57a9ac2272b6170ce245f63d68acb37ec2678c07823ba8920d9930a6" + }, + { + "id": "provider-id-used-as-collection-key", + "source": "src/powersync.ts", + "find": "onInitialRow?.(value)", + "replace": "onInitialRow?.(row as unknown as OutputType)", + "intent": "Track provider raw IDs while writing deserialized output keys. The prefixed-ID receiving axis must reject deletion of legitimate source rows.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "sourceSha256": "c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068", + "position": 16292, + "line": 484, + "transformedSha256": "6899c5e16d277b3716e9750b42d51aa7fd4d4379891a820e87c9bd0e4c2ee885" + }, + { + "id": "unbounded-omission-batch", + "source": "src/powersync.ts", + "find": "gone.slice(\n first,\n first + syncBatchSize,\n )", + "replace": "gone.slice(first)", + "intent": "The first omission commit consumes every remaining key rather than the configured batch bound; three-omission histories distinguish it.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "sourceSha256": "c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068", + "position": 23502, + "line": 683, + "transformedSha256": "3f11c205c802dd355e69b2f41e4c3709fa376d29cabd78c92f58c02ea84b9629" + } + ], + "automaticCommand": [ + "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node", + "/private/tmp/tanstack-db-2394-gap-cli-e1fb-titles/oracle-gaps/index.mjs", + "--package", + "packages/powersync-db-collection", + "--source", + "src/powersync.ts", + "--seed", + "2367", + "--contexts", + "1", + "--pool", + "forks", + "--env-prefix", + "TANSTACK_DB_ORACLE", + "--only", + "powersync.ts:M40,powersync.ts:M41,powersync.ts:M79,powersync.ts:M80,powersync.ts:M81,powersync.ts:M82,powersync.ts:M83,powersync.ts:M84,powersync.ts:M85,powersync.ts:M86,powersync.ts:M87,powersync.ts:M88,powersync.ts:M89,powersync.ts:M90,powersync.ts:M91,powersync.ts:M102,powersync.ts:M103,powersync.ts:M104,powersync.ts:M172,powersync.ts:M173,powersync.ts:M175,powersync.ts:M176,powersync.ts:M177,powersync.ts:M178,powersync.ts:M179,powersync.ts:M180,powersync.ts:M181,powersync.ts:M182,powersync.ts:M183,powersync.ts:M184,powersync.ts:M185,powersync.ts:M186,powersync.ts:M187,powersync.ts:M188,powersync.ts:M189,powersync.ts:M190,powersync.ts:M191,powersync.ts:M192,powersync.ts:M193,powersync.ts:M194,powersync.ts:M195,powersync.ts:M196,powersync.ts:M197,powersync.ts:M198,powersync.ts:M199,powersync.ts:M200,powersync.ts:M201,powersync.ts:M202,powersync.ts:M203,powersync.ts:M208,powersync.ts:M209", + "--out", + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-powersync" + ], + "baselineCapture": { + "first": { + "unitCount": 2, + "testCount": 12, + "uniqueKeys": 12, + "allPassed": true, + "rowCount": 14, + "setupErrors": [], + "unhandled": [], + "fileSeconds": [ + { + "file": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "seconds": 15.649, + "duration": 15.533314209 + }, + { + "file": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "seconds": 15.136, + "duration": 15.027019083 + } + ] + }, + "second": { + "unitCount": 2, + "testCount": 12, + "uniqueKeys": 12, + "allPassed": true, + "rowCount": 14, + "setupErrors": [], + "unhandled": [], + "fileSeconds": [ + { + "file": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "seconds": 14.458, + "duration": 14.357102875 + }, + { + "file": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "seconds": 14.234, + "duration": 14.142228916 + } + ] + } + }, + "artifacts": { + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-powersync-command.json": "51efe91f9f819a3eb467fc2d19669686eb53ed7d9750a74a0f2ea34b9de8950e", + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-powersync.log": "2314efb4cbff30439ef37c4bdfc449aa4d89734d1b9b9098372bf58a96e0b37a", + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-powersync/results.jsonl": "4b47da587db053dfee49dae65a78cf107c9225bff9c04e52fb8ecafdd937b7bd", + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-powersync/baseline-captures.json": "649ab114f4bb3b783cc5f66c0544020f71d848e08e76dfcdce0133d222060ace", + "/private/tmp/tanstack-db-2394-final-adapter-delta/powersync-selection.json": "05e218c91dd49431dc1d4ef661a34b84ab6f4895274d63b262b0301218f34681" + } + }, + "rxdb": { + "sourceSha256": "c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7", + "fullStaticInventoryCount": 177, + "selectedCount": 59, + "completedCount": 48, + "counts": { + "timeout": 9, + "survived": 16, + "killed": 22, + "unreached": 1 + }, + "unrunOrInterruptedCount": 11, + "selectionBasis": "Every automatic operator overlapping added/replaced executable lines in the frozen batching repair; unrelated unchanged loader/lifecycle source operations are inventoried and omitted from this additive selection. Parent-approved bounded contract-driven scope, not a universal gap-free claim.", + "addedRanges": [ + { + "lineStart": 138, + "lineEnd": 140 + }, + { + "lineStart": 150, + "lineEnd": 153 + }, + { + "lineStart": 156, + "lineEnd": 158 + }, + { + "lineStart": 201, + "lineEnd": 201 + }, + { + "lineStart": 211, + "lineEnd": 211 + }, + { + "lineStart": 218, + "lineEnd": 240 + }, + { + "lineStart": 257, + "lineEnd": 257 + }, + { + "lineStart": 302, + "lineEnd": 302 + }, + { + "lineStart": 304, + "lineEnd": 304 + }, + { + "lineStart": 318, + "lineEnd": 318 + } + ], + "fullStaticInventory": [ + { + "id": "rxdb.ts:M1", + "number": 1, + "source": "src/rxdb.ts", + "line": 30, + "position": 651, + "operator": "value-undefined", + "original": "DebugModule.debug(`ts/db:rxdb`)", + "replacement": "undefined", + "anchor": "const debug = DebugModule . debug ( `ts/db:rxdb` )", + "function": "", + "key": "src/rxdb.ts#:value-undefined:const debug = DebugModule . debug ( `ts/db:rxdb` )" + }, + { + "id": "rxdb.ts:M2", + "number": 2, + "source": "src/rxdb.ts", + "line": 131, + "position": 4226, + "operator": "field-drop", + "original": "sync: (params: SyncParams) => {\n const { begin, write, commit, markReady, fail, collection } = params\n const persistence =\n params.metadata === undefined\n ? null\n : validateSyncPersistenceCapability(params.metadata.persistence)\n\n const replacesRetainedRows =\n collection.health.reloading &&\n !collection._state.acceptedSyncedEntries().next().done\n let initialFetchComplete = false\n async function initialFetch() {\n /**\n * RxDB stores a last-write-time\n * which can be used to \"sort\" document writes,\n * so for initial sync we iterate over that.\n */\n let cursor: RxDocumentData | undefined = undefined\n const syncBatchSize = config.syncBatchSize ? config.syncBatchSize : 1000\n // Retained reloads replace atomically. First persisted startup keeps\n // its one ordinary source commit and deletes only restored omissions.\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n begin({\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n })\n\n while (!initialFetchComplete) {\n let query: FilledMangoQuery\n if (cursor) {\n query = {\n selector: {\n $or: [\n { '_meta.lwt': { $gt: cursor._meta.lwt } },\n {\n '_meta.lwt': cursor._meta.lwt,\n [primaryPath]: {\n $gt: getKey(cursor),\n },\n },\n ],\n _deleted: false,\n },\n sort: [{ '_meta.lwt': `asc` }, { [primaryPath]: `asc` }],\n limit: syncBatchSize,\n skip: 0,\n }\n } else {\n query = {\n selector: { _deleted: false },\n sort: [{ '_meta.lwt': `asc` }, { [primaryPath]: `asc` }],\n limit: syncBatchSize,\n skip: 0,\n }\n }\n\n /**\n * Instead of doing a RxCollection.query(),\n * we directly query the storage engine of the RxCollection so we do not use the\n * RxCollection document cache because it likely wont be used anyway\n * since most queries will run directly on the tanstack-db side.\n */\n const preparedQuery = prepareQuery(\n rxCollection.storageInstance.schema,\n query,\n )\n const result = await rxCollection.storageInstance.query(preparedQuery)\n if (isStopped()) return\n const docs = result.documents\n\n cursor = lastOfArray(docs)\n if (docs.length === 0) {\n initialFetchComplete = true\n break\n }\n\n docs.forEach((d) => {\n listed?.add(getKey(d))\n write({\n type: `insert`,\n value: stripRxdbFields(clone(d)),\n })\n })\n }\n const firstApplied = commit()\n if (firstApplied !== true) void firstApplied.catch(() => undefined)\n const applied: Array> = [firstApplied]\n if (listed && persistence) {\n await persistence.hydrateBaseline()\n if (isStopped()) return\n // Copy omitted keys before committing: synchronous application can\n // change the accepted-row iterator's backing map. Buffered RxDB live\n // changes still apply after this initial result.\n const gone: Array = []\n for (const [key] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n for (let first = 0; first < gone.length; first += syncBatchSize) {\n begin()\n for (const key of gone.slice(first, first + syncBatchSize))\n write({ type: `delete`, key })\n const receipt = commit()\n if (receipt !== true) void receipt.catch(() => undefined)\n applied.push(receipt)\n }\n }\n await Promise.all(applied)\n }\n\n type WriteMessage = Parameters[0]\n const buffer: Array = []\n let buffering = true\n const queue = (msg: WriteMessage) => {\n if (buffering) {\n buffer.push(msg)\n return\n }\n begin()\n write(msg)\n commit()\n }\n\n let sub: Subscription | undefined\n const isStopped = () => sub?.closed !== false\n function stopOngoingFetch() {\n buffer.length = 0\n if (!sub) return\n getFromMapOrCreate(\n OPEN_RXDB_SUBSCRIPTIONS,\n rxCollection,\n () => new Set(),\n ).delete(sub)\n sub.unsubscribe()\n sub = undefined\n }\n\n function startOngoingFetch() {\n // Subscribe early and buffer live changes during initial load and ongoing\n sub = rxCollection.$.subscribe({\n next: (ev) => {\n const cur: Row = stripRxdbFields(clone(ev.documentData as Row))\n switch (ev.operation) {\n // The initial read can already hold a document whose insert\n // event was buffered, so an insert writes as an update. Core\n // inserts an updated key it does not hold.\n case `INSERT`:\n case `UPDATE`:\n queue({ type: `update`, value: cur })\n break\n case `DELETE`:\n queue({ type: `delete`, value: cur })\n break\n }\n },\n // An errored stream ends, so later writes would never arrive.\n error: stop,\n })\n\n const subs = getFromMapOrCreate(\n OPEN_RXDB_SUBSCRIPTIONS,\n rxCollection,\n () => new Set(),\n )\n subs.add(sub)\n }\n\n async function start() {\n startOngoingFetch()\n if (isStopped()) return\n await initialFetch()\n if (isStopped()) {\n return\n }\n\n // Take one finite snapshot of changes observed during the initial\n // fetch, then route newer events through the normal live path. The\n // core transaction queue preserves their order without letting a\n // continuous event stream postpone readiness forever.\n const pending = buffer.splice(0)\n buffering = false\n if (pending.length > 0) {\n begin()\n for (const msg of pending) write(msg)\n await commit()\n if (isStopped()) {\n return\n }\n }\n\n // After this run's fail(), markReady() does nothing.\n markReady()\n }\n\n // Core ignores a later fail() in the same run.\n function stop(error: unknown) {\n stopOngoingFetch()\n fail(error)\n }\n void start().catch(stop)\n\n return stopOngoingFetch\n }", + "replacement": "", + "anchor": "const sync : SyncConfig < Row , Key > = { sync : ( params : SyncParams ) => { const { begin , write , commit , markReady , fail , collection } = params const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let initialFetchComplete = false async function initialFetch ( ) { let cursor : RxDocumentData < Row > | undefined = undefined const syncBatchSize = config . syncBatchSize ? config . syncBatchSize : 1000 const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } ) while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) } const firstApplied = commit ( ) if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined ) const applied : Array < true | Promise < void >> = [ firstApplied ] if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } } await Promise . all ( applied ) } type WriteMessage = Parameters < typeof write > [ 0 ] const buffer : Array < WriteMessage > = [ ] let buffering = true const queue = ( msg : WriteMessage ) => { if ( buffering ) { buffer . push ( msg ) return } begin ( ) write ( msg ) commit ( ) } let sub : Subscription | undefined const isStopped = ( ) => sub ?. closed !== false function stopOngoingFetch ( ) { buffer . length = 0 if ( ! sub ) return getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub ) sub . unsubscribe ( ) sub = undefined } function startOngoingFetch ( ) { sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } ) const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) subs . add ( sub ) } async function start ( ) { startOngoingFetch ( ) if ( isStopped ( ) ) return await initialFetch ( ) if ( isStopped ( ) ) { return } const pending = buffer . splice ( 0 ) buffering = false if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } } markReady ( ) } function stop ( error : unknown ) { stopOngoingFetch ( ) fail ( error ) } void start ( ) . catch ( stop ) return stopOngoingFetch } , rowUpdateMode : `full` , }", + "function": "sync", + "key": "src/rxdb.ts#sync:field-drop:const sync : SyncConfig < Row , Key > = { sync : ( params : SyncParams ) => { const { begin , write , commit , markReady , fail , collection } = params const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let initialFetchComplete = false async function initialFetch ( ) { let cursor : RxDocumentData < Row > | undefined = undefined const syncBatchSize = config . syncBatchSize ? config . syncBatchSize : 1000 const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } ) while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) } const firstApplied = commit ( ) if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined ) const applied : Array < true | Promise < void >> = [ firstApplied ] if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } } await Promise . all ( applied ) } type WriteMessage = Parameters < typeof write > [ 0 ] const buffer : Array < WriteMessage > = [ ] let buffering = true const queue = ( msg : WriteMessage ) => { if ( buffering ) { buffer . push ( msg ) return } begin ( ) write ( msg ) commit ( ) } let sub : Subscription | undefined const isStopped = ( ) => sub ?. closed !== false function stopOngoingFetch ( ) { buffer . length = 0 if ( ! sub ) return getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub ) sub . unsubscribe ( ) sub = undefined } function startOngoingFetch ( ) { sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } ) const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) subs . add ( sub ) } async function start ( ) { startOngoingFetch ( ) if ( isStopped ( ) ) return await initialFetch ( ) if ( isStopped ( ) ) { return } const pending = buffer . splice ( 0 ) buffering = false if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } } markReady ( ) } function stop ( error : unknown ) { stopOngoingFetch ( ) fail ( error ) } void start ( ) . catch ( stop ) return stopOngoingFetch } , rowUpdateMode : `full` , }" + }, + { + "id": "rxdb.ts:M3", + "number": 3, + "source": "src/rxdb.ts", + "line": 134, + "position": 4383, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "function": "sync", + "key": "src/rxdb.ts#sync:binary-swap:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )" + }, + { + "id": "rxdb.ts:M4", + "number": 4, + "source": "src/rxdb.ts", + "line": 136, + "position": 4426, + "operator": "value-undefined", + "original": "validateSyncPersistenceCapability(params.metadata.persistence)", + "replacement": "undefined", + "anchor": "const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "function": "sync", + "key": "src/rxdb.ts#sync:value-undefined:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )" + }, + { + "id": "rxdb.ts:M5", + "number": 5, + "source": "src/rxdb.ts", + "line": 139, + "position": 4561, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "key": "src/rxdb.ts#sync:binary-swap:const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done" + }, + { + "id": "rxdb.ts:M6", + "number": 6, + "source": "src/rxdb.ts", + "line": 140, + "position": 4572, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "key": "src/rxdb.ts#sync:drop-not:const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done" + }, + { + "id": "rxdb.ts:M7", + "number": 7, + "source": "src/rxdb.ts", + "line": 141, + "position": 4660, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let initialFetchComplete = false", + "function": "sync", + "key": "src/rxdb.ts#sync:bool-flip:let initialFetchComplete = false" + }, + { + "id": "rxdb.ts:M8", + "number": 8, + "source": "src/rxdb.ts", + "line": 153, + "position": 5225, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined" + }, + { + "id": "rxdb.ts:M9", + "number": 9, + "source": "src/rxdb.ts", + "line": 153, + "position": 5228, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:drop-not:const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined" + }, + { + "id": "rxdb.ts:M10", + "number": 10, + "source": "src/rxdb.ts", + "line": 154, + "position": 5290, + "operator": "delete-call", + "original": "begin({\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n })", + "replacement": ";", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )" + }, + { + "id": "rxdb.ts:M11", + "number": 11, + "source": "src/rxdb.ts", + "line": 155, + "position": 5308, + "operator": "field-drop", + "original": "snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows", + "replacement": "", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )" + }, + { + "id": "rxdb.ts:M12", + "number": 12, + "source": "src/rxdb.ts", + "line": 156, + "position": 5342, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )" + }, + { + "id": "rxdb.ts:M13", + "number": 13, + "source": "src/rxdb.ts", + "line": 157, + "position": 5367, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:drop-not:begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )" + }, + { + "id": "rxdb.ts:M14", + "number": 14, + "source": "src/rxdb.ts", + "line": 161, + "position": 5487, + "operator": "loop-false", + "original": "!initialFetchComplete", + "replacement": "false", + "anchor": "while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:loop-false:while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) }" + }, + { + "id": "rxdb.ts:M15", + "number": 15, + "source": "src/rxdb.ts", + "line": 161, + "position": 5487, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:drop-not:while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) }" + }, + { + "id": "rxdb.ts:M16", + "number": 16, + "source": "src/rxdb.ts", + "line": 163, + "position": 5569, + "operator": "if-true", + "original": "cursor", + "replacement": "true", + "anchor": "if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } }" + }, + { + "id": "rxdb.ts:M17", + "number": 17, + "source": "src/rxdb.ts", + "line": 163, + "position": 5569, + "operator": "if-false", + "original": "cursor", + "replacement": "false", + "anchor": "if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } }" + }, + { + "id": "rxdb.ts:M18", + "number": 18, + "source": "src/rxdb.ts", + "line": 165, + "position": 5615, + "operator": "field-drop", + "original": "selector: {\n $or: [\n { '_meta.lwt': { $gt: cursor._meta.lwt } },\n {\n '_meta.lwt': cursor._meta.lwt,\n [primaryPath]: {\n $gt: getKey(cursor),\n },\n },\n ],\n _deleted: false,\n }", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + { + "id": "rxdb.ts:M19", + "number": 19, + "source": "src/rxdb.ts", + "line": 166, + "position": 5643, + "operator": "field-drop", + "original": "$or: [\n { '_meta.lwt': { $gt: cursor._meta.lwt } },\n {\n '_meta.lwt': cursor._meta.lwt,\n [primaryPath]: {\n $gt: getKey(cursor),\n },\n },\n ]", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~2" + }, + { + "id": "rxdb.ts:M20", + "number": 20, + "source": "src/rxdb.ts", + "line": 167, + "position": 5670, + "operator": "field-drop", + "original": "'_meta.lwt': { $gt: cursor._meta.lwt }", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~3" + }, + { + "id": "rxdb.ts:M21", + "number": 21, + "source": "src/rxdb.ts", + "line": 167, + "position": 5685, + "operator": "field-drop", + "original": "$gt: cursor._meta.lwt", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~4" + }, + { + "id": "rxdb.ts:M22", + "number": 22, + "source": "src/rxdb.ts", + "line": 169, + "position": 5752, + "operator": "field-drop", + "original": "'_meta.lwt': cursor._meta.lwt", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~5" + }, + { + "id": "rxdb.ts:M23", + "number": 23, + "source": "src/rxdb.ts", + "line": 170, + "position": 5803, + "operator": "field-drop", + "original": "[primaryPath]: {\n $gt: getKey(cursor),\n }", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~6" + }, + { + "id": "rxdb.ts:M24", + "number": 24, + "source": "src/rxdb.ts", + "line": 171, + "position": 5842, + "operator": "field-drop", + "original": "$gt: getKey(cursor)", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~7" + }, + { + "id": "rxdb.ts:M25", + "number": 25, + "source": "src/rxdb.ts", + "line": 171, + "position": 5847, + "operator": "value-undefined", + "original": "getKey(cursor)", + "replacement": "undefined", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + { + "id": "rxdb.ts:M26", + "number": 26, + "source": "src/rxdb.ts", + "line": 175, + "position": 5942, + "operator": "field-drop", + "original": "_deleted: false", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~8" + }, + { + "id": "rxdb.ts:M27", + "number": 27, + "source": "src/rxdb.ts", + "line": 175, + "position": 5952, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + { + "id": "rxdb.ts:M28", + "number": 28, + "source": "src/rxdb.ts", + "line": 177, + "position": 5990, + "operator": "field-drop", + "original": "sort: [{ '_meta.lwt': `asc` }, { [primaryPath]: `asc` }]", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~9" + }, + { + "id": "rxdb.ts:M29", + "number": 29, + "source": "src/rxdb.ts", + "line": 177, + "position": 5999, + "operator": "field-drop", + "original": "'_meta.lwt': `asc`", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~10" + }, + { + "id": "rxdb.ts:M30", + "number": 30, + "source": "src/rxdb.ts", + "line": 177, + "position": 6023, + "operator": "field-drop", + "original": "[primaryPath]: `asc`", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~11" + }, + { + "id": "rxdb.ts:M31", + "number": 31, + "source": "src/rxdb.ts", + "line": 178, + "position": 6062, + "operator": "field-drop", + "original": "limit: syncBatchSize", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~12" + }, + { + "id": "rxdb.ts:M32", + "number": 32, + "source": "src/rxdb.ts", + "line": 179, + "position": 6098, + "operator": "field-drop", + "original": "skip: 0", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~13" + }, + { + "id": "rxdb.ts:M33", + "number": 33, + "source": "src/rxdb.ts", + "line": 183, + "position": 6176, + "operator": "field-drop", + "original": "selector: { _deleted: false }", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + { + "id": "rxdb.ts:M34", + "number": 34, + "source": "src/rxdb.ts", + "line": 183, + "position": 6188, + "operator": "field-drop", + "original": "_deleted: false", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~2" + }, + { + "id": "rxdb.ts:M35", + "number": 35, + "source": "src/rxdb.ts", + "line": 183, + "position": 6198, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + { + "id": "rxdb.ts:M36", + "number": 36, + "source": "src/rxdb.ts", + "line": 184, + "position": 6221, + "operator": "field-drop", + "original": "sort: [{ '_meta.lwt': `asc` }, { [primaryPath]: `asc` }]", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~3" + }, + { + "id": "rxdb.ts:M37", + "number": 37, + "source": "src/rxdb.ts", + "line": 184, + "position": 6230, + "operator": "field-drop", + "original": "'_meta.lwt': `asc`", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~4" + }, + { + "id": "rxdb.ts:M38", + "number": 38, + "source": "src/rxdb.ts", + "line": 184, + "position": 6254, + "operator": "field-drop", + "original": "[primaryPath]: `asc`", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~5" + }, + { + "id": "rxdb.ts:M39", + "number": 39, + "source": "src/rxdb.ts", + "line": 185, + "position": 6293, + "operator": "field-drop", + "original": "limit: syncBatchSize", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~6" + }, + { + "id": "rxdb.ts:M40", + "number": 40, + "source": "src/rxdb.ts", + "line": 186, + "position": 6329, + "operator": "field-drop", + "original": "skip: 0", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~7" + }, + { + "id": "rxdb.ts:M41", + "number": 41, + "source": "src/rxdb.ts", + "line": 196, + "position": 6724, + "operator": "value-undefined", + "original": "prepareQuery(\n rxCollection.storageInstance.schema,\n query,\n )", + "replacement": "undefined", + "anchor": "const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , )" + }, + { + "id": "rxdb.ts:M42", + "number": 42, + "source": "src/rxdb.ts", + "line": 200, + "position": 6854, + "operator": "value-undefined", + "original": "rxCollection.storageInstance.query(preparedQuery)", + "replacement": "undefined", + "anchor": "const result = await rxCollection . storageInstance . query ( preparedQuery )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:const result = await rxCollection . storageInstance . query ( preparedQuery )" + }, + { + "id": "rxdb.ts:M43", + "number": 43, + "source": "src/rxdb.ts", + "line": 201, + "position": 6918, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( isStopped ( ) ) return" + }, + { + "id": "rxdb.ts:M44", + "number": 44, + "source": "src/rxdb.ts", + "line": 201, + "position": 6918, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( isStopped ( ) ) return" + }, + { + "id": "rxdb.ts:M45", + "number": 45, + "source": "src/rxdb.ts", + "line": 201, + "position": 6918, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:if ( isStopped ( ) ) return" + }, + { + "id": "rxdb.ts:M46", + "number": 46, + "source": "src/rxdb.ts", + "line": 204, + "position": 6998, + "operator": "value-undefined", + "original": "lastOfArray(docs)", + "replacement": "undefined", + "anchor": "cursor = lastOfArray ( docs )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:cursor = lastOfArray ( docs )" + }, + { + "id": "rxdb.ts:M47", + "number": 47, + "source": "src/rxdb.ts", + "line": 205, + "position": 7030, + "operator": "if-true", + "original": "docs.length === 0", + "replacement": "true", + "anchor": "if ( docs . length === 0 ) { initialFetchComplete = true break }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( docs . length === 0 ) { initialFetchComplete = true break }" + }, + { + "id": "rxdb.ts:M48", + "number": 48, + "source": "src/rxdb.ts", + "line": 205, + "position": 7030, + "operator": "if-false", + "original": "docs.length === 0", + "replacement": "false", + "anchor": "if ( docs . length === 0 ) { initialFetchComplete = true break }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( docs . length === 0 ) { initialFetchComplete = true break }" + }, + { + "id": "rxdb.ts:M49", + "number": 49, + "source": "src/rxdb.ts", + "line": 205, + "position": 7042, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( docs . length === 0 ) { initialFetchComplete = true break }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:if ( docs . length === 0 ) { initialFetchComplete = true break }" + }, + { + "id": "rxdb.ts:M50", + "number": 50, + "source": "src/rxdb.ts", + "line": 206, + "position": 7086, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "initialFetchComplete = true", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:initialFetchComplete = true" + }, + { + "id": "rxdb.ts:M51", + "number": 51, + "source": "src/rxdb.ts", + "line": 210, + "position": 7132, + "operator": "delete-call", + "original": "docs.forEach((d) => {\n listed?.add(getKey(d))\n write({\n type: `insert`,\n value: stripRxdbFields(clone(d)),\n })\n })", + "replacement": ";", + "anchor": "docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } )" + }, + { + "id": "rxdb.ts:M52", + "number": 52, + "source": "src/rxdb.ts", + "line": 211, + "position": 7166, + "operator": "delete-call", + "original": "listed?.add(getKey(d))", + "replacement": ";", + "anchor": "listed ?. add ( getKey ( d ) )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:listed ?. add ( getKey ( d ) )" + }, + { + "id": "rxdb.ts:M53", + "number": 53, + "source": "src/rxdb.ts", + "line": 211, + "position": 7178, + "operator": "value-undefined", + "original": "getKey(d)", + "replacement": "undefined", + "anchor": "listed ?. add ( getKey ( d ) )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:listed ?. add ( getKey ( d ) )" + }, + { + "id": "rxdb.ts:M54", + "number": 54, + "source": "src/rxdb.ts", + "line": 212, + "position": 7201, + "operator": "delete-call", + "original": "write({\n type: `insert`,\n value: stripRxdbFields(clone(d)),\n })", + "replacement": ";", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )" + }, + { + "id": "rxdb.ts:M55", + "number": 55, + "source": "src/rxdb.ts", + "line": 213, + "position": 7223, + "operator": "field-drop", + "original": "type: `insert`", + "replacement": "", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )" + }, + { + "id": "rxdb.ts:M56", + "number": 56, + "source": "src/rxdb.ts", + "line": 214, + "position": 7253, + "operator": "field-drop", + "original": "value: stripRxdbFields(clone(d))", + "replacement": "", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )~2" + }, + { + "id": "rxdb.ts:M57", + "number": 57, + "source": "src/rxdb.ts", + "line": 214, + "position": 7260, + "operator": "value-undefined", + "original": "stripRxdbFields(clone(d))", + "replacement": "undefined", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )" + }, + { + "id": "rxdb.ts:M58", + "number": 58, + "source": "src/rxdb.ts", + "line": 214, + "position": 7276, + "operator": "value-undefined", + "original": "clone(d)", + "replacement": "undefined", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )~2" + }, + { + "id": "rxdb.ts:M59", + "number": 59, + "source": "src/rxdb.ts", + "line": 218, + "position": 7354, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const firstApplied = commit ( )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:const firstApplied = commit ( )" + }, + { + "id": "rxdb.ts:M60", + "number": 60, + "source": "src/rxdb.ts", + "line": 219, + "position": 7375, + "operator": "if-true", + "original": "firstApplied !== true", + "replacement": "true", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M61", + "number": 61, + "source": "src/rxdb.ts", + "line": 219, + "position": 7375, + "operator": "if-false", + "original": "firstApplied !== true", + "replacement": "false", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M62", + "number": 62, + "source": "src/rxdb.ts", + "line": 219, + "position": 7388, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M63", + "number": 63, + "source": "src/rxdb.ts", + "line": 219, + "position": 7392, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M64", + "number": 64, + "source": "src/rxdb.ts", + "line": 219, + "position": 7403, + "operator": "value-undefined", + "original": "firstApplied.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:void firstApplied . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M65", + "number": 65, + "source": "src/rxdb.ts", + "line": 221, + "position": 7519, + "operator": "if-true", + "original": "listed && persistence", + "replacement": "true", + "anchor": "if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }" + }, + { + "id": "rxdb.ts:M66", + "number": 66, + "source": "src/rxdb.ts", + "line": 221, + "position": 7519, + "operator": "if-false", + "original": "listed && persistence", + "replacement": "false", + "anchor": "if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }" + }, + { + "id": "rxdb.ts:M67", + "number": 67, + "source": "src/rxdb.ts", + "line": 221, + "position": 7526, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }" + }, + { + "id": "rxdb.ts:M68", + "number": 68, + "source": "src/rxdb.ts", + "line": 222, + "position": 7560, + "operator": "value-undefined", + "original": "persistence.hydrateBaseline()", + "replacement": "undefined", + "anchor": "await persistence . hydrateBaseline ( )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:await persistence . hydrateBaseline ( )" + }, + { + "id": "rxdb.ts:M69", + "number": 69, + "source": "src/rxdb.ts", + "line": 223, + "position": 7604, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( isStopped ( ) ) return~2" + }, + { + "id": "rxdb.ts:M70", + "number": 70, + "source": "src/rxdb.ts", + "line": 223, + "position": 7604, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( isStopped ( ) ) return~2" + }, + { + "id": "rxdb.ts:M71", + "number": 71, + "source": "src/rxdb.ts", + "line": 223, + "position": 7604, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:if ( isStopped ( ) ) return~2" + }, + { + "id": "rxdb.ts:M72", + "number": 72, + "source": "src/rxdb.ts", + "line": 228, + "position": 7913, + "operator": "value-undefined", + "original": "collection._state.acceptedSyncedEntries()", + "replacement": "undefined", + "anchor": "for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) }" + }, + { + "id": "rxdb.ts:M73", + "number": 73, + "source": "src/rxdb.ts", + "line": 229, + "position": 7974, + "operator": "if-true", + "original": "!listed.has(key)", + "replacement": "true", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( ! listed . has ( key ) ) gone . push ( key )" + }, + { + "id": "rxdb.ts:M74", + "number": 74, + "source": "src/rxdb.ts", + "line": 229, + "position": 7974, + "operator": "if-false", + "original": "!listed.has(key)", + "replacement": "false", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( ! listed . has ( key ) ) gone . push ( key )" + }, + { + "id": "rxdb.ts:M75", + "number": 75, + "source": "src/rxdb.ts", + "line": 229, + "position": 7974, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:drop-not:if ( ! listed . has ( key ) ) gone . push ( key )" + }, + { + "id": "rxdb.ts:M76", + "number": 76, + "source": "src/rxdb.ts", + "line": 229, + "position": 7975, + "operator": "value-undefined", + "original": "listed.has(key)", + "replacement": "undefined", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:if ( ! listed . has ( key ) ) gone . push ( key )" + }, + { + "id": "rxdb.ts:M77", + "number": 77, + "source": "src/rxdb.ts", + "line": 229, + "position": 7992, + "operator": "delete-call", + "original": "gone.push(key)", + "replacement": ";", + "anchor": "gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:gone . push ( key )" + }, + { + "id": "rxdb.ts:M78", + "number": 78, + "source": "src/rxdb.ts", + "line": 231, + "position": 8049, + "operator": "loop-false", + "original": "first < gone.length", + "replacement": "false", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:loop-false:for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }" + }, + { + "id": "rxdb.ts:M79", + "number": 79, + "source": "src/rxdb.ts", + "line": 231, + "position": 8055, + "operator": "binary-swap", + "original": "<", + "replacement": "<=", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }" + }, + { + "id": "rxdb.ts:M80", + "number": 80, + "source": "src/rxdb.ts", + "line": 232, + "position": 8108, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:begin ( )" + }, + { + "id": "rxdb.ts:M81", + "number": 81, + "source": "src/rxdb.ts", + "line": 233, + "position": 8146, + "operator": "value-undefined", + "original": "gone.slice(first, first + syncBatchSize)", + "replacement": "undefined", + "anchor": "for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } )" + }, + { + "id": "rxdb.ts:M82", + "number": 82, + "source": "src/rxdb.ts", + "line": 234, + "position": 8202, + "operator": "delete-call", + "original": "write({ type: `delete`, key })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:write ( { type : `delete` , key } )" + }, + { + "id": "rxdb.ts:M83", + "number": 83, + "source": "src/rxdb.ts", + "line": 234, + "position": 8210, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:write ( { type : `delete` , key } )" + }, + { + "id": "rxdb.ts:M84", + "number": 84, + "source": "src/rxdb.ts", + "line": 234, + "position": 8226, + "operator": "field-drop", + "original": "key", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:write ( { type : `delete` , key } )~2" + }, + { + "id": "rxdb.ts:M85", + "number": 85, + "source": "src/rxdb.ts", + "line": 235, + "position": 8261, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const receipt = commit ( )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:const receipt = commit ( )" + }, + { + "id": "rxdb.ts:M86", + "number": 86, + "source": "src/rxdb.ts", + "line": 236, + "position": 8286, + "operator": "if-true", + "original": "receipt !== true", + "replacement": "true", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( receipt !== true ) void receipt . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M87", + "number": 87, + "source": "src/rxdb.ts", + "line": 236, + "position": 8286, + "operator": "if-false", + "original": "receipt !== true", + "replacement": "false", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( receipt !== true ) void receipt . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M88", + "number": 88, + "source": "src/rxdb.ts", + "line": 236, + "position": 8294, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:if ( receipt !== true ) void receipt . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M89", + "number": 89, + "source": "src/rxdb.ts", + "line": 236, + "position": 8298, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:if ( receipt !== true ) void receipt . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M90", + "number": 90, + "source": "src/rxdb.ts", + "line": 236, + "position": 8309, + "operator": "value-undefined", + "original": "receipt.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:void receipt . catch ( ( ) => undefined )" + }, + { + "id": "rxdb.ts:M91", + "number": 91, + "source": "src/rxdb.ts", + "line": 237, + "position": 8352, + "operator": "delete-call", + "original": "applied.push(receipt)", + "replacement": ";", + "anchor": "applied . push ( receipt )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:applied . push ( receipt )" + }, + { + "id": "rxdb.ts:M92", + "number": 92, + "source": "src/rxdb.ts", + "line": 240, + "position": 8410, + "operator": "value-undefined", + "original": "Promise.all(applied)", + "replacement": "undefined", + "anchor": "await Promise . all ( applied )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:await Promise . all ( applied )" + }, + { + "id": "rxdb.ts:M93", + "number": 93, + "source": "src/rxdb.ts", + "line": 245, + "position": 8561, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "let buffering = true", + "function": "sync", + "key": "src/rxdb.ts#sync:bool-flip:let buffering = true" + }, + { + "id": "rxdb.ts:M94", + "number": 94, + "source": "src/rxdb.ts", + "line": 247, + "position": 8623, + "operator": "if-true", + "original": "buffering", + "replacement": "true", + "anchor": "if ( buffering ) { buffer . push ( msg ) return }", + "function": "queue", + "key": "src/rxdb.ts#queue:if-true:if ( buffering ) { buffer . push ( msg ) return }" + }, + { + "id": "rxdb.ts:M95", + "number": 95, + "source": "src/rxdb.ts", + "line": 247, + "position": 8623, + "operator": "if-false", + "original": "buffering", + "replacement": "false", + "anchor": "if ( buffering ) { buffer . push ( msg ) return }", + "function": "queue", + "key": "src/rxdb.ts#queue:if-false:if ( buffering ) { buffer . push ( msg ) return }" + }, + { + "id": "rxdb.ts:M96", + "number": 96, + "source": "src/rxdb.ts", + "line": 248, + "position": 8646, + "operator": "delete-call", + "original": "buffer.push(msg)", + "replacement": ";", + "anchor": "buffer . push ( msg )", + "function": "queue", + "key": "src/rxdb.ts#queue:delete-call:buffer . push ( msg )" + }, + { + "id": "rxdb.ts:M97", + "number": 97, + "source": "src/rxdb.ts", + "line": 251, + "position": 8698, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "queue", + "key": "src/rxdb.ts#queue:delete-call:begin ( )" + }, + { + "id": "rxdb.ts:M98", + "number": 98, + "source": "src/rxdb.ts", + "line": 252, + "position": 8714, + "operator": "delete-call", + "original": "write(msg)", + "replacement": ";", + "anchor": "write ( msg )", + "function": "queue", + "key": "src/rxdb.ts#queue:delete-call:write ( msg )" + }, + { + "id": "rxdb.ts:M99", + "number": 99, + "source": "src/rxdb.ts", + "line": 253, + "position": 8733, + "operator": "delete-call", + "original": "commit()", + "replacement": ";", + "anchor": "commit ( )", + "function": "queue", + "key": "src/rxdb.ts#queue:delete-call:commit ( )" + }, + { + "id": "rxdb.ts:M100", + "number": 100, + "source": "src/rxdb.ts", + "line": 257, + "position": 8833, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "const isStopped = ( ) => sub ?. closed !== false", + "function": "isStopped", + "key": "src/rxdb.ts#isStopped:binary-swap:const isStopped = ( ) => sub ?. closed !== false" + }, + { + "id": "rxdb.ts:M101", + "number": 101, + "source": "src/rxdb.ts", + "line": 257, + "position": 8837, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "const isStopped = ( ) => sub ?. closed !== false", + "function": "isStopped", + "key": "src/rxdb.ts#isStopped:bool-flip:const isStopped = ( ) => sub ?. closed !== false" + }, + { + "id": "rxdb.ts:M102", + "number": 102, + "source": "src/rxdb.ts", + "line": 260, + "position": 8917, + "operator": "if-true", + "original": "!sub", + "replacement": "true", + "anchor": "if ( ! sub ) return", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:if-true:if ( ! sub ) return" + }, + { + "id": "rxdb.ts:M103", + "number": 103, + "source": "src/rxdb.ts", + "line": 260, + "position": 8917, + "operator": "if-false", + "original": "!sub", + "replacement": "false", + "anchor": "if ( ! sub ) return", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:if-false:if ( ! sub ) return" + }, + { + "id": "rxdb.ts:M104", + "number": 104, + "source": "src/rxdb.ts", + "line": 260, + "position": 8917, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! sub ) return", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:drop-not:if ( ! sub ) return" + }, + { + "id": "rxdb.ts:M105", + "number": 105, + "source": "src/rxdb.ts", + "line": 261, + "position": 8938, + "operator": "delete-call", + "original": "getFromMapOrCreate(\n OPEN_RXDB_SUBSCRIPTIONS,\n rxCollection,\n () => new Set(),\n ).delete(sub)", + "replacement": ";", + "anchor": "getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub )", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:delete-call:getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub )" + }, + { + "id": "rxdb.ts:M106", + "number": 106, + "source": "src/rxdb.ts", + "line": 266, + "position": 9074, + "operator": "delete-call", + "original": "sub.unsubscribe()", + "replacement": ";", + "anchor": "sub . unsubscribe ( )", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:delete-call:sub . unsubscribe ( )" + }, + { + "id": "rxdb.ts:M107", + "number": 107, + "source": "src/rxdb.ts", + "line": 272, + "position": 9259, + "operator": "value-undefined", + "original": "rxCollection.$.subscribe({\n next: (ev) => {\n const cur: Row = stripRxdbFields(clone(ev.documentData as Row))\n switch (ev.operation) {\n // The initial read can already hold a document whose insert\n // event was buffered, so an insert writes as an update. Core\n // inserts an updated key it does not hold.\n case `INSERT`:\n case `UPDATE`:\n queue({ type: `update`, value: cur })\n break\n case `DELETE`:\n queue({ type: `delete`, value: cur })\n break\n }\n },\n // An errored stream ends, so later writes would never arrive.\n error: stop,\n })", + "replacement": "undefined", + "anchor": "sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )", + "function": "startOngoingFetch", + "key": "src/rxdb.ts#startOngoingFetch:value-undefined:sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )" + }, + { + "id": "rxdb.ts:M108", + "number": 108, + "source": "src/rxdb.ts", + "line": 273, + "position": 9296, + "operator": "field-drop", + "original": "next: (ev) => {\n const cur: Row = stripRxdbFields(clone(ev.documentData as Row))\n switch (ev.operation) {\n // The initial read can already hold a document whose insert\n // event was buffered, so an insert writes as an update. Core\n // inserts an updated key it does not hold.\n case `INSERT`:\n case `UPDATE`:\n queue({ type: `update`, value: cur })\n break\n case `DELETE`:\n queue({ type: `delete`, value: cur })\n break\n }\n }", + "replacement": "", + "anchor": "sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )" + }, + { + "id": "rxdb.ts:M109", + "number": 109, + "source": "src/rxdb.ts", + "line": 274, + "position": 9341, + "operator": "value-undefined", + "original": "stripRxdbFields(clone(ev.documentData as Row))", + "replacement": "undefined", + "anchor": "const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) )", + "function": "next", + "key": "src/rxdb.ts#next:value-undefined:const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) )" + }, + { + "id": "rxdb.ts:M110", + "number": 110, + "source": "src/rxdb.ts", + "line": 274, + "position": 9357, + "operator": "value-undefined", + "original": "clone(ev.documentData as Row)", + "replacement": "undefined", + "anchor": "const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) )", + "function": "next", + "key": "src/rxdb.ts#next:value-undefined:const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) )~2" + }, + { + "id": "rxdb.ts:M111", + "number": 111, + "source": "src/rxdb.ts", + "line": 281, + "position": 9707, + "operator": "delete-call", + "original": "queue({ type: `update`, value: cur })", + "replacement": ";", + "anchor": "queue ( { type : `update` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:delete-call:queue ( { type : `update` , value : cur } )" + }, + { + "id": "rxdb.ts:M112", + "number": 112, + "source": "src/rxdb.ts", + "line": 281, + "position": 9715, + "operator": "field-drop", + "original": "type: `update`", + "replacement": "", + "anchor": "queue ( { type : `update` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:queue ( { type : `update` , value : cur } )" + }, + { + "id": "rxdb.ts:M113", + "number": 113, + "source": "src/rxdb.ts", + "line": 281, + "position": 9731, + "operator": "field-drop", + "original": "value: cur", + "replacement": "", + "anchor": "queue ( { type : `update` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:queue ( { type : `update` , value : cur } )~2" + }, + { + "id": "rxdb.ts:M114", + "number": 114, + "source": "src/rxdb.ts", + "line": 284, + "position": 9812, + "operator": "delete-call", + "original": "queue({ type: `delete`, value: cur })", + "replacement": ";", + "anchor": "queue ( { type : `delete` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:delete-call:queue ( { type : `delete` , value : cur } )" + }, + { + "id": "rxdb.ts:M115", + "number": 115, + "source": "src/rxdb.ts", + "line": 284, + "position": 9820, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "queue ( { type : `delete` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:queue ( { type : `delete` , value : cur } )" + }, + { + "id": "rxdb.ts:M116", + "number": 116, + "source": "src/rxdb.ts", + "line": 284, + "position": 9836, + "operator": "field-drop", + "original": "value: cur", + "replacement": "", + "anchor": "queue ( { type : `delete` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:queue ( { type : `delete` , value : cur } )~2" + }, + { + "id": "rxdb.ts:M117", + "number": 117, + "source": "src/rxdb.ts", + "line": 289, + "position": 9982, + "operator": "field-drop", + "original": "error: stop", + "replacement": "", + "anchor": "sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )", + "function": "startOngoingFetch", + "key": "src/rxdb.ts#startOngoingFetch:field-drop:sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )" + }, + { + "id": "rxdb.ts:M118", + "number": 118, + "source": "src/rxdb.ts", + "line": 292, + "position": 10028, + "operator": "value-undefined", + "original": "getFromMapOrCreate(\n OPEN_RXDB_SUBSCRIPTIONS,\n rxCollection,\n () => new Set(),\n )", + "replacement": "undefined", + "anchor": "const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , )", + "function": "startOngoingFetch", + "key": "src/rxdb.ts#startOngoingFetch:value-undefined:const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , )" + }, + { + "id": "rxdb.ts:M119", + "number": 119, + "source": "src/rxdb.ts", + "line": 297, + "position": 10152, + "operator": "delete-call", + "original": "subs.add(sub)", + "replacement": ";", + "anchor": "subs . add ( sub )", + "function": "startOngoingFetch", + "key": "src/rxdb.ts#startOngoingFetch:delete-call:subs . add ( sub )" + }, + { + "id": "rxdb.ts:M120", + "number": 120, + "source": "src/rxdb.ts", + "line": 301, + "position": 10214, + "operator": "delete-call", + "original": "startOngoingFetch()", + "replacement": ";", + "anchor": "startOngoingFetch ( )", + "function": "start", + "key": "src/rxdb.ts#start:delete-call:startOngoingFetch ( )" + }, + { + "id": "rxdb.ts:M121", + "number": 121, + "source": "src/rxdb.ts", + "line": 302, + "position": 10246, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) return", + "function": "start", + "key": "src/rxdb.ts#start:if-true:if ( isStopped ( ) ) return" + }, + { + "id": "rxdb.ts:M122", + "number": 122, + "source": "src/rxdb.ts", + "line": 302, + "position": 10246, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) return", + "function": "start", + "key": "src/rxdb.ts#start:if-false:if ( isStopped ( ) ) return" + }, + { + "id": "rxdb.ts:M123", + "number": 123, + "source": "src/rxdb.ts", + "line": 302, + "position": 10246, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) return", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:if ( isStopped ( ) ) return" + }, + { + "id": "rxdb.ts:M124", + "number": 124, + "source": "src/rxdb.ts", + "line": 303, + "position": 10280, + "operator": "value-undefined", + "original": "initialFetch()", + "replacement": "undefined", + "anchor": "await initialFetch ( )", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:await initialFetch ( )" + }, + { + "id": "rxdb.ts:M125", + "number": 125, + "source": "src/rxdb.ts", + "line": 304, + "position": 10307, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:if-true:if ( isStopped ( ) ) { return }" + }, + { + "id": "rxdb.ts:M126", + "number": 126, + "source": "src/rxdb.ts", + "line": 304, + "position": 10307, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:if-false:if ( isStopped ( ) ) { return }" + }, + { + "id": "rxdb.ts:M127", + "number": 127, + "source": "src/rxdb.ts", + "line": 304, + "position": 10307, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:if ( isStopped ( ) ) { return }" + }, + { + "id": "rxdb.ts:M128", + "number": 128, + "source": "src/rxdb.ts", + "line": 312, + "position": 10662, + "operator": "value-undefined", + "original": "buffer.splice(0)", + "replacement": "undefined", + "anchor": "const pending = buffer . splice ( 0 )", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:const pending = buffer . splice ( 0 )" + }, + { + "id": "rxdb.ts:M129", + "number": 129, + "source": "src/rxdb.ts", + "line": 313, + "position": 10699, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "buffering = false", + "function": "start", + "key": "src/rxdb.ts#start:bool-flip:buffering = false" + }, + { + "id": "rxdb.ts:M130", + "number": 130, + "source": "src/rxdb.ts", + "line": 314, + "position": 10717, + "operator": "if-true", + "original": "pending.length > 0", + "replacement": "true", + "anchor": "if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }", + "function": "start", + "key": "src/rxdb.ts#start:if-true:if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }" + }, + { + "id": "rxdb.ts:M131", + "number": 131, + "source": "src/rxdb.ts", + "line": 314, + "position": 10717, + "operator": "if-false", + "original": "pending.length > 0", + "replacement": "false", + "anchor": "if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }", + "function": "start", + "key": "src/rxdb.ts#start:if-false:if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }" + }, + { + "id": "rxdb.ts:M132", + "number": 132, + "source": "src/rxdb.ts", + "line": 314, + "position": 10732, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }", + "function": "start", + "key": "src/rxdb.ts#start:binary-swap:if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }" + }, + { + "id": "rxdb.ts:M133", + "number": 133, + "source": "src/rxdb.ts", + "line": 315, + "position": 10749, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "start", + "key": "src/rxdb.ts#start:delete-call:begin ( )" + }, + { + "id": "rxdb.ts:M134", + "number": 134, + "source": "src/rxdb.ts", + "line": 316, + "position": 10794, + "operator": "delete-call", + "original": "write(msg)", + "replacement": ";", + "anchor": "write ( msg )", + "function": "start", + "key": "src/rxdb.ts#start:delete-call:write ( msg )" + }, + { + "id": "rxdb.ts:M135", + "number": 135, + "source": "src/rxdb.ts", + "line": 317, + "position": 10821, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "await commit ( )", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:await commit ( )" + }, + { + "id": "rxdb.ts:M136", + "number": 136, + "source": "src/rxdb.ts", + "line": 318, + "position": 10844, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:if-true:if ( isStopped ( ) ) { return }~2" + }, + { + "id": "rxdb.ts:M137", + "number": 137, + "source": "src/rxdb.ts", + "line": 318, + "position": 10844, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:if-false:if ( isStopped ( ) ) { return }~2" + }, + { + "id": "rxdb.ts:M138", + "number": 138, + "source": "src/rxdb.ts", + "line": 318, + "position": 10844, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:if ( isStopped ( ) ) { return }~2" + }, + { + "id": "rxdb.ts:M139", + "number": 139, + "source": "src/rxdb.ts", + "line": 324, + "position": 10971, + "operator": "delete-call", + "original": "markReady()", + "replacement": ";", + "anchor": "markReady ( )", + "function": "start", + "key": "src/rxdb.ts#start:delete-call:markReady ( )" + }, + { + "id": "rxdb.ts:M140", + "number": 140, + "source": "src/rxdb.ts", + "line": 329, + "position": 11092, + "operator": "delete-call", + "original": "stopOngoingFetch()", + "replacement": ";", + "anchor": "stopOngoingFetch ( )", + "function": "stop", + "key": "src/rxdb.ts#stop:delete-call:stopOngoingFetch ( )" + }, + { + "id": "rxdb.ts:M141", + "number": 141, + "source": "src/rxdb.ts", + "line": 330, + "position": 11119, + "operator": "delete-call", + "original": "fail(error)", + "replacement": ";", + "anchor": "fail ( error )", + "function": "stop", + "key": "src/rxdb.ts#stop:delete-call:fail ( error )" + }, + { + "id": "rxdb.ts:M142", + "number": 142, + "source": "src/rxdb.ts", + "line": 332, + "position": 11150, + "operator": "value-undefined", + "original": "start().catch(stop)", + "replacement": "undefined", + "anchor": "void start ( ) . catch ( stop )", + "function": "sync", + "key": "src/rxdb.ts#sync:value-undefined:void start ( ) . catch ( stop )" + }, + { + "id": "rxdb.ts:M143", + "number": 143, + "source": "src/rxdb.ts", + "line": 338, + "position": 11343, + "operator": "field-drop", + "original": "rowUpdateMode: `full`", + "replacement": "", + "anchor": "const sync : SyncConfig < Row , Key > = { sync : ( params : SyncParams ) => { const { begin , write , commit , markReady , fail , collection } = params const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let initialFetchComplete = false async function initialFetch ( ) { let cursor : RxDocumentData < Row > | undefined = undefined const syncBatchSize = config . syncBatchSize ? config . syncBatchSize : 1000 const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } ) while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) } const firstApplied = commit ( ) if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined ) const applied : Array < true | Promise < void >> = [ firstApplied ] if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } } await Promise . all ( applied ) } type WriteMessage = Parameters < typeof write > [ 0 ] const buffer : Array < WriteMessage > = [ ] let buffering = true const queue = ( msg : WriteMessage ) => { if ( buffering ) { buffer . push ( msg ) return } begin ( ) write ( msg ) commit ( ) } let sub : Subscription | undefined const isStopped = ( ) => sub ?. closed !== false function stopOngoingFetch ( ) { buffer . length = 0 if ( ! sub ) return getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub ) sub . unsubscribe ( ) sub = undefined } function startOngoingFetch ( ) { sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } ) const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) subs . add ( sub ) } async function start ( ) { startOngoingFetch ( ) if ( isStopped ( ) ) return await initialFetch ( ) if ( isStopped ( ) ) { return } const pending = buffer . splice ( 0 ) buffering = false if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } } markReady ( ) } function stop ( error : unknown ) { stopOngoingFetch ( ) fail ( error ) } void start ( ) . catch ( stop ) return stopOngoingFetch } , rowUpdateMode : `full` , }", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:field-drop:const sync : SyncConfig < Row , Key > = { sync : ( params : SyncParams ) => { const { begin , write , commit , markReady , fail , collection } = params const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let initialFetchComplete = false async function initialFetch ( ) { let cursor : RxDocumentData < Row > | undefined = undefined const syncBatchSize = config . syncBatchSize ? config . syncBatchSize : 1000 const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } ) while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) } const firstApplied = commit ( ) if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined ) const applied : Array < true | Promise < void >> = [ firstApplied ] if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } } await Promise . all ( applied ) } type WriteMessage = Parameters < typeof write > [ 0 ] const buffer : Array < WriteMessage > = [ ] let buffering = true const queue = ( msg : WriteMessage ) => { if ( buffering ) { buffer . push ( msg ) return } begin ( ) write ( msg ) commit ( ) } let sub : Subscription | undefined const isStopped = ( ) => sub ?. closed !== false function stopOngoingFetch ( ) { buffer . length = 0 if ( ! sub ) return getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub ) sub . unsubscribe ( ) sub = undefined } function startOngoingFetch ( ) { sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } ) const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) subs . add ( sub ) } async function start ( ) { startOngoingFetch ( ) if ( isStopped ( ) ) return await initialFetch ( ) if ( isStopped ( ) ) { return } const pending = buffer . splice ( 0 ) buffering = false if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } } markReady ( ) } function stop ( error : unknown ) { stopOngoingFetch ( ) fail ( error ) } void start ( ) . catch ( stop ) return stopOngoingFetch } , rowUpdateMode : `full` , }" + }, + { + "id": "rxdb.ts:M144", + "number": 144, + "source": "src/rxdb.ts", + "line": 343, + "position": 11454, + "operator": "field-drop", + "original": "getKey", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }" + }, + { + "id": "rxdb.ts:M145", + "number": 145, + "source": "src/rxdb.ts", + "line": 344, + "position": 11466, + "operator": "field-drop", + "original": "sync", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }~2" + }, + { + "id": "rxdb.ts:M146", + "number": 146, + "source": "src/rxdb.ts", + "line": 345, + "position": 11476, + "operator": "field-drop", + "original": "onInsert: async (params) => {\n debug(`insert`, params)\n const newItems = params.transaction.mutations.map((m) => m.modified)\n const result = await rxCollection.bulkUpsert(newItems)\n if (result.error.length > 0) {\n throw rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0]))\n }\n }", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }" + }, + { + "id": "rxdb.ts:M147", + "number": 147, + "source": "src/rxdb.ts", + "line": 346, + "position": 11512, + "operator": "delete-call", + "original": "debug(`insert`, params)", + "replacement": ";", + "anchor": "debug ( `insert` , params )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:delete-call:debug ( `insert` , params )" + }, + { + "id": "rxdb.ts:M148", + "number": 148, + "source": "src/rxdb.ts", + "line": 347, + "position": 11559, + "operator": "value-undefined", + "original": "params.transaction.mutations.map((m) => m.modified)", + "replacement": "undefined", + "anchor": "const newItems = params . transaction . mutations . map ( ( m ) => m . modified )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:value-undefined:const newItems = params . transaction . mutations . map ( ( m ) => m . modified )" + }, + { + "id": "rxdb.ts:M149", + "number": 149, + "source": "src/rxdb.ts", + "line": 348, + "position": 11638, + "operator": "value-undefined", + "original": "rxCollection.bulkUpsert(newItems)", + "replacement": "undefined", + "anchor": "const result = await rxCollection . bulkUpsert ( newItems )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:value-undefined:const result = await rxCollection . bulkUpsert ( newItems )" + }, + { + "id": "rxdb.ts:M150", + "number": 150, + "source": "src/rxdb.ts", + "line": 349, + "position": 11682, + "operator": "if-true", + "original": "result.error.length > 0", + "replacement": "true", + "anchor": "if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:if-true:if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }" + }, + { + "id": "rxdb.ts:M151", + "number": 151, + "source": "src/rxdb.ts", + "line": 349, + "position": 11682, + "operator": "if-false", + "original": "result.error.length > 0", + "replacement": "false", + "anchor": "if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:if-false:if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }" + }, + { + "id": "rxdb.ts:M152", + "number": 152, + "source": "src/rxdb.ts", + "line": 349, + "position": 11702, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:binary-swap:if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }" + }, + { + "id": "rxdb.ts:M153", + "number": 153, + "source": "src/rxdb.ts", + "line": 350, + "position": 11723, + "operator": "value-undefined", + "original": "rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0]))", + "replacement": "undefined", + "anchor": "throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:value-undefined:throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) )" + }, + { + "id": "rxdb.ts:M154", + "number": 154, + "source": "src/rxdb.ts", + "line": 350, + "position": 11752, + "operator": "value-undefined", + "original": "ensureNotFalsy(result.error[0])", + "replacement": "undefined", + "anchor": "throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:value-undefined:throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) )~2" + }, + { + "id": "rxdb.ts:M155", + "number": 155, + "source": "src/rxdb.ts", + "line": 353, + "position": 11804, + "operator": "field-drop", + "original": "onUpdate: async (params) => {\n debug(`update`, params)\n const mutations = params.transaction.mutations.filter(\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n (m) => m.type === `update`,\n )\n\n for (const mutation of mutations) {\n const newValue = stripRxdbFields(mutation.modified)\n const id = getKey(newValue)\n const doc = await rxCollection.findOne(id).exec()\n if (!doc) {\n continue\n }\n await doc.incrementalPatch(newValue)\n }\n }", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }" + }, + { + "id": "rxdb.ts:M156", + "number": 156, + "source": "src/rxdb.ts", + "line": 354, + "position": 11840, + "operator": "delete-call", + "original": "debug(`update`, params)", + "replacement": ";", + "anchor": "debug ( `update` , params )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:delete-call:debug ( `update` , params )" + }, + { + "id": "rxdb.ts:M157", + "number": 157, + "source": "src/rxdb.ts", + "line": 355, + "position": 11888, + "operator": "value-undefined", + "original": "params.transaction.mutations.filter(\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n (m) => m.type === `update`,\n )", + "replacement": "undefined", + "anchor": "const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , )" + }, + { + "id": "rxdb.ts:M158", + "number": 158, + "source": "src/rxdb.ts", + "line": 357, + "position": 12027, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:binary-swap:const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , )" + }, + { + "id": "rxdb.ts:M159", + "number": 159, + "source": "src/rxdb.ts", + "line": 361, + "position": 12117, + "operator": "value-undefined", + "original": "stripRxdbFields(mutation.modified)", + "replacement": "undefined", + "anchor": "const newValue = stripRxdbFields ( mutation . modified )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:const newValue = stripRxdbFields ( mutation . modified )" + }, + { + "id": "rxdb.ts:M160", + "number": 160, + "source": "src/rxdb.ts", + "line": 362, + "position": 12171, + "operator": "value-undefined", + "original": "getKey(newValue)", + "replacement": "undefined", + "anchor": "const id = getKey ( newValue )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:const id = getKey ( newValue )" + }, + { + "id": "rxdb.ts:M161", + "number": 161, + "source": "src/rxdb.ts", + "line": 363, + "position": 12214, + "operator": "value-undefined", + "original": "rxCollection.findOne(id).exec()", + "replacement": "undefined", + "anchor": "const doc = await rxCollection . findOne ( id ) . exec ( )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:const doc = await rxCollection . findOne ( id ) . exec ( )" + }, + { + "id": "rxdb.ts:M162", + "number": 162, + "source": "src/rxdb.ts", + "line": 364, + "position": 12258, + "operator": "if-true", + "original": "!doc", + "replacement": "true", + "anchor": "if ( ! doc ) { continue }", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:if-true:if ( ! doc ) { continue }" + }, + { + "id": "rxdb.ts:M163", + "number": 163, + "source": "src/rxdb.ts", + "line": 364, + "position": 12258, + "operator": "if-false", + "original": "!doc", + "replacement": "false", + "anchor": "if ( ! doc ) { continue }", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:if-false:if ( ! doc ) { continue }" + }, + { + "id": "rxdb.ts:M164", + "number": 164, + "source": "src/rxdb.ts", + "line": 364, + "position": 12258, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! doc ) { continue }", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:drop-not:if ( ! doc ) { continue }" + }, + { + "id": "rxdb.ts:M165", + "number": 165, + "source": "src/rxdb.ts", + "line": 367, + "position": 12309, + "operator": "value-undefined", + "original": "doc.incrementalPatch(newValue)", + "replacement": "undefined", + "anchor": "await doc . incrementalPatch ( newValue )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:await doc . incrementalPatch ( newValue )" + }, + { + "id": "rxdb.ts:M166", + "number": 166, + "source": "src/rxdb.ts", + "line": 370, + "position": 12359, + "operator": "field-drop", + "original": "onDelete: async (params) => {\n debug(`delete`, params)\n const mutations = params.transaction.mutations.filter(\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n (m) => m.type === `delete`,\n )\n const ids = mutations.map((mutation) => getKey(mutation.original))\n const result = await rxCollection.bulkRemove(ids)\n if (result.error.length > 0) {\n throw result.error\n }\n }", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }" + }, + { + "id": "rxdb.ts:M167", + "number": 167, + "source": "src/rxdb.ts", + "line": 371, + "position": 12395, + "operator": "delete-call", + "original": "debug(`delete`, params)", + "replacement": ";", + "anchor": "debug ( `delete` , params )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:delete-call:debug ( `delete` , params )" + }, + { + "id": "rxdb.ts:M168", + "number": 168, + "source": "src/rxdb.ts", + "line": 372, + "position": 12443, + "operator": "value-undefined", + "original": "params.transaction.mutations.filter(\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n (m) => m.type === `delete`,\n )", + "replacement": "undefined", + "anchor": "const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:value-undefined:const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , )" + }, + { + "id": "rxdb.ts:M169", + "number": 169, + "source": "src/rxdb.ts", + "line": 374, + "position": 12582, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:binary-swap:const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , )" + }, + { + "id": "rxdb.ts:M170", + "number": 170, + "source": "src/rxdb.ts", + "line": 376, + "position": 12622, + "operator": "value-undefined", + "original": "mutations.map((mutation) => getKey(mutation.original))", + "replacement": "undefined", + "anchor": "const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:value-undefined:const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) )" + }, + { + "id": "rxdb.ts:M171", + "number": 171, + "source": "src/rxdb.ts", + "line": 376, + "position": 12650, + "operator": "value-undefined", + "original": "getKey(mutation.original)", + "replacement": "undefined", + "anchor": "const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:value-undefined:const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) )~2" + }, + { + "id": "rxdb.ts:M172", + "number": 172, + "source": "src/rxdb.ts", + "line": 377, + "position": 12704, + "operator": "value-undefined", + "original": "rxCollection.bulkRemove(ids)", + "replacement": "undefined", + "anchor": "const result = await rxCollection . bulkRemove ( ids )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:value-undefined:const result = await rxCollection . bulkRemove ( ids )" + }, + { + "id": "rxdb.ts:M173", + "number": 173, + "source": "src/rxdb.ts", + "line": 378, + "position": 12743, + "operator": "if-true", + "original": "result.error.length > 0", + "replacement": "true", + "anchor": "if ( result . error . length > 0 ) { throw result . error }", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:if-true:if ( result . error . length > 0 ) { throw result . error }" + }, + { + "id": "rxdb.ts:M174", + "number": 174, + "source": "src/rxdb.ts", + "line": 378, + "position": 12743, + "operator": "if-false", + "original": "result.error.length > 0", + "replacement": "false", + "anchor": "if ( result . error . length > 0 ) { throw result . error }", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:if-false:if ( result . error . length > 0 ) { throw result . error }" + }, + { + "id": "rxdb.ts:M175", + "number": 175, + "source": "src/rxdb.ts", + "line": 378, + "position": 12763, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( result . error . length > 0 ) { throw result . error }", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:binary-swap:if ( result . error . length > 0 ) { throw result . error }" + }, + { + "id": "rxdb.ts:M176", + "number": 176, + "source": "src/rxdb.ts", + "line": 383, + "position": 12825, + "operator": "value-undefined", + "original": "withCollectionConfigFactory(collectionConfig, () =>\n rxdbCollectionOptions(config),\n )", + "replacement": "undefined", + "anchor": "return withCollectionConfigFactory ( collectionConfig , ( ) => rxdbCollectionOptions ( config ) , )", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:value-undefined:return withCollectionConfigFactory ( collectionConfig , ( ) => rxdbCollectionOptions ( config ) , )" + }, + { + "id": "rxdb.ts:M177", + "number": 177, + "source": "src/rxdb.ts", + "line": 384, + "position": 12881, + "operator": "value-undefined", + "original": "rxdbCollectionOptions(config)", + "replacement": "undefined", + "anchor": "return withCollectionConfigFactory ( collectionConfig , ( ) => rxdbCollectionOptions ( config ) , )", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:value-undefined:return withCollectionConfigFactory ( collectionConfig , ( ) => rxdbCollectionOptions ( config ) , )~2" + } + ], + "selected": [ + { + "id": "rxdb.ts:M5", + "number": 5, + "source": "src/rxdb.ts", + "line": 139, + "position": 4561, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "key": "src/rxdb.ts#sync:binary-swap:const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "span": { + "start": 4561, + "end": 4563 + }, + "execution": { + "id": "rxdb.ts:M5", + "number": 5, + "source": "src/rxdb.ts", + "line": 139, + "position": 4561, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 21.871163625 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M6", + "number": 6, + "source": "src/rxdb.ts", + "line": 140, + "position": 4572, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "key": "src/rxdb.ts#sync:drop-not:const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "span": { + "start": 4572, + "end": 4573 + }, + "execution": { + "id": "rxdb.ts:M6", + "number": 6, + "source": "src/rxdb.ts", + "line": 140, + "position": 4572, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done", + "function": "sync", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 3.798943868714286, + "changedTests": [], + "class": "no-observed-difference" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M8", + "number": 8, + "source": "src/rxdb.ts", + "line": 153, + "position": 5225, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "span": { + "start": 5225, + "end": 5227 + }, + "execution": { + "id": "rxdb.ts:M8", + "number": 8, + "source": "src/rxdb.ts", + "line": 153, + "position": 5225, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 3.828311327714286, + "changedTests": [], + "class": "no-observed-difference" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M9", + "number": 9, + "source": "src/rxdb.ts", + "line": 153, + "position": 5228, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:drop-not:const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "span": { + "start": 5228, + "end": 5229 + }, + "execution": { + "id": "rxdb.ts:M9", + "number": 9, + "source": "src/rxdb.ts", + "line": 153, + "position": 5228, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > fixed seed 2367" + ], + "laws": [ + "persisted startup" + ], + "randomAlso": false, + "seconds": 20.827343334000002 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M10", + "number": 10, + "source": "src/rxdb.ts", + "line": 154, + "position": 5290, + "operator": "delete-call", + "original": "begin({\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n })", + "replacement": ";", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "span": { + "start": 5290, + "end": 5470 + }, + "execution": { + "id": "rxdb.ts:M10", + "number": 10, + "source": "src/rxdb.ts", + "line": 154, + "position": 5290, + "operator": "delete-call", + "original": "begin({\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n })", + "replacement": ";", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 35.395527626 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M11", + "number": 11, + "source": "src/rxdb.ts", + "line": 155, + "position": 5308, + "operator": "field-drop", + "original": "snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows", + "replacement": "", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "span": { + "start": 5308, + "end": 5458 + }, + "execution": { + "id": "rxdb.ts:M11", + "number": 11, + "source": "src/rxdb.ts", + "line": 155, + "position": 5308, + "operator": "field-drop", + "original": "snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows", + "replacement": "", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 2.3288427090000003, + "changedTests": [], + "class": "no-observed-difference" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M12", + "number": 12, + "source": "src/rxdb.ts", + "line": 156, + "position": 5342, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "span": { + "start": 5342, + "end": 5345 + }, + "execution": { + "id": "rxdb.ts:M12", + "number": 12, + "source": "src/rxdb.ts", + "line": 156, + "position": 5342, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 2.2701970410000003, + "changedTests": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts > adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts > adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > random seed", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts > adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > determinism" + ], + "class": "comparison-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M13", + "number": 13, + "source": "src/rxdb.ts", + "line": 157, + "position": 5367, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:drop-not:begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "span": { + "start": 5367, + "end": 5368 + }, + "execution": { + "id": "rxdb.ts:M13", + "number": 13, + "source": "src/rxdb.ts", + "line": 157, + "position": 5367, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } )", + "function": "initialFetch", + "oracleFiles": [], + "ordinaryFiles": [], + "outcome": "unreached" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M43", + "number": 43, + "source": "src/rxdb.ts", + "line": 201, + "position": 6918, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( isStopped ( ) ) return", + "span": { + "start": 6918, + "end": 6929 + }, + "execution": { + "id": "rxdb.ts:M43", + "number": 43, + "source": "src/rxdb.ts", + "line": 201, + "position": 6918, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 35.395527626 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M44", + "number": 44, + "source": "src/rxdb.ts", + "line": 201, + "position": 6918, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( isStopped ( ) ) return", + "span": { + "start": 6918, + "end": 6929 + }, + "execution": { + "id": "rxdb.ts:M44", + "number": 44, + "source": "src/rxdb.ts", + "line": 201, + "position": 6918, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 0, + "changedTests": [], + "class": "generator-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M45", + "number": 45, + "source": "src/rxdb.ts", + "line": 201, + "position": 6918, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:if ( isStopped ( ) ) return", + "span": { + "start": 6918, + "end": 6929 + }, + "execution": { + "id": "rxdb.ts:M45", + "number": 45, + "source": "src/rxdb.ts", + "line": 201, + "position": 6918, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 2.007013292, + "changedTests": [], + "class": "generator-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M51", + "number": 51, + "source": "src/rxdb.ts", + "line": 210, + "position": 7132, + "operator": "delete-call", + "original": "docs.forEach((d) => {\n listed?.add(getKey(d))\n write({\n type: `insert`,\n value: stripRxdbFields(clone(d)),\n })\n })", + "replacement": ";", + "anchor": "docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } )", + "span": { + "start": 7132, + "end": 7314 + }, + "execution": { + "id": "rxdb.ts:M51", + "number": 51, + "source": "src/rxdb.ts", + "line": 210, + "position": 7132, + "operator": "delete-call", + "original": "docs.forEach((d) => {\n listed?.add(getKey(d))\n write({\n type: `insert`,\n value: stripRxdbFields(clone(d)),\n })\n })", + "replacement": ";", + "anchor": "docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.915 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M52", + "number": 52, + "source": "src/rxdb.ts", + "line": 211, + "position": 7166, + "operator": "delete-call", + "original": "listed?.add(getKey(d))", + "replacement": ";", + "anchor": "listed ?. add ( getKey ( d ) )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:listed ?. add ( getKey ( d ) )", + "span": { + "start": 7166, + "end": 7188 + }, + "execution": { + "id": "rxdb.ts:M52", + "number": 52, + "source": "src/rxdb.ts", + "line": 211, + "position": 7166, + "operator": "delete-call", + "original": "listed?.add(getKey(d))", + "replacement": ";", + "anchor": "listed ?. add ( getKey ( d ) )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 2.427 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M53", + "number": 53, + "source": "src/rxdb.ts", + "line": 211, + "position": 7178, + "operator": "value-undefined", + "original": "getKey(d)", + "replacement": "undefined", + "anchor": "listed ?. add ( getKey ( d ) )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:listed ?. add ( getKey ( d ) )", + "span": { + "start": 7178, + "end": 7187 + }, + "execution": { + "id": "rxdb.ts:M53", + "number": 53, + "source": "src/rxdb.ts", + "line": 211, + "position": 7178, + "operator": "value-undefined", + "original": "getKey(d)", + "replacement": "undefined", + "anchor": "listed ?. add ( getKey ( d ) )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 2.427 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M59", + "number": 59, + "source": "src/rxdb.ts", + "line": 218, + "position": 7354, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const firstApplied = commit ( )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:const firstApplied = commit ( )", + "span": { + "start": 7354, + "end": 7362 + }, + "execution": { + "id": "rxdb.ts:M59", + "number": 59, + "source": "src/rxdb.ts", + "line": 218, + "position": 7354, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const firstApplied = commit ( )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 35.395527626 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M60", + "number": 60, + "source": "src/rxdb.ts", + "line": 219, + "position": 7375, + "operator": "if-true", + "original": "firstApplied !== true", + "replacement": "true", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "span": { + "start": 7375, + "end": 7396 + }, + "execution": { + "id": "rxdb.ts:M60", + "number": 60, + "source": "src/rxdb.ts", + "line": 219, + "position": 7375, + "operator": "if-true", + "original": "firstApplied !== true", + "replacement": "true", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 0, + "changedTests": [], + "class": "generator-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M61", + "number": 61, + "source": "src/rxdb.ts", + "line": 219, + "position": 7375, + "operator": "if-false", + "original": "firstApplied !== true", + "replacement": "false", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "span": { + "start": 7375, + "end": 7396 + }, + "execution": { + "id": "rxdb.ts:M61", + "number": 61, + "source": "src/rxdb.ts", + "line": 219, + "position": 7375, + "operator": "if-false", + "original": "firstApplied !== true", + "replacement": "false", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.19909912500000018 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M62", + "number": 62, + "source": "src/rxdb.ts", + "line": 219, + "position": 7388, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "span": { + "start": 7388, + "end": 7391 + }, + "execution": { + "id": "rxdb.ts:M62", + "number": 62, + "source": "src/rxdb.ts", + "line": 219, + "position": 7388, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.177414 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M63", + "number": 63, + "source": "src/rxdb.ts", + "line": 219, + "position": 7392, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "span": { + "start": 7392, + "end": 7396 + }, + "execution": { + "id": "rxdb.ts:M63", + "number": 63, + "source": "src/rxdb.ts", + "line": 219, + "position": 7392, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 3.647919417, + "changedTests": [], + "class": "generator-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M64", + "number": 64, + "source": "src/rxdb.ts", + "line": 219, + "position": 7403, + "operator": "value-undefined", + "original": "firstApplied.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:void firstApplied . catch ( ( ) => undefined )", + "span": { + "start": 7403, + "end": 7438 + }, + "execution": { + "id": "rxdb.ts:M64", + "number": 64, + "source": "src/rxdb.ts", + "line": 219, + "position": 7403, + "operator": "value-undefined", + "original": "firstApplied.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void firstApplied . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.17786366599999973 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M65", + "number": 65, + "source": "src/rxdb.ts", + "line": 221, + "position": 7519, + "operator": "if-true", + "original": "listed && persistence", + "replacement": "true", + "anchor": "if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "span": { + "start": 7519, + "end": 7540 + }, + "execution": { + "id": "rxdb.ts:M65", + "number": 65, + "source": "src/rxdb.ts", + "line": 221, + "position": 7519, + "operator": "if-true", + "original": "listed && persistence", + "replacement": "true", + "anchor": "if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 3.151502458, + "changedTests": [], + "class": "no-observed-difference" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M66", + "number": 66, + "source": "src/rxdb.ts", + "line": 221, + "position": 7519, + "operator": "if-false", + "original": "listed && persistence", + "replacement": "false", + "anchor": "if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "span": { + "start": 7519, + "end": 7540 + }, + "execution": { + "id": "rxdb.ts:M66", + "number": 66, + "source": "src/rxdb.ts", + "line": 221, + "position": 7519, + "operator": "if-false", + "original": "listed && persistence", + "replacement": "false", + "anchor": "if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > fixed seed 2367" + ], + "laws": [ + "persisted startup" + ], + "randomAlso": false, + "seconds": 20.820981459000002 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M67", + "number": 67, + "source": "src/rxdb.ts", + "line": 221, + "position": 7526, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "span": { + "start": 7526, + "end": 7528 + }, + "execution": { + "id": "rxdb.ts:M67", + "number": 67, + "source": "src/rxdb.ts", + "line": 221, + "position": 7526, + "operator": "binary-swap", + "original": "&&", + "replacement": "||", + "anchor": "if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } }", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 3.9573318743333337, + "changedTests": [], + "class": "no-observed-difference" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M68", + "number": 68, + "source": "src/rxdb.ts", + "line": 222, + "position": 7560, + "operator": "value-undefined", + "original": "persistence.hydrateBaseline()", + "replacement": "undefined", + "anchor": "await persistence . hydrateBaseline ( )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:await persistence . hydrateBaseline ( )", + "span": { + "start": 7560, + "end": 7589 + }, + "execution": { + "id": "rxdb.ts:M68", + "number": 68, + "source": "src/rxdb.ts", + "line": 222, + "position": 7560, + "operator": "value-undefined", + "original": "persistence.hydrateBaseline()", + "replacement": "undefined", + "anchor": "await persistence . hydrateBaseline ( )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > fixed seed 2367" + ], + "laws": [ + "persisted startup" + ], + "randomAlso": false, + "seconds": 20.826412458000004 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M69", + "number": 69, + "source": "src/rxdb.ts", + "line": 223, + "position": 7604, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( isStopped ( ) ) return~2", + "span": { + "start": 7604, + "end": 7615 + }, + "execution": { + "id": "rxdb.ts:M69", + "number": 69, + "source": "src/rxdb.ts", + "line": 223, + "position": 7604, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 1.90475 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M70", + "number": 70, + "source": "src/rxdb.ts", + "line": 223, + "position": 7604, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( isStopped ( ) ) return~2", + "span": { + "start": 7604, + "end": 7615 + }, + "execution": { + "id": "rxdb.ts:M70", + "number": 70, + "source": "src/rxdb.ts", + "line": 223, + "position": 7604, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 1.90475 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M71", + "number": 71, + "source": "src/rxdb.ts", + "line": 223, + "position": 7604, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:if ( isStopped ( ) ) return~2", + "span": { + "start": 7604, + "end": 7615 + }, + "execution": { + "id": "rxdb.ts:M71", + "number": 71, + "source": "src/rxdb.ts", + "line": 223, + "position": 7604, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) return", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 1.90475 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M72", + "number": 72, + "source": "src/rxdb.ts", + "line": 228, + "position": 7913, + "operator": "value-undefined", + "original": "collection._state.acceptedSyncedEntries()", + "replacement": "undefined", + "anchor": "for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) }", + "span": { + "start": 7913, + "end": 7954 + }, + "execution": { + "id": "rxdb.ts:M72", + "number": 72, + "source": "src/rxdb.ts", + "line": 228, + "position": 7913, + "operator": "value-undefined", + "original": "collection._state.acceptedSyncedEntries()", + "replacement": "undefined", + "anchor": "for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) }", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 35.395527626 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M73", + "number": 73, + "source": "src/rxdb.ts", + "line": 229, + "position": 7974, + "operator": "if-true", + "original": "!listed.has(key)", + "replacement": "true", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( ! listed . has ( key ) ) gone . push ( key )", + "span": { + "start": 7974, + "end": 7990 + }, + "execution": { + "id": "rxdb.ts:M73", + "number": 73, + "source": "src/rxdb.ts", + "line": 229, + "position": 7974, + "operator": "if-true", + "original": "!listed.has(key)", + "replacement": "true", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.3125791250000002 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M74", + "number": 74, + "source": "src/rxdb.ts", + "line": 229, + "position": 7974, + "operator": "if-false", + "original": "!listed.has(key)", + "replacement": "false", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( ! listed . has ( key ) ) gone . push ( key )", + "span": { + "start": 7974, + "end": 7990 + }, + "execution": { + "id": "rxdb.ts:M74", + "number": 74, + "source": "src/rxdb.ts", + "line": 229, + "position": 7974, + "operator": "if-false", + "original": "!listed.has(key)", + "replacement": "false", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.2577511670000001 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M75", + "number": 75, + "source": "src/rxdb.ts", + "line": 229, + "position": 7974, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:drop-not:if ( ! listed . has ( key ) ) gone . push ( key )", + "span": { + "start": 7974, + "end": 7975 + }, + "execution": { + "id": "rxdb.ts:M75", + "number": 75, + "source": "src/rxdb.ts", + "line": 229, + "position": 7974, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.2760983750000003 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M76", + "number": 76, + "source": "src/rxdb.ts", + "line": 229, + "position": 7975, + "operator": "value-undefined", + "original": "listed.has(key)", + "replacement": "undefined", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:if ( ! listed . has ( key ) ) gone . push ( key )", + "span": { + "start": 7975, + "end": 7990 + }, + "execution": { + "id": "rxdb.ts:M76", + "number": 76, + "source": "src/rxdb.ts", + "line": 229, + "position": 7975, + "operator": "value-undefined", + "original": "listed.has(key)", + "replacement": "undefined", + "anchor": "if ( ! listed . has ( key ) ) gone . push ( key )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.28132350000000045 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M77", + "number": 77, + "source": "src/rxdb.ts", + "line": 229, + "position": 7992, + "operator": "delete-call", + "original": "gone.push(key)", + "replacement": ";", + "anchor": "gone . push ( key )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:gone . push ( key )", + "span": { + "start": 7992, + "end": 8006 + }, + "execution": { + "id": "rxdb.ts:M77", + "number": 77, + "source": "src/rxdb.ts", + "line": 229, + "position": 7992, + "operator": "delete-call", + "original": "gone.push(key)", + "replacement": ";", + "anchor": "gone . push ( key )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.80825 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M78", + "number": 78, + "source": "src/rxdb.ts", + "line": 231, + "position": 8049, + "operator": "loop-false", + "original": "first < gone.length", + "replacement": "false", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:loop-false:for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }", + "span": { + "start": 8049, + "end": 8068 + }, + "execution": { + "id": "rxdb.ts:M78", + "number": 78, + "source": "src/rxdb.ts", + "line": 231, + "position": 8049, + "operator": "loop-false", + "original": "first < gone.length", + "replacement": "false", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.80825 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M79", + "number": 79, + "source": "src/rxdb.ts", + "line": 231, + "position": 8055, + "operator": "binary-swap", + "original": "<", + "replacement": "<=", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }", + "span": { + "start": 8055, + "end": 8056 + }, + "execution": { + "id": "rxdb.ts:M79", + "number": 79, + "source": "src/rxdb.ts", + "line": 231, + "position": 8055, + "operator": "binary-swap", + "original": "<", + "replacement": "<=", + "anchor": "for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) }", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 1.6165, + "changedTests": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts > adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts > adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > random seed", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts > adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > determinism", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts > adapter-conformance.persisted-baseline-lifecycle.rxdb.node-sqlite > fixed seed 2367", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts > adapter-conformance.persisted-baseline-lifecycle.rxdb.node-sqlite > random seed", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts > adapter-conformance.persisted-baseline-lifecycle.rxdb.node-sqlite > determinism", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts > persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > fixed seed 2367", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts > persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > random seed", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts > persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > determinism", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts > persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through node-sqlite > adapter.rxdb.persisted-startup > fixed seed 2367", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts > persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through node-sqlite > adapter.rxdb.persisted-startup > random seed", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts > persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through node-sqlite > adapter.rxdb.persisted-startup > determinism" + ], + "class": "comparison-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M80", + "number": 80, + "source": "src/rxdb.ts", + "line": 232, + "position": 8108, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:begin ( )", + "span": { + "start": 8108, + "end": 8115 + }, + "execution": { + "id": "rxdb.ts:M80", + "number": 80, + "source": "src/rxdb.ts", + "line": 232, + "position": 8108, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 15.471536750999999 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M81", + "number": 81, + "source": "src/rxdb.ts", + "line": 233, + "position": 8146, + "operator": "value-undefined", + "original": "gone.slice(first, first + syncBatchSize)", + "replacement": "undefined", + "anchor": "for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } )", + "span": { + "start": 8146, + "end": 8186 + }, + "execution": { + "id": "rxdb.ts:M81", + "number": 81, + "source": "src/rxdb.ts", + "line": 233, + "position": 8146, + "operator": "value-undefined", + "original": "gone.slice(first, first + syncBatchSize)", + "replacement": "undefined", + "anchor": "for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 15.558911751 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M82", + "number": 82, + "source": "src/rxdb.ts", + "line": 234, + "position": 8202, + "operator": "delete-call", + "original": "write({ type: `delete`, key })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:write ( { type : `delete` , key } )", + "span": { + "start": 8202, + "end": 8232 + }, + "execution": { + "id": "rxdb.ts:M82", + "number": 82, + "source": "src/rxdb.ts", + "line": 234, + "position": 8202, + "operator": "delete-call", + "original": "write({ type: `delete`, key })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key } )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.895625 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M83", + "number": 83, + "source": "src/rxdb.ts", + "line": 234, + "position": 8210, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:write ( { type : `delete` , key } )", + "span": { + "start": 8210, + "end": 8224 + }, + "execution": { + "id": "rxdb.ts:M83", + "number": 83, + "source": "src/rxdb.ts", + "line": 234, + "position": 8210, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > fixed seed 2367" + ], + "laws": [ + "startup batching" + ], + "randomAlso": false, + "seconds": 1.782625 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M84", + "number": 84, + "source": "src/rxdb.ts", + "line": 234, + "position": 8226, + "operator": "field-drop", + "original": "key", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:write ( { type : `delete` , key } )~2", + "span": { + "start": 8226, + "end": 8229 + }, + "execution": { + "id": "rxdb.ts:M84", + "number": 84, + "source": "src/rxdb.ts", + "line": 234, + "position": 8226, + "operator": "field-drop", + "original": "key", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 15.558911751 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M85", + "number": 85, + "source": "src/rxdb.ts", + "line": 235, + "position": 8261, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const receipt = commit ( )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:const receipt = commit ( )", + "span": { + "start": 8261, + "end": 8269 + }, + "execution": { + "id": "rxdb.ts:M85", + "number": 85, + "source": "src/rxdb.ts", + "line": 235, + "position": 8261, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "const receipt = commit ( )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "timeout", + "detail": "run time limit", + "seconds": 15.959953417666666 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M86", + "number": 86, + "source": "src/rxdb.ts", + "line": 236, + "position": 8286, + "operator": "if-true", + "original": "receipt !== true", + "replacement": "true", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "span": { + "start": 8286, + "end": 8302 + }, + "execution": { + "id": "rxdb.ts:M86", + "number": 86, + "source": "src/rxdb.ts", + "line": 236, + "position": 8286, + "operator": "if-true", + "original": "receipt !== true", + "replacement": "true", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 0, + "changedTests": [], + "class": "generator-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M87", + "number": 87, + "source": "src/rxdb.ts", + "line": 236, + "position": 8286, + "operator": "if-false", + "original": "receipt !== true", + "replacement": "false", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "span": { + "start": 8286, + "end": 8302 + }, + "execution": { + "id": "rxdb.ts:M87", + "number": 87, + "source": "src/rxdb.ts", + "line": 236, + "position": 8286, + "operator": "if-false", + "original": "receipt !== true", + "replacement": "false", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 2.3355000416666667, + "changedTests": [], + "class": "generator-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M88", + "number": 88, + "source": "src/rxdb.ts", + "line": 236, + "position": 8294, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "span": { + "start": 8294, + "end": 8297 + }, + "execution": { + "id": "rxdb.ts:M88", + "number": 88, + "source": "src/rxdb.ts", + "line": 236, + "position": 8294, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 2.2620934586666666, + "changedTests": [], + "class": "generator-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M89", + "number": 89, + "source": "src/rxdb.ts", + "line": 236, + "position": 8298, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "span": { + "start": 8298, + "end": 8302 + }, + "execution": { + "id": "rxdb.ts:M89", + "number": 89, + "source": "src/rxdb.ts", + "line": 236, + "position": 8298, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "if ( receipt !== true ) void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 3.2397340829999997, + "changedTests": [], + "class": "generator-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M90", + "number": 90, + "source": "src/rxdb.ts", + "line": 236, + "position": 8309, + "operator": "value-undefined", + "original": "receipt.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:void receipt . catch ( ( ) => undefined )", + "span": { + "start": 8309, + "end": 8339 + }, + "execution": { + "id": "rxdb.ts:M90", + "number": 90, + "source": "src/rxdb.ts", + "line": 236, + "position": 8309, + "operator": "value-undefined", + "original": "receipt.catch(() => undefined)", + "replacement": "undefined", + "anchor": "void receipt . catch ( ( ) => undefined )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "survived", + "seconds": 3.1878025, + "changedTests": [], + "class": "generator-gap" + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M91", + "number": 91, + "source": "src/rxdb.ts", + "line": 237, + "position": 8352, + "operator": "delete-call", + "original": "applied.push(receipt)", + "replacement": ";", + "anchor": "applied . push ( receipt )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:applied . push ( receipt )", + "span": { + "start": 8352, + "end": 8373 + }, + "execution": { + "id": "rxdb.ts:M91", + "number": 91, + "source": "src/rxdb.ts", + "line": 237, + "position": 8352, + "operator": "delete-call", + "original": "applied.push(receipt)", + "replacement": ";", + "anchor": "applied . push ( receipt )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through node-sqlite > adapter.rxdb.persisted-startup > fixed seed 2367" + ], + "laws": [ + "persisted startup" + ], + "randomAlso": false, + "seconds": 2.843399125000001 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M92", + "number": 92, + "source": "src/rxdb.ts", + "line": 240, + "position": 8410, + "operator": "value-undefined", + "original": "Promise.all(applied)", + "replacement": "undefined", + "anchor": "await Promise . all ( applied )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:await Promise . all ( applied )", + "span": { + "start": 8410, + "end": 8430 + }, + "execution": { + "id": "rxdb.ts:M92", + "number": 92, + "source": "src/rxdb.ts", + "line": 240, + "position": 8410, + "operator": "value-undefined", + "original": "Promise.all(applied)", + "replacement": "undefined", + "anchor": "await Promise . all ( applied )", + "function": "initialFetch", + "oracleFiles": [ + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts" + ], + "ordinaryFiles": [], + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 0.1669806669999998 + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M100", + "number": 100, + "source": "src/rxdb.ts", + "line": 257, + "position": 8833, + "operator": "binary-swap", + "original": "!==", + "replacement": "===", + "anchor": "const isStopped = ( ) => sub ?. closed !== false", + "function": "isStopped", + "key": "src/rxdb.ts#isStopped:binary-swap:const isStopped = ( ) => sub ?. closed !== false", + "span": { + "start": 8833, + "end": 8836 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M101", + "number": 101, + "source": "src/rxdb.ts", + "line": 257, + "position": 8837, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "const isStopped = ( ) => sub ?. closed !== false", + "function": "isStopped", + "key": "src/rxdb.ts#isStopped:bool-flip:const isStopped = ( ) => sub ?. closed !== false", + "span": { + "start": 8837, + "end": 8842 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M121", + "number": 121, + "source": "src/rxdb.ts", + "line": 302, + "position": 10246, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) return", + "function": "start", + "key": "src/rxdb.ts#start:if-true:if ( isStopped ( ) ) return", + "span": { + "start": 10246, + "end": 10257 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M122", + "number": 122, + "source": "src/rxdb.ts", + "line": 302, + "position": 10246, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) return", + "function": "start", + "key": "src/rxdb.ts#start:if-false:if ( isStopped ( ) ) return", + "span": { + "start": 10246, + "end": 10257 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M123", + "number": 123, + "source": "src/rxdb.ts", + "line": 302, + "position": 10246, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) return", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:if ( isStopped ( ) ) return", + "span": { + "start": 10246, + "end": 10257 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M125", + "number": 125, + "source": "src/rxdb.ts", + "line": 304, + "position": 10307, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:if-true:if ( isStopped ( ) ) { return }", + "span": { + "start": 10307, + "end": 10318 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M126", + "number": 126, + "source": "src/rxdb.ts", + "line": 304, + "position": 10307, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:if-false:if ( isStopped ( ) ) { return }", + "span": { + "start": 10307, + "end": 10318 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M127", + "number": 127, + "source": "src/rxdb.ts", + "line": 304, + "position": 10307, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:if ( isStopped ( ) ) { return }", + "span": { + "start": 10307, + "end": 10318 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M136", + "number": 136, + "source": "src/rxdb.ts", + "line": 318, + "position": 10844, + "operator": "if-true", + "original": "isStopped()", + "replacement": "true", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:if-true:if ( isStopped ( ) ) { return }~2", + "span": { + "start": 10844, + "end": 10855 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M137", + "number": 137, + "source": "src/rxdb.ts", + "line": 318, + "position": 10844, + "operator": "if-false", + "original": "isStopped()", + "replacement": "false", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:if-false:if ( isStopped ( ) ) { return }~2", + "span": { + "start": 10844, + "end": 10855 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + }, + { + "id": "rxdb.ts:M138", + "number": 138, + "source": "src/rxdb.ts", + "line": 318, + "position": 10844, + "operator": "value-undefined", + "original": "isStopped()", + "replacement": "undefined", + "anchor": "if ( isStopped ( ) ) { return }", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:if ( isStopped ( ) ) { return }~2", + "span": { + "start": 10844, + "end": 10855 + }, + "execution": { + "outcome": "unrun-or-interrupted", + "reason": "Parent stopped the supplementary sweep; no final judgment was emitted for this operator." + }, + "disposition": "No allowance granted; runtime failures, unrun operators and surviving distinctions remain separate from kills." + } + ], + "omitted": [ + { + "operator": { + "id": "rxdb.ts:M1", + "number": 1, + "source": "src/rxdb.ts", + "line": 30, + "position": 651, + "operator": "value-undefined", + "original": "DebugModule.debug(`ts/db:rxdb`)", + "replacement": "undefined", + "anchor": "const debug = DebugModule . debug ( `ts/db:rxdb` )", + "function": "", + "key": "src/rxdb.ts#:value-undefined:const debug = DebugModule . debug ( `ts/db:rxdb` )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M2", + "number": 2, + "source": "src/rxdb.ts", + "line": 131, + "position": 4226, + "operator": "field-drop", + "original": "sync: (params: SyncParams) => {\n const { begin, write, commit, markReady, fail, collection } = params\n const persistence =\n params.metadata === undefined\n ? null\n : validateSyncPersistenceCapability(params.metadata.persistence)\n\n const replacesRetainedRows =\n collection.health.reloading &&\n !collection._state.acceptedSyncedEntries().next().done\n let initialFetchComplete = false\n async function initialFetch() {\n /**\n * RxDB stores a last-write-time\n * which can be used to \"sort\" document writes,\n * so for initial sync we iterate over that.\n */\n let cursor: RxDocumentData | undefined = undefined\n const syncBatchSize = config.syncBatchSize ? config.syncBatchSize : 1000\n // Retained reloads replace atomically. First persisted startup keeps\n // its one ordinary source commit and deletes only restored omissions.\n const listed =\n persistence && !replacesRetainedRows ? new Set() : undefined\n begin({\n snapshot:\n persistence === null\n ? !collection._state.acceptedSyncedEntries().next().done\n : replacesRetainedRows,\n })\n\n while (!initialFetchComplete) {\n let query: FilledMangoQuery\n if (cursor) {\n query = {\n selector: {\n $or: [\n { '_meta.lwt': { $gt: cursor._meta.lwt } },\n {\n '_meta.lwt': cursor._meta.lwt,\n [primaryPath]: {\n $gt: getKey(cursor),\n },\n },\n ],\n _deleted: false,\n },\n sort: [{ '_meta.lwt': `asc` }, { [primaryPath]: `asc` }],\n limit: syncBatchSize,\n skip: 0,\n }\n } else {\n query = {\n selector: { _deleted: false },\n sort: [{ '_meta.lwt': `asc` }, { [primaryPath]: `asc` }],\n limit: syncBatchSize,\n skip: 0,\n }\n }\n\n /**\n * Instead of doing a RxCollection.query(),\n * we directly query the storage engine of the RxCollection so we do not use the\n * RxCollection document cache because it likely wont be used anyway\n * since most queries will run directly on the tanstack-db side.\n */\n const preparedQuery = prepareQuery(\n rxCollection.storageInstance.schema,\n query,\n )\n const result = await rxCollection.storageInstance.query(preparedQuery)\n if (isStopped()) return\n const docs = result.documents\n\n cursor = lastOfArray(docs)\n if (docs.length === 0) {\n initialFetchComplete = true\n break\n }\n\n docs.forEach((d) => {\n listed?.add(getKey(d))\n write({\n type: `insert`,\n value: stripRxdbFields(clone(d)),\n })\n })\n }\n const firstApplied = commit()\n if (firstApplied !== true) void firstApplied.catch(() => undefined)\n const applied: Array> = [firstApplied]\n if (listed && persistence) {\n await persistence.hydrateBaseline()\n if (isStopped()) return\n // Copy omitted keys before committing: synchronous application can\n // change the accepted-row iterator's backing map. Buffered RxDB live\n // changes still apply after this initial result.\n const gone: Array = []\n for (const [key] of collection._state.acceptedSyncedEntries()) {\n if (!listed.has(key)) gone.push(key)\n }\n for (let first = 0; first < gone.length; first += syncBatchSize) {\n begin()\n for (const key of gone.slice(first, first + syncBatchSize))\n write({ type: `delete`, key })\n const receipt = commit()\n if (receipt !== true) void receipt.catch(() => undefined)\n applied.push(receipt)\n }\n }\n await Promise.all(applied)\n }\n\n type WriteMessage = Parameters[0]\n const buffer: Array = []\n let buffering = true\n const queue = (msg: WriteMessage) => {\n if (buffering) {\n buffer.push(msg)\n return\n }\n begin()\n write(msg)\n commit()\n }\n\n let sub: Subscription | undefined\n const isStopped = () => sub?.closed !== false\n function stopOngoingFetch() {\n buffer.length = 0\n if (!sub) return\n getFromMapOrCreate(\n OPEN_RXDB_SUBSCRIPTIONS,\n rxCollection,\n () => new Set(),\n ).delete(sub)\n sub.unsubscribe()\n sub = undefined\n }\n\n function startOngoingFetch() {\n // Subscribe early and buffer live changes during initial load and ongoing\n sub = rxCollection.$.subscribe({\n next: (ev) => {\n const cur: Row = stripRxdbFields(clone(ev.documentData as Row))\n switch (ev.operation) {\n // The initial read can already hold a document whose insert\n // event was buffered, so an insert writes as an update. Core\n // inserts an updated key it does not hold.\n case `INSERT`:\n case `UPDATE`:\n queue({ type: `update`, value: cur })\n break\n case `DELETE`:\n queue({ type: `delete`, value: cur })\n break\n }\n },\n // An errored stream ends, so later writes would never arrive.\n error: stop,\n })\n\n const subs = getFromMapOrCreate(\n OPEN_RXDB_SUBSCRIPTIONS,\n rxCollection,\n () => new Set(),\n )\n subs.add(sub)\n }\n\n async function start() {\n startOngoingFetch()\n if (isStopped()) return\n await initialFetch()\n if (isStopped()) {\n return\n }\n\n // Take one finite snapshot of changes observed during the initial\n // fetch, then route newer events through the normal live path. The\n // core transaction queue preserves their order without letting a\n // continuous event stream postpone readiness forever.\n const pending = buffer.splice(0)\n buffering = false\n if (pending.length > 0) {\n begin()\n for (const msg of pending) write(msg)\n await commit()\n if (isStopped()) {\n return\n }\n }\n\n // After this run's fail(), markReady() does nothing.\n markReady()\n }\n\n // Core ignores a later fail() in the same run.\n function stop(error: unknown) {\n stopOngoingFetch()\n fail(error)\n }\n void start().catch(stop)\n\n return stopOngoingFetch\n }", + "replacement": "", + "anchor": "const sync : SyncConfig < Row , Key > = { sync : ( params : SyncParams ) => { const { begin , write , commit , markReady , fail , collection } = params const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let initialFetchComplete = false async function initialFetch ( ) { let cursor : RxDocumentData < Row > | undefined = undefined const syncBatchSize = config . syncBatchSize ? config . syncBatchSize : 1000 const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } ) while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) } const firstApplied = commit ( ) if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined ) const applied : Array < true | Promise < void >> = [ firstApplied ] if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } } await Promise . all ( applied ) } type WriteMessage = Parameters < typeof write > [ 0 ] const buffer : Array < WriteMessage > = [ ] let buffering = true const queue = ( msg : WriteMessage ) => { if ( buffering ) { buffer . push ( msg ) return } begin ( ) write ( msg ) commit ( ) } let sub : Subscription | undefined const isStopped = ( ) => sub ?. closed !== false function stopOngoingFetch ( ) { buffer . length = 0 if ( ! sub ) return getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub ) sub . unsubscribe ( ) sub = undefined } function startOngoingFetch ( ) { sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } ) const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) subs . add ( sub ) } async function start ( ) { startOngoingFetch ( ) if ( isStopped ( ) ) return await initialFetch ( ) if ( isStopped ( ) ) { return } const pending = buffer . splice ( 0 ) buffering = false if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } } markReady ( ) } function stop ( error : unknown ) { stopOngoingFetch ( ) fail ( error ) } void start ( ) . catch ( stop ) return stopOngoingFetch } , rowUpdateMode : `full` , }", + "function": "sync", + "key": "src/rxdb.ts#sync:field-drop:const sync : SyncConfig < Row , Key > = { sync : ( params : SyncParams ) => { const { begin , write , commit , markReady , fail , collection } = params const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let initialFetchComplete = false async function initialFetch ( ) { let cursor : RxDocumentData < Row > | undefined = undefined const syncBatchSize = config . syncBatchSize ? config . syncBatchSize : 1000 const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } ) while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) } const firstApplied = commit ( ) if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined ) const applied : Array < true | Promise < void >> = [ firstApplied ] if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } } await Promise . all ( applied ) } type WriteMessage = Parameters < typeof write > [ 0 ] const buffer : Array < WriteMessage > = [ ] let buffering = true const queue = ( msg : WriteMessage ) => { if ( buffering ) { buffer . push ( msg ) return } begin ( ) write ( msg ) commit ( ) } let sub : Subscription | undefined const isStopped = ( ) => sub ?. closed !== false function stopOngoingFetch ( ) { buffer . length = 0 if ( ! sub ) return getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub ) sub . unsubscribe ( ) sub = undefined } function startOngoingFetch ( ) { sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } ) const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) subs . add ( sub ) } async function start ( ) { startOngoingFetch ( ) if ( isStopped ( ) ) return await initialFetch ( ) if ( isStopped ( ) ) { return } const pending = buffer . splice ( 0 ) buffering = false if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } } markReady ( ) } function stop ( error : unknown ) { stopOngoingFetch ( ) fail ( error ) } void start ( ) . catch ( stop ) return stopOngoingFetch } , rowUpdateMode : `full` , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M3", + "number": 3, + "source": "src/rxdb.ts", + "line": 134, + "position": 4383, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "function": "sync", + "key": "src/rxdb.ts#sync:binary-swap:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M4", + "number": 4, + "source": "src/rxdb.ts", + "line": 136, + "position": 4426, + "operator": "value-undefined", + "original": "validateSyncPersistenceCapability(params.metadata.persistence)", + "replacement": "undefined", + "anchor": "const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "function": "sync", + "key": "src/rxdb.ts#sync:value-undefined:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M7", + "number": 7, + "source": "src/rxdb.ts", + "line": 141, + "position": 4660, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "let initialFetchComplete = false", + "function": "sync", + "key": "src/rxdb.ts#sync:bool-flip:let initialFetchComplete = false" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M14", + "number": 14, + "source": "src/rxdb.ts", + "line": 161, + "position": 5487, + "operator": "loop-false", + "original": "!initialFetchComplete", + "replacement": "false", + "anchor": "while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:loop-false:while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M15", + "number": 15, + "source": "src/rxdb.ts", + "line": 161, + "position": 5487, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:drop-not:while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M16", + "number": 16, + "source": "src/rxdb.ts", + "line": 163, + "position": 5569, + "operator": "if-true", + "original": "cursor", + "replacement": "true", + "anchor": "if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M17", + "number": 17, + "source": "src/rxdb.ts", + "line": 163, + "position": 5569, + "operator": "if-false", + "original": "cursor", + "replacement": "false", + "anchor": "if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M18", + "number": 18, + "source": "src/rxdb.ts", + "line": 165, + "position": 5615, + "operator": "field-drop", + "original": "selector: {\n $or: [\n { '_meta.lwt': { $gt: cursor._meta.lwt } },\n {\n '_meta.lwt': cursor._meta.lwt,\n [primaryPath]: {\n $gt: getKey(cursor),\n },\n },\n ],\n _deleted: false,\n }", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M19", + "number": 19, + "source": "src/rxdb.ts", + "line": 166, + "position": 5643, + "operator": "field-drop", + "original": "$or: [\n { '_meta.lwt': { $gt: cursor._meta.lwt } },\n {\n '_meta.lwt': cursor._meta.lwt,\n [primaryPath]: {\n $gt: getKey(cursor),\n },\n },\n ]", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M20", + "number": 20, + "source": "src/rxdb.ts", + "line": 167, + "position": 5670, + "operator": "field-drop", + "original": "'_meta.lwt': { $gt: cursor._meta.lwt }", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M21", + "number": 21, + "source": "src/rxdb.ts", + "line": 167, + "position": 5685, + "operator": "field-drop", + "original": "$gt: cursor._meta.lwt", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M22", + "number": 22, + "source": "src/rxdb.ts", + "line": 169, + "position": 5752, + "operator": "field-drop", + "original": "'_meta.lwt': cursor._meta.lwt", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~5" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M23", + "number": 23, + "source": "src/rxdb.ts", + "line": 170, + "position": 5803, + "operator": "field-drop", + "original": "[primaryPath]: {\n $gt: getKey(cursor),\n }", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~6" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M24", + "number": 24, + "source": "src/rxdb.ts", + "line": 171, + "position": 5842, + "operator": "field-drop", + "original": "$gt: getKey(cursor)", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~7" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M25", + "number": 25, + "source": "src/rxdb.ts", + "line": 171, + "position": 5847, + "operator": "value-undefined", + "original": "getKey(cursor)", + "replacement": "undefined", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M26", + "number": 26, + "source": "src/rxdb.ts", + "line": 175, + "position": 5942, + "operator": "field-drop", + "original": "_deleted: false", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~8" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M27", + "number": 27, + "source": "src/rxdb.ts", + "line": 175, + "position": 5952, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M28", + "number": 28, + "source": "src/rxdb.ts", + "line": 177, + "position": 5990, + "operator": "field-drop", + "original": "sort: [{ '_meta.lwt': `asc` }, { [primaryPath]: `asc` }]", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~9" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M29", + "number": 29, + "source": "src/rxdb.ts", + "line": 177, + "position": 5999, + "operator": "field-drop", + "original": "'_meta.lwt': `asc`", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~10" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M30", + "number": 30, + "source": "src/rxdb.ts", + "line": 177, + "position": 6023, + "operator": "field-drop", + "original": "[primaryPath]: `asc`", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~11" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M31", + "number": 31, + "source": "src/rxdb.ts", + "line": 178, + "position": 6062, + "operator": "field-drop", + "original": "limit: syncBatchSize", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~12" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M32", + "number": 32, + "source": "src/rxdb.ts", + "line": 179, + "position": 6098, + "operator": "field-drop", + "original": "skip: 0", + "replacement": "", + "anchor": "query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~13" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M33", + "number": 33, + "source": "src/rxdb.ts", + "line": 183, + "position": 6176, + "operator": "field-drop", + "original": "selector: { _deleted: false }", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M34", + "number": 34, + "source": "src/rxdb.ts", + "line": 183, + "position": 6188, + "operator": "field-drop", + "original": "_deleted: false", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M35", + "number": 35, + "source": "src/rxdb.ts", + "line": 183, + "position": 6198, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M36", + "number": 36, + "source": "src/rxdb.ts", + "line": 184, + "position": 6221, + "operator": "field-drop", + "original": "sort: [{ '_meta.lwt': `asc` }, { [primaryPath]: `asc` }]", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~3" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M37", + "number": 37, + "source": "src/rxdb.ts", + "line": 184, + "position": 6230, + "operator": "field-drop", + "original": "'_meta.lwt': `asc`", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~4" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M38", + "number": 38, + "source": "src/rxdb.ts", + "line": 184, + "position": 6254, + "operator": "field-drop", + "original": "[primaryPath]: `asc`", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~5" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M39", + "number": 39, + "source": "src/rxdb.ts", + "line": 185, + "position": 6293, + "operator": "field-drop", + "original": "limit: syncBatchSize", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~6" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M40", + "number": 40, + "source": "src/rxdb.ts", + "line": 186, + "position": 6329, + "operator": "field-drop", + "original": "skip: 0", + "replacement": "", + "anchor": "query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , }~7" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M41", + "number": 41, + "source": "src/rxdb.ts", + "line": 196, + "position": 6724, + "operator": "value-undefined", + "original": "prepareQuery(\n rxCollection.storageInstance.schema,\n query,\n )", + "replacement": "undefined", + "anchor": "const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M42", + "number": 42, + "source": "src/rxdb.ts", + "line": 200, + "position": 6854, + "operator": "value-undefined", + "original": "rxCollection.storageInstance.query(preparedQuery)", + "replacement": "undefined", + "anchor": "const result = await rxCollection . storageInstance . query ( preparedQuery )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:const result = await rxCollection . storageInstance . query ( preparedQuery )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M46", + "number": 46, + "source": "src/rxdb.ts", + "line": 204, + "position": 6998, + "operator": "value-undefined", + "original": "lastOfArray(docs)", + "replacement": "undefined", + "anchor": "cursor = lastOfArray ( docs )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:cursor = lastOfArray ( docs )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M47", + "number": 47, + "source": "src/rxdb.ts", + "line": 205, + "position": 7030, + "operator": "if-true", + "original": "docs.length === 0", + "replacement": "true", + "anchor": "if ( docs . length === 0 ) { initialFetchComplete = true break }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-true:if ( docs . length === 0 ) { initialFetchComplete = true break }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M48", + "number": 48, + "source": "src/rxdb.ts", + "line": 205, + "position": 7030, + "operator": "if-false", + "original": "docs.length === 0", + "replacement": "false", + "anchor": "if ( docs . length === 0 ) { initialFetchComplete = true break }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:if-false:if ( docs . length === 0 ) { initialFetchComplete = true break }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M49", + "number": 49, + "source": "src/rxdb.ts", + "line": 205, + "position": 7042, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "if ( docs . length === 0 ) { initialFetchComplete = true break }", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:binary-swap:if ( docs . length === 0 ) { initialFetchComplete = true break }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M50", + "number": 50, + "source": "src/rxdb.ts", + "line": 206, + "position": 7086, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "initialFetchComplete = true", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:bool-flip:initialFetchComplete = true" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M54", + "number": 54, + "source": "src/rxdb.ts", + "line": 212, + "position": 7201, + "operator": "delete-call", + "original": "write({\n type: `insert`,\n value: stripRxdbFields(clone(d)),\n })", + "replacement": ";", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:delete-call:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M55", + "number": 55, + "source": "src/rxdb.ts", + "line": 213, + "position": 7223, + "operator": "field-drop", + "original": "type: `insert`", + "replacement": "", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M56", + "number": 56, + "source": "src/rxdb.ts", + "line": 214, + "position": 7253, + "operator": "field-drop", + "original": "value: stripRxdbFields(clone(d))", + "replacement": "", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:field-drop:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M57", + "number": 57, + "source": "src/rxdb.ts", + "line": 214, + "position": 7260, + "operator": "value-undefined", + "original": "stripRxdbFields(clone(d))", + "replacement": "undefined", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M58", + "number": 58, + "source": "src/rxdb.ts", + "line": 214, + "position": 7276, + "operator": "value-undefined", + "original": "clone(d)", + "replacement": "undefined", + "anchor": "write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )", + "function": "initialFetch", + "key": "src/rxdb.ts#initialFetch:value-undefined:write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M93", + "number": 93, + "source": "src/rxdb.ts", + "line": 245, + "position": 8561, + "operator": "bool-flip", + "original": "true", + "replacement": "false", + "anchor": "let buffering = true", + "function": "sync", + "key": "src/rxdb.ts#sync:bool-flip:let buffering = true" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M94", + "number": 94, + "source": "src/rxdb.ts", + "line": 247, + "position": 8623, + "operator": "if-true", + "original": "buffering", + "replacement": "true", + "anchor": "if ( buffering ) { buffer . push ( msg ) return }", + "function": "queue", + "key": "src/rxdb.ts#queue:if-true:if ( buffering ) { buffer . push ( msg ) return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M95", + "number": 95, + "source": "src/rxdb.ts", + "line": 247, + "position": 8623, + "operator": "if-false", + "original": "buffering", + "replacement": "false", + "anchor": "if ( buffering ) { buffer . push ( msg ) return }", + "function": "queue", + "key": "src/rxdb.ts#queue:if-false:if ( buffering ) { buffer . push ( msg ) return }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M96", + "number": 96, + "source": "src/rxdb.ts", + "line": 248, + "position": 8646, + "operator": "delete-call", + "original": "buffer.push(msg)", + "replacement": ";", + "anchor": "buffer . push ( msg )", + "function": "queue", + "key": "src/rxdb.ts#queue:delete-call:buffer . push ( msg )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M97", + "number": 97, + "source": "src/rxdb.ts", + "line": 251, + "position": 8698, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "queue", + "key": "src/rxdb.ts#queue:delete-call:begin ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M98", + "number": 98, + "source": "src/rxdb.ts", + "line": 252, + "position": 8714, + "operator": "delete-call", + "original": "write(msg)", + "replacement": ";", + "anchor": "write ( msg )", + "function": "queue", + "key": "src/rxdb.ts#queue:delete-call:write ( msg )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M99", + "number": 99, + "source": "src/rxdb.ts", + "line": 253, + "position": 8733, + "operator": "delete-call", + "original": "commit()", + "replacement": ";", + "anchor": "commit ( )", + "function": "queue", + "key": "src/rxdb.ts#queue:delete-call:commit ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M102", + "number": 102, + "source": "src/rxdb.ts", + "line": 260, + "position": 8917, + "operator": "if-true", + "original": "!sub", + "replacement": "true", + "anchor": "if ( ! sub ) return", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:if-true:if ( ! sub ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M103", + "number": 103, + "source": "src/rxdb.ts", + "line": 260, + "position": 8917, + "operator": "if-false", + "original": "!sub", + "replacement": "false", + "anchor": "if ( ! sub ) return", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:if-false:if ( ! sub ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M104", + "number": 104, + "source": "src/rxdb.ts", + "line": 260, + "position": 8917, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! sub ) return", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:drop-not:if ( ! sub ) return" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M105", + "number": 105, + "source": "src/rxdb.ts", + "line": 261, + "position": 8938, + "operator": "delete-call", + "original": "getFromMapOrCreate(\n OPEN_RXDB_SUBSCRIPTIONS,\n rxCollection,\n () => new Set(),\n ).delete(sub)", + "replacement": ";", + "anchor": "getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub )", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:delete-call:getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M106", + "number": 106, + "source": "src/rxdb.ts", + "line": 266, + "position": 9074, + "operator": "delete-call", + "original": "sub.unsubscribe()", + "replacement": ";", + "anchor": "sub . unsubscribe ( )", + "function": "stopOngoingFetch", + "key": "src/rxdb.ts#stopOngoingFetch:delete-call:sub . unsubscribe ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M107", + "number": 107, + "source": "src/rxdb.ts", + "line": 272, + "position": 9259, + "operator": "value-undefined", + "original": "rxCollection.$.subscribe({\n next: (ev) => {\n const cur: Row = stripRxdbFields(clone(ev.documentData as Row))\n switch (ev.operation) {\n // The initial read can already hold a document whose insert\n // event was buffered, so an insert writes as an update. Core\n // inserts an updated key it does not hold.\n case `INSERT`:\n case `UPDATE`:\n queue({ type: `update`, value: cur })\n break\n case `DELETE`:\n queue({ type: `delete`, value: cur })\n break\n }\n },\n // An errored stream ends, so later writes would never arrive.\n error: stop,\n })", + "replacement": "undefined", + "anchor": "sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )", + "function": "startOngoingFetch", + "key": "src/rxdb.ts#startOngoingFetch:value-undefined:sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M108", + "number": 108, + "source": "src/rxdb.ts", + "line": 273, + "position": 9296, + "operator": "field-drop", + "original": "next: (ev) => {\n const cur: Row = stripRxdbFields(clone(ev.documentData as Row))\n switch (ev.operation) {\n // The initial read can already hold a document whose insert\n // event was buffered, so an insert writes as an update. Core\n // inserts an updated key it does not hold.\n case `INSERT`:\n case `UPDATE`:\n queue({ type: `update`, value: cur })\n break\n case `DELETE`:\n queue({ type: `delete`, value: cur })\n break\n }\n }", + "replacement": "", + "anchor": "sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M109", + "number": 109, + "source": "src/rxdb.ts", + "line": 274, + "position": 9341, + "operator": "value-undefined", + "original": "stripRxdbFields(clone(ev.documentData as Row))", + "replacement": "undefined", + "anchor": "const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) )", + "function": "next", + "key": "src/rxdb.ts#next:value-undefined:const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M110", + "number": 110, + "source": "src/rxdb.ts", + "line": 274, + "position": 9357, + "operator": "value-undefined", + "original": "clone(ev.documentData as Row)", + "replacement": "undefined", + "anchor": "const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) )", + "function": "next", + "key": "src/rxdb.ts#next:value-undefined:const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M111", + "number": 111, + "source": "src/rxdb.ts", + "line": 281, + "position": 9707, + "operator": "delete-call", + "original": "queue({ type: `update`, value: cur })", + "replacement": ";", + "anchor": "queue ( { type : `update` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:delete-call:queue ( { type : `update` , value : cur } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M112", + "number": 112, + "source": "src/rxdb.ts", + "line": 281, + "position": 9715, + "operator": "field-drop", + "original": "type: `update`", + "replacement": "", + "anchor": "queue ( { type : `update` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:queue ( { type : `update` , value : cur } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M113", + "number": 113, + "source": "src/rxdb.ts", + "line": 281, + "position": 9731, + "operator": "field-drop", + "original": "value: cur", + "replacement": "", + "anchor": "queue ( { type : `update` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:queue ( { type : `update` , value : cur } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M114", + "number": 114, + "source": "src/rxdb.ts", + "line": 284, + "position": 9812, + "operator": "delete-call", + "original": "queue({ type: `delete`, value: cur })", + "replacement": ";", + "anchor": "queue ( { type : `delete` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:delete-call:queue ( { type : `delete` , value : cur } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M115", + "number": 115, + "source": "src/rxdb.ts", + "line": 284, + "position": 9820, + "operator": "field-drop", + "original": "type: `delete`", + "replacement": "", + "anchor": "queue ( { type : `delete` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:queue ( { type : `delete` , value : cur } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M116", + "number": 116, + "source": "src/rxdb.ts", + "line": 284, + "position": 9836, + "operator": "field-drop", + "original": "value: cur", + "replacement": "", + "anchor": "queue ( { type : `delete` , value : cur } )", + "function": "next", + "key": "src/rxdb.ts#next:field-drop:queue ( { type : `delete` , value : cur } )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M117", + "number": 117, + "source": "src/rxdb.ts", + "line": 289, + "position": 9982, + "operator": "field-drop", + "original": "error: stop", + "replacement": "", + "anchor": "sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )", + "function": "startOngoingFetch", + "key": "src/rxdb.ts#startOngoingFetch:field-drop:sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M118", + "number": 118, + "source": "src/rxdb.ts", + "line": 292, + "position": 10028, + "operator": "value-undefined", + "original": "getFromMapOrCreate(\n OPEN_RXDB_SUBSCRIPTIONS,\n rxCollection,\n () => new Set(),\n )", + "replacement": "undefined", + "anchor": "const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , )", + "function": "startOngoingFetch", + "key": "src/rxdb.ts#startOngoingFetch:value-undefined:const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M119", + "number": 119, + "source": "src/rxdb.ts", + "line": 297, + "position": 10152, + "operator": "delete-call", + "original": "subs.add(sub)", + "replacement": ";", + "anchor": "subs . add ( sub )", + "function": "startOngoingFetch", + "key": "src/rxdb.ts#startOngoingFetch:delete-call:subs . add ( sub )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M120", + "number": 120, + "source": "src/rxdb.ts", + "line": 301, + "position": 10214, + "operator": "delete-call", + "original": "startOngoingFetch()", + "replacement": ";", + "anchor": "startOngoingFetch ( )", + "function": "start", + "key": "src/rxdb.ts#start:delete-call:startOngoingFetch ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M124", + "number": 124, + "source": "src/rxdb.ts", + "line": 303, + "position": 10280, + "operator": "value-undefined", + "original": "initialFetch()", + "replacement": "undefined", + "anchor": "await initialFetch ( )", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:await initialFetch ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M128", + "number": 128, + "source": "src/rxdb.ts", + "line": 312, + "position": 10662, + "operator": "value-undefined", + "original": "buffer.splice(0)", + "replacement": "undefined", + "anchor": "const pending = buffer . splice ( 0 )", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:const pending = buffer . splice ( 0 )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M129", + "number": 129, + "source": "src/rxdb.ts", + "line": 313, + "position": 10699, + "operator": "bool-flip", + "original": "false", + "replacement": "true", + "anchor": "buffering = false", + "function": "start", + "key": "src/rxdb.ts#start:bool-flip:buffering = false" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M130", + "number": 130, + "source": "src/rxdb.ts", + "line": 314, + "position": 10717, + "operator": "if-true", + "original": "pending.length > 0", + "replacement": "true", + "anchor": "if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }", + "function": "start", + "key": "src/rxdb.ts#start:if-true:if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M131", + "number": 131, + "source": "src/rxdb.ts", + "line": 314, + "position": 10717, + "operator": "if-false", + "original": "pending.length > 0", + "replacement": "false", + "anchor": "if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }", + "function": "start", + "key": "src/rxdb.ts#start:if-false:if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M132", + "number": 132, + "source": "src/rxdb.ts", + "line": 314, + "position": 10732, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }", + "function": "start", + "key": "src/rxdb.ts#start:binary-swap:if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M133", + "number": 133, + "source": "src/rxdb.ts", + "line": 315, + "position": 10749, + "operator": "delete-call", + "original": "begin()", + "replacement": ";", + "anchor": "begin ( )", + "function": "start", + "key": "src/rxdb.ts#start:delete-call:begin ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M134", + "number": 134, + "source": "src/rxdb.ts", + "line": 316, + "position": 10794, + "operator": "delete-call", + "original": "write(msg)", + "replacement": ";", + "anchor": "write ( msg )", + "function": "start", + "key": "src/rxdb.ts#start:delete-call:write ( msg )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M135", + "number": 135, + "source": "src/rxdb.ts", + "line": 317, + "position": 10821, + "operator": "value-undefined", + "original": "commit()", + "replacement": "undefined", + "anchor": "await commit ( )", + "function": "start", + "key": "src/rxdb.ts#start:value-undefined:await commit ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M139", + "number": 139, + "source": "src/rxdb.ts", + "line": 324, + "position": 10971, + "operator": "delete-call", + "original": "markReady()", + "replacement": ";", + "anchor": "markReady ( )", + "function": "start", + "key": "src/rxdb.ts#start:delete-call:markReady ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M140", + "number": 140, + "source": "src/rxdb.ts", + "line": 329, + "position": 11092, + "operator": "delete-call", + "original": "stopOngoingFetch()", + "replacement": ";", + "anchor": "stopOngoingFetch ( )", + "function": "stop", + "key": "src/rxdb.ts#stop:delete-call:stopOngoingFetch ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M141", + "number": 141, + "source": "src/rxdb.ts", + "line": 330, + "position": 11119, + "operator": "delete-call", + "original": "fail(error)", + "replacement": ";", + "anchor": "fail ( error )", + "function": "stop", + "key": "src/rxdb.ts#stop:delete-call:fail ( error )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M142", + "number": 142, + "source": "src/rxdb.ts", + "line": 332, + "position": 11150, + "operator": "value-undefined", + "original": "start().catch(stop)", + "replacement": "undefined", + "anchor": "void start ( ) . catch ( stop )", + "function": "sync", + "key": "src/rxdb.ts#sync:value-undefined:void start ( ) . catch ( stop )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M143", + "number": 143, + "source": "src/rxdb.ts", + "line": 338, + "position": 11343, + "operator": "field-drop", + "original": "rowUpdateMode: `full`", + "replacement": "", + "anchor": "const sync : SyncConfig < Row , Key > = { sync : ( params : SyncParams ) => { const { begin , write , commit , markReady , fail , collection } = params const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let initialFetchComplete = false async function initialFetch ( ) { let cursor : RxDocumentData < Row > | undefined = undefined const syncBatchSize = config . syncBatchSize ? config . syncBatchSize : 1000 const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } ) while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) } const firstApplied = commit ( ) if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined ) const applied : Array < true | Promise < void >> = [ firstApplied ] if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } } await Promise . all ( applied ) } type WriteMessage = Parameters < typeof write > [ 0 ] const buffer : Array < WriteMessage > = [ ] let buffering = true const queue = ( msg : WriteMessage ) => { if ( buffering ) { buffer . push ( msg ) return } begin ( ) write ( msg ) commit ( ) } let sub : Subscription | undefined const isStopped = ( ) => sub ?. closed !== false function stopOngoingFetch ( ) { buffer . length = 0 if ( ! sub ) return getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub ) sub . unsubscribe ( ) sub = undefined } function startOngoingFetch ( ) { sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } ) const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) subs . add ( sub ) } async function start ( ) { startOngoingFetch ( ) if ( isStopped ( ) ) return await initialFetch ( ) if ( isStopped ( ) ) { return } const pending = buffer . splice ( 0 ) buffering = false if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } } markReady ( ) } function stop ( error : unknown ) { stopOngoingFetch ( ) fail ( error ) } void start ( ) . catch ( stop ) return stopOngoingFetch } , rowUpdateMode : `full` , }", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:field-drop:const sync : SyncConfig < Row , Key > = { sync : ( params : SyncParams ) => { const { begin , write , commit , markReady , fail , collection } = params const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence ) const replacesRetainedRows = collection . health . reloading && ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done let initialFetchComplete = false async function initialFetch ( ) { let cursor : RxDocumentData < Row > | undefined = undefined const syncBatchSize = config . syncBatchSize ? config . syncBatchSize : 1000 const listed = persistence && ! replacesRetainedRows ? new Set < string > ( ) : undefined begin ( { snapshot : persistence === null ? ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done : replacesRetainedRows , } ) while ( ! initialFetchComplete ) { let query : FilledMangoQuery < Row > if ( cursor ) { query = { selector : { $or : [ { '_meta.lwt' : { $gt : cursor . _meta . lwt } } , { '_meta.lwt' : cursor . _meta . lwt , [ primaryPath ] : { $gt : getKey ( cursor ) , } , } , ] , _deleted : false , } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } else { query = { selector : { _deleted : false } , sort : [ { '_meta.lwt' : `asc` } , { [ primaryPath ] : `asc` } ] , limit : syncBatchSize , skip : 0 , } } const preparedQuery = prepareQuery < Row > ( rxCollection . storageInstance . schema , query , ) const result = await rxCollection . storageInstance . query ( preparedQuery ) if ( isStopped ( ) ) return const docs = result . documents cursor = lastOfArray ( docs ) if ( docs . length === 0 ) { initialFetchComplete = true break } docs . forEach ( ( d ) => { listed ?. add ( getKey ( d ) ) write ( { type : `insert` , value : stripRxdbFields ( clone ( d ) ) , } ) } ) } const firstApplied = commit ( ) if ( firstApplied !== true ) void firstApplied . catch ( ( ) => undefined ) const applied : Array < true | Promise < void >> = [ firstApplied ] if ( listed && persistence ) { await persistence . hydrateBaseline ( ) if ( isStopped ( ) ) return const gone : Array < string > = [ ] for ( const [ key ] of collection . _state . acceptedSyncedEntries ( ) ) { if ( ! listed . has ( key ) ) gone . push ( key ) } for ( let first = 0 ; first < gone . length ; first += syncBatchSize ) { begin ( ) for ( const key of gone . slice ( first , first + syncBatchSize ) ) write ( { type : `delete` , key } ) const receipt = commit ( ) if ( receipt !== true ) void receipt . catch ( ( ) => undefined ) applied . push ( receipt ) } } await Promise . all ( applied ) } type WriteMessage = Parameters < typeof write > [ 0 ] const buffer : Array < WriteMessage > = [ ] let buffering = true const queue = ( msg : WriteMessage ) => { if ( buffering ) { buffer . push ( msg ) return } begin ( ) write ( msg ) commit ( ) } let sub : Subscription | undefined const isStopped = ( ) => sub ?. closed !== false function stopOngoingFetch ( ) { buffer . length = 0 if ( ! sub ) return getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) . delete ( sub ) sub . unsubscribe ( ) sub = undefined } function startOngoingFetch ( ) { sub = rxCollection . $ . subscribe ( { next : ( ev ) => { const cur : Row = stripRxdbFields ( clone ( ev . documentData as Row ) ) switch ( ev . operation ) { case `INSERT` : case `UPDATE` : queue ( { type : `update` , value : cur } ) break case `DELETE` : queue ( { type : `delete` , value : cur } ) break } } , error : stop , } ) const subs = getFromMapOrCreate ( OPEN_RXDB_SUBSCRIPTIONS , rxCollection , ( ) => new Set ( ) , ) subs . add ( sub ) } async function start ( ) { startOngoingFetch ( ) if ( isStopped ( ) ) return await initialFetch ( ) if ( isStopped ( ) ) { return } const pending = buffer . splice ( 0 ) buffering = false if ( pending . length > 0 ) { begin ( ) for ( const msg of pending ) write ( msg ) await commit ( ) if ( isStopped ( ) ) { return } } markReady ( ) } function stop ( error : unknown ) { stopOngoingFetch ( ) fail ( error ) } void start ( ) . catch ( stop ) return stopOngoingFetch } , rowUpdateMode : `full` , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M144", + "number": 144, + "source": "src/rxdb.ts", + "line": 343, + "position": 11454, + "operator": "field-drop", + "original": "getKey", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M145", + "number": 145, + "source": "src/rxdb.ts", + "line": 344, + "position": 11466, + "operator": "field-drop", + "original": "sync", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M146", + "number": 146, + "source": "src/rxdb.ts", + "line": 345, + "position": 11476, + "operator": "field-drop", + "original": "onInsert: async (params) => {\n debug(`insert`, params)\n const newItems = params.transaction.mutations.map((m) => m.modified)\n const result = await rxCollection.bulkUpsert(newItems)\n if (result.error.length > 0) {\n throw rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0]))\n }\n }", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M147", + "number": 147, + "source": "src/rxdb.ts", + "line": 346, + "position": 11512, + "operator": "delete-call", + "original": "debug(`insert`, params)", + "replacement": ";", + "anchor": "debug ( `insert` , params )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:delete-call:debug ( `insert` , params )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M148", + "number": 148, + "source": "src/rxdb.ts", + "line": 347, + "position": 11559, + "operator": "value-undefined", + "original": "params.transaction.mutations.map((m) => m.modified)", + "replacement": "undefined", + "anchor": "const newItems = params . transaction . mutations . map ( ( m ) => m . modified )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:value-undefined:const newItems = params . transaction . mutations . map ( ( m ) => m . modified )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M149", + "number": 149, + "source": "src/rxdb.ts", + "line": 348, + "position": 11638, + "operator": "value-undefined", + "original": "rxCollection.bulkUpsert(newItems)", + "replacement": "undefined", + "anchor": "const result = await rxCollection . bulkUpsert ( newItems )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:value-undefined:const result = await rxCollection . bulkUpsert ( newItems )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M150", + "number": 150, + "source": "src/rxdb.ts", + "line": 349, + "position": 11682, + "operator": "if-true", + "original": "result.error.length > 0", + "replacement": "true", + "anchor": "if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:if-true:if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M151", + "number": 151, + "source": "src/rxdb.ts", + "line": 349, + "position": 11682, + "operator": "if-false", + "original": "result.error.length > 0", + "replacement": "false", + "anchor": "if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:if-false:if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M152", + "number": 152, + "source": "src/rxdb.ts", + "line": 349, + "position": 11702, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:binary-swap:if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M153", + "number": 153, + "source": "src/rxdb.ts", + "line": 350, + "position": 11723, + "operator": "value-undefined", + "original": "rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0]))", + "replacement": "undefined", + "anchor": "throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:value-undefined:throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M154", + "number": 154, + "source": "src/rxdb.ts", + "line": 350, + "position": 11752, + "operator": "value-undefined", + "original": "ensureNotFalsy(result.error[0])", + "replacement": "undefined", + "anchor": "throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) )", + "function": "onInsert", + "key": "src/rxdb.ts#onInsert:value-undefined:throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M155", + "number": 155, + "source": "src/rxdb.ts", + "line": 353, + "position": 11804, + "operator": "field-drop", + "original": "onUpdate: async (params) => {\n debug(`update`, params)\n const mutations = params.transaction.mutations.filter(\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n (m) => m.type === `update`,\n )\n\n for (const mutation of mutations) {\n const newValue = stripRxdbFields(mutation.modified)\n const id = getKey(newValue)\n const doc = await rxCollection.findOne(id).exec()\n if (!doc) {\n continue\n }\n await doc.incrementalPatch(newValue)\n }\n }", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M156", + "number": 156, + "source": "src/rxdb.ts", + "line": 354, + "position": 11840, + "operator": "delete-call", + "original": "debug(`update`, params)", + "replacement": ";", + "anchor": "debug ( `update` , params )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:delete-call:debug ( `update` , params )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M157", + "number": 157, + "source": "src/rxdb.ts", + "line": 355, + "position": 11888, + "operator": "value-undefined", + "original": "params.transaction.mutations.filter(\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n (m) => m.type === `update`,\n )", + "replacement": "undefined", + "anchor": "const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M158", + "number": 158, + "source": "src/rxdb.ts", + "line": 357, + "position": 12027, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:binary-swap:const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M159", + "number": 159, + "source": "src/rxdb.ts", + "line": 361, + "position": 12117, + "operator": "value-undefined", + "original": "stripRxdbFields(mutation.modified)", + "replacement": "undefined", + "anchor": "const newValue = stripRxdbFields ( mutation . modified )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:const newValue = stripRxdbFields ( mutation . modified )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M160", + "number": 160, + "source": "src/rxdb.ts", + "line": 362, + "position": 12171, + "operator": "value-undefined", + "original": "getKey(newValue)", + "replacement": "undefined", + "anchor": "const id = getKey ( newValue )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:const id = getKey ( newValue )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M161", + "number": 161, + "source": "src/rxdb.ts", + "line": 363, + "position": 12214, + "operator": "value-undefined", + "original": "rxCollection.findOne(id).exec()", + "replacement": "undefined", + "anchor": "const doc = await rxCollection . findOne ( id ) . exec ( )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:const doc = await rxCollection . findOne ( id ) . exec ( )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M162", + "number": 162, + "source": "src/rxdb.ts", + "line": 364, + "position": 12258, + "operator": "if-true", + "original": "!doc", + "replacement": "true", + "anchor": "if ( ! doc ) { continue }", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:if-true:if ( ! doc ) { continue }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M163", + "number": 163, + "source": "src/rxdb.ts", + "line": 364, + "position": 12258, + "operator": "if-false", + "original": "!doc", + "replacement": "false", + "anchor": "if ( ! doc ) { continue }", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:if-false:if ( ! doc ) { continue }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M164", + "number": 164, + "source": "src/rxdb.ts", + "line": 364, + "position": 12258, + "operator": "drop-not", + "original": "!", + "replacement": "", + "anchor": "if ( ! doc ) { continue }", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:drop-not:if ( ! doc ) { continue }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M165", + "number": 165, + "source": "src/rxdb.ts", + "line": 367, + "position": 12309, + "operator": "value-undefined", + "original": "doc.incrementalPatch(newValue)", + "replacement": "undefined", + "anchor": "await doc . incrementalPatch ( newValue )", + "function": "onUpdate", + "key": "src/rxdb.ts#onUpdate:value-undefined:await doc . incrementalPatch ( newValue )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M166", + "number": 166, + "source": "src/rxdb.ts", + "line": 370, + "position": 12359, + "operator": "field-drop", + "original": "onDelete: async (params) => {\n debug(`delete`, params)\n const mutations = params.transaction.mutations.filter(\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n (m) => m.type === `delete`,\n )\n const ids = mutations.map((mutation) => getKey(mutation.original))\n const result = await rxCollection.bulkRemove(ids)\n if (result.error.length > 0) {\n throw result.error\n }\n }", + "replacement": "", + "anchor": "const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:field-drop:const collectionConfig : CollectionConfig < Row , string > = { ... restConfig , getKey , sync , onInsert : async ( params ) => { debug ( `insert` , params ) const newItems = params . transaction . mutations . map ( ( m ) => m . modified ) const result = await rxCollection . bulkUpsert ( newItems ) if ( result . error . length > 0 ) { throw rxStorageWriteErrorToRxError ( ensureNotFalsy ( result . error [ 0 ] ) ) } } , onUpdate : async ( params ) => { debug ( `update` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `update` , ) for ( const mutation of mutations ) { const newValue = stripRxdbFields ( mutation . modified ) const id = getKey ( newValue ) const doc = await rxCollection . findOne ( id ) . exec ( ) if ( ! doc ) { continue } await doc . incrementalPatch ( newValue ) } } , onDelete : async ( params ) => { debug ( `delete` , params ) const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , ) const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) ) const result = await rxCollection . bulkRemove ( ids ) if ( result . error . length > 0 ) { throw result . error } } , }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M167", + "number": 167, + "source": "src/rxdb.ts", + "line": 371, + "position": 12395, + "operator": "delete-call", + "original": "debug(`delete`, params)", + "replacement": ";", + "anchor": "debug ( `delete` , params )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:delete-call:debug ( `delete` , params )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M168", + "number": 168, + "source": "src/rxdb.ts", + "line": 372, + "position": 12443, + "operator": "value-undefined", + "original": "params.transaction.mutations.filter(\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n (m) => m.type === `delete`,\n )", + "replacement": "undefined", + "anchor": "const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:value-undefined:const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M169", + "number": 169, + "source": "src/rxdb.ts", + "line": 374, + "position": 12582, + "operator": "binary-swap", + "original": "===", + "replacement": "!==", + "anchor": "const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:binary-swap:const mutations = params . transaction . mutations . filter ( ( m ) => m . type === `delete` , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M170", + "number": 170, + "source": "src/rxdb.ts", + "line": 376, + "position": 12622, + "operator": "value-undefined", + "original": "mutations.map((mutation) => getKey(mutation.original))", + "replacement": "undefined", + "anchor": "const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:value-undefined:const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M171", + "number": 171, + "source": "src/rxdb.ts", + "line": 376, + "position": 12650, + "operator": "value-undefined", + "original": "getKey(mutation.original)", + "replacement": "undefined", + "anchor": "const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:value-undefined:const ids = mutations . map ( ( mutation ) => getKey ( mutation . original ) )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M172", + "number": 172, + "source": "src/rxdb.ts", + "line": 377, + "position": 12704, + "operator": "value-undefined", + "original": "rxCollection.bulkRemove(ids)", + "replacement": "undefined", + "anchor": "const result = await rxCollection . bulkRemove ( ids )", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:value-undefined:const result = await rxCollection . bulkRemove ( ids )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M173", + "number": 173, + "source": "src/rxdb.ts", + "line": 378, + "position": 12743, + "operator": "if-true", + "original": "result.error.length > 0", + "replacement": "true", + "anchor": "if ( result . error . length > 0 ) { throw result . error }", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:if-true:if ( result . error . length > 0 ) { throw result . error }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M174", + "number": 174, + "source": "src/rxdb.ts", + "line": 378, + "position": 12743, + "operator": "if-false", + "original": "result.error.length > 0", + "replacement": "false", + "anchor": "if ( result . error . length > 0 ) { throw result . error }", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:if-false:if ( result . error . length > 0 ) { throw result . error }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M175", + "number": 175, + "source": "src/rxdb.ts", + "line": 378, + "position": 12763, + "operator": "binary-swap", + "original": ">", + "replacement": ">=", + "anchor": "if ( result . error . length > 0 ) { throw result . error }", + "function": "onDelete", + "key": "src/rxdb.ts#onDelete:binary-swap:if ( result . error . length > 0 ) { throw result . error }" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M176", + "number": 176, + "source": "src/rxdb.ts", + "line": 383, + "position": 12825, + "operator": "value-undefined", + "original": "withCollectionConfigFactory(collectionConfig, () =>\n rxdbCollectionOptions(config),\n )", + "replacement": "undefined", + "anchor": "return withCollectionConfigFactory ( collectionConfig , ( ) => rxdbCollectionOptions ( config ) , )", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:value-undefined:return withCollectionConfigFactory ( collectionConfig , ( ) => rxdbCollectionOptions ( config ) , )" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + }, + { + "operator": { + "id": "rxdb.ts:M177", + "number": 177, + "source": "src/rxdb.ts", + "line": 384, + "position": 12881, + "operator": "value-undefined", + "original": "rxdbCollectionOptions(config)", + "replacement": "undefined", + "anchor": "return withCollectionConfigFactory ( collectionConfig , ( ) => rxdbCollectionOptions ( config ) , )", + "function": "rxdbCollectionOptions", + "key": "src/rxdb.ts#rxdbCollectionOptions:value-undefined:return withCollectionConfigFactory ( collectionConfig , ( ) => rxdbCollectionOptions ( config ) , )~2" + }, + "reason": "Outside the added/replaced executable-line selection; retained in the complete inventory. Not executed or waived by this additive diagnostic campaign." + } + ], + "pilot": { + "run": { + "id": "91ef8636c22a", + "inputs": "1 source files, 2 oracle files, 0 ordinary test files, 4 authored mutants, seed 2367, pool forks; a partial run (--only or --sample)" + }, + "options": { + "pkg": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection", + "envPrefix": "TANSTACK_DB_ORACLE", + "daemon": false, + "ordinary": false, + "contexts": 1, + "sources": [ + "src/rxdb.ts" + ], + "seed": "2367", + "out": "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/pilot-rxdb", + "only": [ + "authored:forced-snapshot-startup", + "authored:skip-restored-omissions", + "authored:unbounded-omission-batch", + "authored:resume-sweep-after-terminal-stream-error" + ], + "testTimeout": 15000, + "hangLimit": 10000, + "firstLimit": 2000, + "pool": "forks", + "mutants": "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/controls-rxdb" + }, + "excluded": [], + "nondeterministic": [], + "mutantSeconds": 28.935, + "evidence": { + "reused": 0, + "computed": 12, + "cut": 1, + "idle": 0, + "spin": 0, + "rerun": 0, + "unchanged": 0 + }, + "results": [ + { + "id": "authored:forced-snapshot-startup", + "authored": true, + "authoredId": "forced-snapshot-startup", + "source": "src/rxdb.ts", + "operator": "authored", + "function": "sync", + "intent": "Restore unconditional snapshot replacement for persisted first startup; accepted public/durable result may be right while the independent startup-work law rejects it.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "original": "const replacesRetainedRows =\n collection.health.reloading &&\n !collection._state.acceptedSyncedEntries().next().done", + "replacement": "const replacesRetainedRows = persistence !== null", + "line": 138, + "outcome": "killed", + "by": [ + "persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > fixed seed 2367" + ], + "laws": [ + "startup batching" + ], + "randomAlso": false, + "seconds": 20.653005251, + "key": "src/rxdb.ts#authored:forced-snapshot-startup" + }, + { + "id": "authored:skip-restored-omissions", + "authored": true, + "authoredId": "skip-restored-omissions", + "source": "src/rxdb.ts", + "operator": "authored", + "function": "initialFetch", + "intent": "Keep the positive source result but omit the restored-row reconciliation, reproducing the obsolete persisted-row class under ordinary batched startup.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "original": "if (!listed.has(key)) gone.push(key)", + "replacement": "if (false) gone.push(key)", + "line": 229, + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367", + "persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle", + "persisted startup" + ], + "randomAlso": false, + "seconds": 0.3986957500000001, + "key": "src/rxdb.ts#authored:skip-restored-omissions" + }, + { + "id": "authored:unbounded-omission-batch", + "authored": true, + "authoredId": "unbounded-omission-batch", + "source": "src/rxdb.ts", + "operator": "authored", + "function": "initialFetch", + "intent": "Delete every remaining restored omission in the first commit, violating the independent row-work bound for three saved omissions.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "original": "gone.slice(first, first + syncBatchSize)", + "replacement": "gone.slice(first)", + "line": 233, + "outcome": "killed", + "by": [ + "persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > fixed seed 2367" + ], + "laws": [ + "startup batching" + ], + "randomAlso": false, + "seconds": 2.492997542, + "key": "src/rxdb.ts#authored:unbounded-omission-batch" + }, + { + "id": "authored:resume-sweep-after-terminal-stream-error", + "authored": true, + "authoredId": "resume-sweep-after-terminal-stream-error", + "source": "src/rxdb.ts", + "operator": "authored", + "function": "initialFetch", + "intent": "After a genuine terminal RxDB stream error, resume the held adapter hydrate continuation and wrongly accept restored omission deletes.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "original": "await persistence.hydrateBaseline()\n if (isStopped()) return", + "replacement": "await persistence.hydrateBaseline()", + "line": 222, + "outcome": "killed", + "by": [ + "adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled > fixed seed 2367" + ], + "laws": [ + "persisted source lifecycle" + ], + "randomAlso": false, + "seconds": 1.3625032919999998, + "key": "src/rxdb.ts#authored:resume-sweep-after-terminal-stream-error" + } + ] + }, + "authoredControls": [ + { + "id": "forced-snapshot-startup", + "source": "src/rxdb.ts", + "find": "const replacesRetainedRows =\n collection.health.reloading &&\n !collection._state.acceptedSyncedEntries().next().done", + "replace": "const replacesRetainedRows = persistence !== null", + "intent": "Restore unconditional snapshot replacement for persisted first startup; accepted public/durable result may be right while the independent startup-work law rejects it.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "sourceSha256": "c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7", + "position": 4496, + "line": 138, + "transformedSha256": "6d7d909e34e8d1e34d8d341723aa033f27363b5f7a37f600992d0e922f63aded" + }, + { + "id": "skip-restored-omissions", + "source": "src/rxdb.ts", + "find": "if (!listed.has(key)) gone.push(key)", + "replace": "if (false) gone.push(key)", + "intent": "Keep the positive source result but omit the restored-row reconciliation, reproducing the obsolete persisted-row class under ordinary batched startup.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "sourceSha256": "c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7", + "position": 7970, + "line": 229, + "transformedSha256": "f870e8f991091deb8d0b96f467aff6f92c370f8030d2bb7a9e465319b75165dd" + }, + { + "id": "unbounded-omission-batch", + "source": "src/rxdb.ts", + "find": "gone.slice(first, first + syncBatchSize)", + "replace": "gone.slice(first)", + "intent": "Delete every remaining restored omission in the first commit, violating the independent row-work bound for three saved omissions.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "sourceSha256": "c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7", + "position": 8146, + "line": 233, + "transformedSha256": "b9a79692db0d3538a81880086f6f3e5616ecb198ef9b90f5840d146f75cb7bdd" + }, + { + "id": "resume-sweep-after-terminal-stream-error", + "source": "src/rxdb.ts", + "find": "await persistence.hydrateBaseline()\n if (isStopped()) return", + "replace": "await persistence.hydrateBaseline()", + "intent": "After a genuine terminal RxDB stream error, resume the held adapter hydrate continuation and wrongly accept restored omission deletes.", + "author": "/root/gap_instrument_review (independent reviewer; accepted source/work/lifecycle laws)", + "sourceSha256": "c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7", + "position": 7554, + "line": 222, + "transformedSha256": "4d89712fb798866ce12394772c119f777f7e225ffb0090be43c7af326a5b5182" + } + ], + "automaticCommand": [ + "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node", + "/private/tmp/tanstack-db-2394-gap-cli-e1fb-titles/oracle-gaps/index.mjs", + "--package", + "packages/rxdb-db-collection", + "--source", + "src/rxdb.ts", + "--seed", + "2367", + "--contexts", + "1", + "--pool", + "forks", + "--env-prefix", + "TANSTACK_DB_ORACLE", + "--only", + "rxdb.ts:M5,rxdb.ts:M6,rxdb.ts:M8,rxdb.ts:M9,rxdb.ts:M10,rxdb.ts:M11,rxdb.ts:M12,rxdb.ts:M13,rxdb.ts:M43,rxdb.ts:M44,rxdb.ts:M45,rxdb.ts:M51,rxdb.ts:M52,rxdb.ts:M53,rxdb.ts:M59,rxdb.ts:M60,rxdb.ts:M61,rxdb.ts:M62,rxdb.ts:M63,rxdb.ts:M64,rxdb.ts:M65,rxdb.ts:M66,rxdb.ts:M67,rxdb.ts:M68,rxdb.ts:M69,rxdb.ts:M70,rxdb.ts:M71,rxdb.ts:M72,rxdb.ts:M73,rxdb.ts:M74,rxdb.ts:M75,rxdb.ts:M76,rxdb.ts:M77,rxdb.ts:M78,rxdb.ts:M79,rxdb.ts:M80,rxdb.ts:M81,rxdb.ts:M82,rxdb.ts:M83,rxdb.ts:M84,rxdb.ts:M85,rxdb.ts:M86,rxdb.ts:M87,rxdb.ts:M88,rxdb.ts:M89,rxdb.ts:M90,rxdb.ts:M91,rxdb.ts:M92,rxdb.ts:M100,rxdb.ts:M101,rxdb.ts:M121,rxdb.ts:M122,rxdb.ts:M123,rxdb.ts:M125,rxdb.ts:M126,rxdb.ts:M127,rxdb.ts:M136,rxdb.ts:M137,rxdb.ts:M138", + "--out", + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-rxdb" + ], + "baselineCapture": { + "first": { + "unitCount": 2, + "testCount": 12, + "uniqueKeys": 12, + "allPassed": true, + "rowCount": 14, + "setupErrors": [], + "unhandled": [], + "fileSeconds": [ + { + "file": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "seconds": 3.996, + "duration": 3.5774136250000006 + }, + { + "file": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "seconds": 1.924, + "duration": 1.516590624 + } + ] + }, + "second": { + "unitCount": 2, + "testCount": 12, + "uniqueKeys": 12, + "allPassed": true, + "rowCount": 14, + "setupErrors": [], + "unhandled": [], + "fileSeconds": [ + { + "file": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts", + "seconds": 3.34, + "duration": 2.834746 + }, + { + "file": "/private/tmp/tanstack-db-2394-final-adapter-gap/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "seconds": 2.048, + "duration": 1.554428917 + } + ] + } + }, + "artifacts": { + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-rxdb-command.json": "496009e8a87bd5c66e812be9d371a8367670cd85500337deff439bf47100b60f", + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-rxdb.log": "cf7570f0c8205a150f1ddb857d425a500bafbd8a77344d8393dd635bae09d92e", + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-rxdb/results.jsonl": "cf5d8cd338b6423004583017eead7df60e02c3b1a73b3598193d33a0e2175612", + "/private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-rxdb/baseline-captures.json": "2c34d410a150920388de52e928696bbd6df73aac1c9b927daafbedc819faec52", + "/private/tmp/tanstack-db-2394-final-adapter-delta/rxdb-selection.json": "a604aae15191cfef4622d43ab798721331ea617125c8a07be5fed105430a0861" + } + } + }, + "liveIterator": [ + { + "id": "authored.live-iterator.powersync", + "source": "packages/powersync-db-collection/src/powersync.ts", + "sourceSha256": "c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068", + "sourceLines": [ + 667, + 691 + ], + "mutantBodySha256": "008669ffafe4eacb7a6a75bf51f7b5c6f2d615eadc80a2193cd150706807d316", + "returnedCodeWithExecutionSentinelSha256": "17663c9aa34ac11d1130f4d8d51f7b05e0c2bf7da1b0c438940c9d3e1a36f442", + "effect": "Commit full omission batches during the acceptedSyncedEntries iterator rather than collect all omitted keys before mutation.", + "outcome": "survived", + "hosts": [ + "controlled", + "node-sqlite", + "actual-chromium-opfs" + ], + "history": "previous7,next0,after-restore,no mutation; PowerSync batch2", + "positiveSourceExecutionWitness": true, + "disposition": "Anchored survivor. Neither unsafe behavior nor equivalent-behavior closure established. No allowance granted.", + "artifacts": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/.review-2367/startup-perf/batching-controls/live-iterator" + }, + { + "id": "authored.live-iterator.rxdb", + "source": "packages/rxdb-db-collection/src/rxdb.ts", + "sourceSha256": "c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7", + "sourceLines": [ + 224, + 237 + ], + "mutantBodySha256": "3072c2782916417a758eab745c3038a65c57eb6a6e22d31e74fc29769342ca9a", + "returnedCodeWithExecutionSentinelSha256": "6c7e22481376aea20606d99b5817fe51a45c43e7ecedea7f50ce6d4529b844d3", + "effect": "Commit full omission batches during the acceptedSyncedEntries iterator rather than collect all omitted keys before mutation.", + "outcome": "survived", + "hosts": [ + "controlled", + "node-sqlite", + "actual-chromium-opfs" + ], + "history": "previous7,next0,after-restore,no mutation; PowerSync batch2", + "positiveSourceExecutionWitness": true, + "disposition": "Anchored survivor. Neither unsafe behavior nor equivalent-behavior closure established. No allowance granted.", + "artifacts": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/.review-2367/startup-perf/batching-controls/live-iterator" + } + ], + "integrity": { + "head": "0e86c1fdae85520aff142f141f71ccf46a8ee8bd", + "checkedCounts": { + "sources": 3782, + "buildArtifacts": 1191, + "workspaceBundles": 2, + "controls": 2, + "instruments": 13, + "instrumentFiles": 5 + }, + "changedFrozenFiles": [], + "originalCheckoutSourceLeaks": [] + }, + "termination": { + "roots": [ + 39411, + 40136 + ], + "processes": [ + { + "pid": 39411, + "ppid": 39388, + "command": "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node /private/tmp/tanstack-db-2394-gap-cli-e1fb-titles/oracle-gaps/index.mjs --package packages/powersync-db-collection --source src/powersync.ts --seed 2367 --contexts 1 --pool forks --env-prefix TANSTACK_DB_ORACLE --only powersync.ts:M40,powersync.ts:M41,powersync.ts:M79,powersync.ts:M80,powersync.ts:M81,powersync.ts:M82,powersync.ts:M83,powersync.ts:M84,powersync.ts:M85,powersync.ts:M86,powersync.ts:M87,powersync.ts:M88,powersync.ts:M89,powersync.ts:M90,powersync.ts:M91,powersync.ts:M102,powersync.ts:M103,powersync.ts:M104,powersync.ts:M172,powersync.ts:M173,powersync.ts:M175,powersync.ts:M176,powersync.ts:M177,powersync.ts:M178,powersync.ts:M179,powersync.ts:M180,powersync.ts:M181,powersync.ts:M182,powersync.ts:M183,powersync.ts:M184,powersync.ts:M185,powersync.ts:M186,powersync.ts:M187,powersync.ts:M188,powersync.ts:M189,powersync.ts:M190,powersync.ts:M191,powersync.ts:M192,powersync.ts:M193,powersync.ts:M194,powersync.ts:M195,powersync.ts:M196,powersync.ts:M197,powersync.ts:M198,powersync.ts:M199,powersync.ts:M200,powersync.ts:M201,powersync.ts:M202,powersync.ts:M203,powersync.ts:M208,powersync.ts:M209 --out /private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-powersync" + }, + { + "pid": 39424, + "ppid": 39411, + "command": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/node_modules/.pnpm/@esbuild+darwin-arm64@0.28.2/node_modules/@esbuild/darwin-arm64/bin/esbuild --service=0.28.2 --ping" + }, + { + "pid": 40136, + "ppid": 40126, + "command": "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node /private/tmp/tanstack-db-2394-gap-cli-e1fb-titles/oracle-gaps/index.mjs --package packages/rxdb-db-collection --source src/rxdb.ts --seed 2367 --contexts 1 --pool forks --env-prefix TANSTACK_DB_ORACLE --only rxdb.ts:M5,rxdb.ts:M6,rxdb.ts:M8,rxdb.ts:M9,rxdb.ts:M10,rxdb.ts:M11,rxdb.ts:M12,rxdb.ts:M13,rxdb.ts:M43,rxdb.ts:M44,rxdb.ts:M45,rxdb.ts:M51,rxdb.ts:M52,rxdb.ts:M53,rxdb.ts:M59,rxdb.ts:M60,rxdb.ts:M61,rxdb.ts:M62,rxdb.ts:M63,rxdb.ts:M64,rxdb.ts:M65,rxdb.ts:M66,rxdb.ts:M67,rxdb.ts:M68,rxdb.ts:M69,rxdb.ts:M70,rxdb.ts:M71,rxdb.ts:M72,rxdb.ts:M73,rxdb.ts:M74,rxdb.ts:M75,rxdb.ts:M76,rxdb.ts:M77,rxdb.ts:M78,rxdb.ts:M79,rxdb.ts:M80,rxdb.ts:M81,rxdb.ts:M82,rxdb.ts:M83,rxdb.ts:M84,rxdb.ts:M85,rxdb.ts:M86,rxdb.ts:M87,rxdb.ts:M88,rxdb.ts:M89,rxdb.ts:M90,rxdb.ts:M91,rxdb.ts:M92,rxdb.ts:M100,rxdb.ts:M101,rxdb.ts:M121,rxdb.ts:M122,rxdb.ts:M123,rxdb.ts:M125,rxdb.ts:M126,rxdb.ts:M127,rxdb.ts:M136,rxdb.ts:M137,rxdb.ts:M138 --out /private/tmp/tanstack-db-2394-final-adapter-gap/.retention-audit/automatic-rxdb" + }, + { + "pid": 40150, + "ppid": 40136, + "command": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/node_modules/.pnpm/@esbuild+darwin-arm64@0.28.2/node_modules/@esbuild/darwin-arm64/bin/esbuild --service=0.28.2 --ping" + }, + { + "pid": 69651, + "ppid": 39411, + "command": "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node --experimental-import-meta-resolve --require /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/node_modules/.pnpm/vitest@5.1.0_@types+node@25.2.2_@vitest+coverage-istanbul@5.1.0_@vitest+ui@5.1.0_jsdom@_681eb78251539224b85f714f069f564c/node_modules/vitest/suppress-warnings.cjs --conditions node --conditions development /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/node_modules/.pnpm/vitest@5.1.0_@types+node@25.2.2_@vitest+coverage-istanbul@5.1.0_@vitest+ui@5.1.0_jsdom@_681eb78251539224b85f714f069f564c/node_modules/vitest/dist/workers/forks.js" + }, + { + "pid": 72484, + "ppid": 40136, + "command": "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node --experimental-import-meta-resolve --require /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/node_modules/.pnpm/vitest@5.1.0_@types+node@25.2.2_@vitest+coverage-istanbul@5.1.0_@vitest+ui@5.1.0_jsdom@_681eb78251539224b85f714f069f564c/node_modules/vitest/suppress-warnings.cjs --conditions node --conditions development /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/node_modules/.pnpm/vitest@5.1.0_@types+node@25.2.2_@vitest+coverage-istanbul@5.1.0_@vitest+ui@5.1.0_jsdom@_681eb78251539224b85f714f069f564c/node_modules/vitest/dist/workers/forks.js" + } + ], + "postTerminationKnownPidsAlive": [] + }, + "runtimeDiagnosis": { + "psM40": "Driver premise failure: Error: Persisted startup driver did not reach its premise. It is a crash, not an assertion kill.", + "timeoutDetails": "CLI reports run time limit; individual source-value/begin/commit deletions commonly leave receiving operations unfinished. These outcomes do not establish their intended product-law judgment.", + "driverWait": "untilStartup checks2000zero-delay turns then throws on a missing premise.", + "cliLimit": "first limit=min(full,2000+3000*cleanFileSeconds)ms; full=10000+3000*cleanFileSeconds ms; cutoff is rerun alone at full limit.", + "observedFullLimitsMs": { + "powersyncLifecycle": 56599.942627, + "powersyncStartup": 55081.057249, + "rxdbLifecycle": 20732.240875, + "rxdbStartup": 14663.286751 + }, + "shrinkCost": "Not separately profiled. No claim that shrinking rather than repeated receiving waits dominates.", + "instrumentVerdict": "No new capture or identity flaw demonstrated; completed raw classifications retained. Timed-out, crashed, unreached and interrupted candidates are not kills or waivers." + }, + "runner": { + "path": "/private/tmp/tanstack-db-2394-run-final-adapter-gap-automatic.py", + "sha256": "18410eaa7375df034e3a1b3e1446f7063522dfb7b7a2299a8b6c7fe54c3ae625" + } +} diff --git a/docs/contributing/oracle-reviews/2026-11-10-final-adapter-changed-code-gap-audit.md b/docs/contributing/oracle-reviews/2026-11-10-final-adapter-changed-code-gap-audit.md new file mode 100644 index 0000000000..7c1f61b20b --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-final-adapter-changed-code-gap-audit.md @@ -0,0 +1,97 @@ +# Final PowerSync/RxDB changed-code gap audit + +The bounded diagnostic sweep was stopped at the parent's request after **52 of +110 selected automatic operators** completed. It produced **22 kills, +18 survivors, one crash, ten timeouts and one unreached operator**. The remaining +58 operators are unrun or interrupted, with no verdict. Separately, the eight +authored startup/work/lifecycle controls were all killed. Two authored +live-iterator alternatives survived controlled storage, native SQLite and actual +Chromium OPFS. No new production defect is proved by this supplementary sweep. +No allowance has been granted. + +| Adapter | Selected | Completed | Killed | Survived | Crashed | Timeout | Unreached | Unrun/interrupted | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| PowerSync | 51 | 4 | 0 | 2 | 1 | 1 | 0 | 47 | +| RxDB | 59 | 48 | 22 | 16 | 0 | 9 | 1 | 11 | + +The adjacent [JSON evidence](2026-11-10-final-adapter-changed-code-gap-audit.json) +preserves all 746 static operators, the exact selected source spans, contextual +keys, source hashes, replacements, complete outcomes, omitted-selection reasons, +commands, artifact hashes and distinct unrun records. The selection includes +operators on changed executable expressions in the final batching repair; it +omits unchanged function bodies. This is partial discovery, not a whole-source +or universal closure claim. + +## Anchored surviving distinctions + +Every ID below resolves to an exact source SHA, span, anchor, operator and +replacement in the JSON. These are scoped decision candidates, not automatic +recommendations for a waiver. + +| Candidates | Present distinction and risk | Needed evidence or decision | +| --- | --- | --- | +| powersync.ts:M41, rxdb.ts:M6, rxdb.ts:M11, rxdb.ts:M12 | Change the retained-reload snapshot predicate or option. Atomic publication and retained replacement may differ before the final cut. M12 changes captured observations; the other three show no captured difference. No production defect is established. | Generate and observe the intermediate retained-reload publication boundary, or obtain an exact maintainer allowance. | +| powersync.ts:M79 | Replace the required sync-write operation type with undefined. The public write protocol is not enforced for this PowerSync boundary by these two selected owners; rows alone do not distinguish it. | Add shared receiving validation of required operation type, or obtain an exact maintainer allowance. | +| rxdb.ts:M8, rxdb.ts:M65, rxdb.ts:M67 | Broaden source-key collection or the baseline-reconciliation guard. Extra collection/reconciliation can occur in retained or unwrapped runs; this persisted-startup campaign observes no difference. It does not prove equivalent behavior. | Distinguish retained/unwrapped source work and reconcile boundaries, or obtain an exact maintainer allowance. | +| rxdb.ts:M44, rxdb.ts:M45 | Remove the stop fence after an awaited source read. The true stopped branch of this specific fence is not reached by the selected histories. No evidence establishes what happens on source-read completion after retirement. | Generate retirement while the initial RxDB storage read is held, then release it, or obtain an exact maintainer allowance. | +| rxdb.ts:M60, rxdb.ts:M63, rxdb.ts:M86, rxdb.ts:M89 | Always attach the catch handler, including a hypothetical synchronous true receipt. The differentiating synchronous-receipt branch is not reached. A true receipt would not have catch; this campaign does not establish whether that receipt is legal at this boundary. | Exercise the legal synchronous receipt path or document its inapplicability with an exact maintainer allowance. | +| rxdb.ts:M87, rxdb.ts:M88, rxdb.ts:M90 | Omit the immediate catch handler for an omission-commit receipt. The selected histories do not distinguish rejected omission receipts and their reporting timing. This is a generator gap, not a demonstrated production error. | Exercise omission-receipt rejection at the relevant await/reporting boundary, or obtain an exact maintainer allowance. | +| rxdb.ts:M79 | Run one additional empty omission transaction at the end of the loop. Captured work differs but existing comparisons permit it. Whether this extra empty transaction violates an accepted work law requires an explicit authority decision. | Add a justified work comparison, or obtain an exact maintainer allowance. | + +The live-iterator controls are `authored.live-iterator.powersync` and +`authored.live-iterator.rxdb`. Both commit configured-size omission batches while +iterating accepted rows instead of copying omitted keys first. Each survived a +previous-seven/next-empty, after-restore history on all three hosts. Positive +execution sentinels authenticate the exact transformed source. These results +establish survival for those histories; they establish neither unsafe behavior +nor equivalence. Source and returned-code hashes, line ranges and all artifact +paths are retained in the JSON. + +## Runtime and unrun outcomes + +The **12 non-assertion outcomes** are PowerSync M40 (crashed), M80 (timeout), +and RxDB M5, M10, M43, M59, M72, M80, M81, M84, M85 (timeouts) and M13 +(unreached). They are not kills. M40 reports `Persisted startup driver did not +reach its premise`; that driver waits up to 2,000 zero-delay turns. The timeout +results report `run time limit`; several remove a required source value, begin +or commit and leave receiving operations unfinished. This does not identify an +assertion-level product-law failure. + +The CLI first cuts a slow file at its clean-duration-scaled short limit, then +confirms it alone at the full limit. Observed full limits were approximately +55–57 seconds for PowerSync files and 15–21 seconds for RxDB files. Repeated +receiving waits and these confirmations made automatic discovery much slower +than the eight-control pilot. Shrinking time was not separately profiled. +The parent stopped both exact runner process trees; the termination record is +retained. All 58 candidates without a final result remain explicitly unrun or +interrupted. They are not waiver proposals. + +## Receiving and instrument integrity + +The snapshot is `/private/tmp/tanstack-db-2394-final-adapter-gap`, based on +`0e86c1fdae85520aff142f141f71ccf46a8ee8bd`, with final PowerSync source SHA-256 +`c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068` and RxDB +`c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7`. +Each adapter's two clean passes captured 12 unique full test identities across +two owner files, all passed, with no setup or unhandled errors. + +The isolated diagnostic transform uses two generated runs per campaign, fixed +seed 2367, no literal fixed histories and no checker calibrations. It retains +all canonical constructed-history builders, models, drivers, comparisons and +reach checks. These compact runs are supplementary; normal repository budgets +remain unchanged. It uses Node 24.22.1, forks and one context per adapter. +The isolated CLI contains the separately reviewed expected-failure, +complete-capture and full-title-identity repairs. The installed CLI is unchanged. + +Final independent integrity verification found no changed hashes among 3,782 +snapshot inputs, 1,191 copied build artifacts, two workspace bundles, two control +files, 13 instrument modules and five orchestration files, and no imports of live +checkout source. No new instrument flaw was demonstrated. This finding does +not promote runtime failures or unexecuted candidates into evidence of closure. + +The eight authored controls rejected forced persisted snapshots, skipped restored +omissions, unbounded omission batches, raw provider IDs in PowerSync source-key +tracking and the RxDB post-hydration terminal-stream fence removal. The +batching evidence retained by the PR manifest and the +[lifecycle](2026-11-10-persisted-baseline-lifecycle.md) evidence remain the primary +normal-budget evidence. This stopped discovery audit does not replace them. diff --git a/docs/contributing/oracle-reviews/2026-11-10-persisted-baseline-lifecycle.md b/docs/contributing/oracle-reviews/2026-11-10-persisted-baseline-lifecycle.md new file mode 100644 index 0000000000..8d56a45904 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-persisted-baseline-lifecycle.md @@ -0,0 +1,242 @@ +# Persisted baseline lifecycle receiving evidence + +Reviewed base: `0e86c1fdae85520aff142f141f71ccf46a8ee8bd` plus the working +oracle receivers. This is the author's integration record. No live adapter +production file was edited for this work. Immutable test-only Vite overlays +retain the original module IDs and verify both original and candidate hashes. + +## Law and ownership + +Persisted baseline work cannot extend a retired sync run's authority. Cleanup +releases provider resources once. Accepted rows remain durable. Reload keeps +its accepted rows until its complete replacement applies. An initial provider +failure preserves its exact cause and ends the initial load. A terminal source +failure cannot later accept an omission sweep from that failed continuation. +Authorities are the shared eager owner's LIFE-2/5/10, READY-4 and HEALTH-5, +the glossary's accepted sync transaction, cleanup and reload contracts, and +TrailBase's existing persisted-hydration complete-result row judgment. + +The [receiver judgment](../../../packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle.ts) +uses a differential preservation check for retirement and the existing +TrailBase set model for replacement and subsequent live rows. It imports no +adapter transition, ownership classifier or diff helper. Source failure, +acknowledgment rejection, application and cleanup remain distinct boundaries. +The [composition driver](../../../packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-driver.ts) +settles every genuine underlying receipt before it freezes the retirement cut. +The held acknowledgment returned to the adapter does not undo application. +Candidate mode additionally requires an actual adapter `hydrateBaseline()` +invocation and holds its return after the genuine restore has completed. + +The same grammar/check receives real local RxDB memory storage and PowerSync +SQLite, each on controlled persistence, real Node SQLite and real Chromium +worker/OPFS persistence. These local SDK receiving paths make no remote service +or replication claim. PowerSync has no post-ready terminal stream-error +capability, so its grammar excludes `stream-fail`; RxDB supplies that event +through its existing Observable boundary. + +## Grammar and observation cuts + +Each normal property runs 24 generated histories at campaign seed 239467 and +24 without a supplied seed. Each state has one randomized constructed builder, +with fresh object identities and a WeakMap recording its required state. +The comparison checks generated builder outputs as well as the framework's +seed-derived constructions. Eight fixed histories, plus two RxDB stream-error +histories, replay the same model, driver and comparison. + +The domains are three prior/old/replacement keys (bitsets 0–7), batch sizes 1–2, +default or network-first rendering for cleanup/reload, and default rendering for +injected source/receipt failures. A real source error before its initial query +settles holds the actual persisted restore. After a delivered source result, +cleanup/reload/receipt rejection hold acknowledgment and candidate hydration +completion separately. RxDB also terminates the source while that genuine +hydrate return remains held. Public/durable rows, Collection status, preload +settlement, health cause identity, provider resource releases, onLoad releases, +handled diagnostics and a subsequent replacement live insert are compared. + +Reconstruction retains all fixed traces inside the generated language. Removing +the action axis loses cleanup, reload or either failure obligation; removing +empty/overlap/omission memberships loses complete-result distinctions; removing +batch size loses multi-commit receipt cuts; removing initial rendering loses +network-first lifecycle reception. Empty and full bitsets are in range. Failures +with network-first rendering are excluded by this receiver's stated grammar, +and PowerSync terminal stream errors are not legal provider events here. The +existing startup owner covers fallback/deadline semantics independently. + +## Candidate identities + +| Source | Original SHA-256 | Final frozen candidate SHA-256 | +| --- | --- | --- | +| RxDB | `2dca19cff8bb602b2aa59f46c7635035e49d0f1cda0a2222e1917f6d2eb8aa62` | `c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7` | +| PowerSync | `d945eb99bff416fc9c7ba041a802fa842072e5cf03a71d02dd2ed214610df408` | `c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068` | + +RxDB uses its existing subscription's `closed` state rather than adding an +independent active flag. Earlier RxDB candidate `df25ff89…` used an active flag; +its results below are historical. Earlier PowerSync candidate `831ea195…` +differs from the final candidate only in reading private `_lifecycle.reloading` +instead of public `health.reloading`. Final frozen files and manifests are under +`/private/tmp/pr2394-baseline-lifecycle-evidence/final-candidates`. + +## Executed results + +| Receiving path | Original | Final candidate | +| --- | --- | --- | +| RxDB controlled + Node SQLite | 46/46 GREEN | 48/48 GREEN | +| RxDB Chromium/OPFS | 23/23 GREEN | 24/24 GREEN | +| PowerSync controlled + Node SQLite | 40/40 GREEN | 42/42 GREEN | +| PowerSync Chromium/OPFS | 20/20 GREEN | 21/21 GREEN | + +The original mode sets `TANSTACK_DB_BASELINE_EXPECT_HYDRATE=0`: it proves the +acknowledgment neighbor without claiming the candidate's new await was entered. +Normal final mode requires actual hydrate invocation/held-return reach. No +live production edit may be justified by a retrospective original-mode result. + +For RxDB's final candidate, fixed/fresh counts by path are: + +| State | Controlled fixed/fresh | Node SQLite fixed/fresh | Chromium fixed/fresh | +| --- | ---: | ---: | ---: | +| accepted-before-retirement | 27/28 | 27/30 | 27/27 | +| adapter-hydrate-continuation | 27/28 | 27/30 | 27/27 | +| cleanup-held-continuation | 11/16 | 11/15 | 11/13 | +| empty-replacement | 5/7 | 5/5 | 5/9 | +| receipt-rejection-held-baseline | 4/1 | 4/2 | 4/7 | +| reload-held-continuation | 2/2 | 2/5 | 2/3 | +| replacement-omits-old | 18/16 | 18/17 | 18/14 | +| replacement-overlap | 18/17 | 18/19 | 18/12 | +| source-failure-held-baseline | 7/6 | 7/4 | 7/7 | +| stream-failure-held-continuation | 10/9 | 10/8 | 10/4 | + +These counts exclude literal fixed histories but include every seed-derived +construction and subsequent generated histories. Each minimum-one builder is +also challenged by its own permanent missing-state calibration. + +Commands, from the adapter package, disable coverage/typechecking only for the +bounded oracle execution; separate strict TypeScript checks remain GREEN: + +```sh +TANSTACK_DB_BASELINE_EXPECT_HYDRATE=0 \ +pnpm --config.verifyDepsBeforeRun=false exec vitest run \ + tests/persisted-baseline-lifecycle-oracle.test.ts \ + --coverage.enabled=false --typecheck.enabled=false --maxWorkers=1 + +TANSTACK_DB_BASELINE_CANDIDATE_DIR=/private/tmp/pr2394-baseline-lifecycle-evidence/final-candidates \ +pnpm --config.verifyDepsBeforeRun=false exec vitest run \ + --config /private/tmp/pr2394-baseline-lifecycle-evidence/rxdb-candidate.config.ts \ + tests/persisted-baseline-lifecycle-oracle.test.ts --maxWorkers=1 +``` + +PowerSync native commands invoke the validated Node 24.22.1 executable directly, +prepend its directory to PATH, use `--pool=forks --maxWorkers=1`, and serialize +native runs with the other PR campaigns. The same runtime’s provider-selection probe reports ABI 137 and +`better-sqlite3`; the driver passes that exported selection to PowerSync. +Its bounded gate adds `-t 'fixed history:'`. +Browser commands run from the browser persistence package using +`test:oracles:adapters-browser tests/persisted-baseline-lifecycle-oracle.test.ts` +with an adapter name filter; the same candidate directory enables the overlay. + +PowerSync final Chromium fixed/fresh reach is accepted/hydrate 29/27, +cleanup 13/14, empty replacement 6/11, receipt rejection 7/7, reload 9/6, +omission 20/25, overlap 20/13 and initial source failure 4/6. Original Chromium +fixed/fresh reach is accepted 27/25, cleanup 16/16, empty 6/5, receipt rejection +5/5, reload 6/4, omission 18/17, overlap 17/14 and source failure 5/7. +Both runs include all fixed histories, builder calibrations and determinism. +PowerSync final controlled fixed/fresh reach is accepted/hydrate 29/22, +cleanup 13/13, empty 6/3, receipt rejection 7/4, reload 9/5, omission 20/13, +overlap 20/13 and source failure 4/11. Final Node SQLite fixed/fresh reach is +accepted/hydrate 29/26, cleanup 13/16, empty 6/8, receipt rejection 7/6, reload +9/4, omission 20/14, overlap 20/17 and source failure 4/7. Both normal properties +pass all 42 checks. Original controlled and Node SQLite pass all 40 checks; +original fixed counts equal the original Chromium fixed counts above, and +fresh controlled counts are accepted 27, cleanup 14, empty 13, receipt rejection +6, reload 7, omission 19, overlap 11 and source failure 5. Fresh Node SQLite +counts are accepted 26, cleanup 15, empty 10, receipt rejection 4, reload 7, +omission 14, overlap 15 and source failure 6. + +## RED controls and model corrections + +Removing only RxDB's post-hydrate subscription fence produces candidate +`4d89712fb798866ce12394772c119f777f7e225ffb0090be43c7af326a5b5182`. +All four fixed stream-error comparisons on controlled and Node SQLite reject +it at checkpoint `lifecycle`, law `persisted source lifecycle`, difference +`accepted obligations changed after terminal source failure`. Both histories +have prior bitset 3, old bitset 1 and batch size 1, with replacement 0 or 6. +The log contains a positive exact transform/hash witness. This proves an +observable public/durable distinction for the fence, without imposing a +zero-internal-work law. Both fixed Chromium stream-error comparisons reject +the same mutant at the identical law/checkpoint, with an exact hash witness. +The clean final candidate passes those two histories again. + +A generated-only recorder control suppresses `cleanup-held-continuation` only +on a generated builder output whose serialized history differs from every +seed-derived construction in the running campaign. A test-only framework +transform records the seed-derived signatures; the shared model, production +observations and comparisons remain unchanged. A fresh run draws campaign seed +`-661318797`, then fails at path `29:0:0:0:0`, checkpoint `reach`, law +`constructed builder reach`, difference +`generated construction omitted cleanup-held-continuation`. It shrinks from +prior 0/old 4/replacement 5/batch 2 to all-empty memberships/batch 1, cleanup, +default rendering. The registered owner’s guarded replay records exactly one +completed execution; +mutant witness has `failed:true`, clean witness `failed:false`. + +Several initial red results indicted the proposed test rule, not production: + +- Requiring reload to reject the Collection's retained preload promise was + wrong. Reload keeps that promise and replacement readiness fulfills it; + cleanup rejects it with AbortError. The original trace remains in + `/private/tmp/pr2394-baseline-lifecycle-rx-fixed.log`. +- A synthetic acknowledgment rejection does not itself retire the source. + The first model froze omitted saved rows too soon. Counterexample prior 1, + old 0, replacement 0 validly accepted omission before `fail()` observed the + rejection. The corrected judgment requires every delivered source row, + permits each omitted saved row to remain or leave, and still checks exact + failure identity, no successful readiness, resource release and handled + rejection. `/private/tmp/pr2394-baseline-lifecycle-rx-candidate.log` retains + the rejected prediction. No production repair was made for it. +- Before a genuinely held restore completes, the persisted wrapper may defer + public failure. The adapter must report the exact error and release its + provider while held; public failure is checked after restore. In RxDB's + terminal stream-error branch the genuine restore already completed, so + public failure is required while only the adapter return remains held. +- PowerSync diagnostics contain an opaque random private tracking-table + suffix. Determinism rejected two equal failures with different suffixes. + The recorder now replaces only `__rows_tracking_[hex]`, keeping message + remainder, order/count and injected error identity. This changes no HEALTH-5 + exception. The unprojected failures remain in the original browser logs. +- An early no-fence run had no positive transform witness and used stale Vite + cache. Its survival is discarded. Content-keyed plugin/cache names and + positive exact hash output establish the credited mutant execution. + +## Integration checks, limits and review + +Strict `tsc --noEmit` passes for RxDB, PowerSync and browser persistence. +Scoped ESLint passes and Prettier was applied to every owned file. Branch-wide +`lint:changed` completed with exit 0 after the support edits. Existing Playwright +portfolios and run budgets remain; the dedicated browser config receives these +additional properties and default Vitest discovery excludes their browser-only +owners. Property registration and direct-replay metadata are maintained by the +parent integration agent. + +ORC-001/2/3/5/9/14: accepted authorities, independent judgment, prose and shared +receiving comparisons are above and beside executable code. ORC-004/7/15: +grammar controls, equal budgets, replay and enforced generated reach are above. +ORC-006/10/13: wrong durable rows and per-builder omissions calibrate the same +check; the genuine stream-error fence mutant rejects a nearby wrong design; +cleanup preserves primary errors through `withOracleCleanup`. ORC-008 does not +apply to a new state machine: this receiver adds a differential preservation +law and imports the existing row judgment. ORC-011: an independent reviewer +challenged the mistaken receipt-retirement prediction against the contract. +Its correction is documented above. No remaining concrete shared-semantic-fault +hypothesis requiring a different formulation was identified. This is not an +exhaustive semantic search. ORC-016: every fixed history uses the +same declared grammar/check; no ordinary unit tests were added to oracle files. +ORC-012: this record separates executed results, corrections and remaining gates. + +Limits: one prior run and one replacement, three keys, local SDK data, no remote +replication, arbitrary concurrent user mutation, failed storage I/O, arbitrary +partial-construction cleanup, synchronous Observable registration failure or +all intermediate publication states. The receiver proves lifecycle behavior +at the named cuts; startup cost and batching cardinality belong to separate +measurement and work-refinement evidence. No whole-PR readiness or universal +bug-class closure is claimed. These finite lifecycle receiving gates pass; +primary startup, retained reload and work-refinement gates remain separate +evidence. diff --git a/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-batching.md b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-batching.md new file mode 100644 index 0000000000..597289180b --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-batching.md @@ -0,0 +1,257 @@ +# Persisted eager startup batching, 2026-11-10 + +This record extends the shared persisted-startup judgment with deterministic +work observations. It does not establish a latency or heap threshold. + +## Law and boundary + +The maintainer requested preservation of PowerSync's ordinary first-startup +batch commits and RxDB's existing one full-load source commit. Persistence +must not turn those initial loads into a complete storage replacement. +Retained nonempty reloads keep atomic snapshot replacement. Network-first +fallback is still first startup; readiness alone does not identify a reload. + +The existing independent set model continues to judge complete public rows, +durable rows, refresh fulfillment and local-only reopen. Its accepted contract +is unchanged. The additive work comparison permits skipping equal source rows. +Source row writes cannot exceed the source input count. Deletes cannot exceed +saved omissions. PowerSync's individual source and omission commits contain at +most the configured B rows. RxDB has at most one row-bearing source commit. +Neither initial path may select snapshot mode or call truncate. + +The observation recorder wraps the adapter's begin/write/truncate/commit API +before persistence wraps it. It supplies no writes or readiness. The first +markReady ends the initial work observation; later refreshes cannot change it. +The work comparison runs only after the existing public/durable/reopen checks. + +## Pre-edit receiving evidence + +Baseline head: `0e86c1fdae85520aff142f141f71ccf46a8ee8bd`. Both adapter sources +remained unchanged throughout the following runs: + +| Source | SHA256 | +| --- | --- | +| PowerSync | `d945eb99bff416fc9c7ba041a802fa842072e5cf03a71d02dd2ed214610df408` | +| RxDB | `2dca19cff8bb602b2aa59f46c7635035e49d0f1cda0a2222e1917f6d2eb8aa62` | + +The same shared model and composition run on controlled persistence, real +Node SQLite and actual Chromium worker/OPFS. PowerSync uses its real local +SQLite SDK on each host. RxDB uses its real memory-storage SDK. These are local +storage witnesses; they do not establish remote replication behavior. + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/powersync-db-collection exec vitest run tests/persisted-startup-conformance-oracle.property.test.ts --coverage.enabled=false -t 'fixed history' +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/rxdb-db-collection exec vitest run tests/persisted-startup-conformance-oracle.property.test.ts --coverage.enabled=false -t 'fixed history' +``` + +PowerSync reports 64 intended work violations per Node host (128 total). +RxDB initially reported 26 per host (52 total). A further receiving pass adds +three-omission histories and reports 32 per host (64 total). Every case reaches the existing final set +comparisons before rejection of forced snapshot replacement. PowerSync also +exceeds B=1 in 14 histories and B=2 in four. + +The focused actual Chromium receiver runs the expanded 64/32 histories and +reports the same intended work failures, without page errors or leftover OPFS +artifacts. Its effective Vite configuration has `server.watch === null` and an +empty actual watcher inventory. An earlier HMR-enabled attempt and a stock +Playwright server-probe error are setup diagnostics, not this receiving proof. + +The PowerSync receiving grammar then adds a source-ID representation axis. +PowerSync's documented schema/deserialization transforms do not require output +IDs to equal provider IDs. A prefixed provider ID deserializes to the same +Collection key used by the independent set model. The driver records the raw +IDs that actually pass through deserialization. The candidate records that +already-deserialized output key, without parsing twice or changing on-demand +raw-ID operation selection. + +Identity histories remain intact. Only nonempty sources get a prefixed +counterpart, because encoding an empty source changes no behavior. That gives +104 effective PowerSync histories; an earlier 128-history diagnostic contained +24 inert encoding duplicates and is not the final count. Both Node hosts +reject all 104 at the intended work comparison (208 total). Static Chromium +rejects PowerSync104 and RxDB32, with no page errors or artifacts. The +source-key-transform premise is reached 40 times. Production hashes above +remain unchanged. + +The final pre-edit Chromium reach counts are: + +| State | PowerSync | RxDB | +| --- | ---: | ---: | +| response-before-restore | 60 | 18 | +| restore-before-response | 44 | 14 | +| network-first-source-result | 36 | 10 | +| network-first-fallback | 24 | 8 | +| empty-result | 24 | 12 | +| empty-baseline | 16 | 4 | +| overlapping-update | 40 | 10 | +| omitted-row | 88 | 28 | +| persisting-mutation | 52 | 16 | +| source-below-batch | 36 | — | +| source-at-batch | 32 | — | +| source-above-batch | 36 | — | +| omissions-above-batch | 12 | — | +| paged-single-source | — | 4 | +| omissions-above-read-batch | — | 6 | +| source-key-transform | 40 | — | + +Every new declared state also has a randomized constructed builder in each +Node campaign. Fixed histories include B=1/B=2 and three saved rows omitted by +an empty source, with both restore orders, fallback and mutation variants. +The no-mutation form makes synchronous omission application possible and tests +a proposed live-iterator alternative. Its executed outcome is recorded below. + +Raw pre-edit logs and result data are retained in +`.review-2367/startup-perf/batching-preedit-node-final-red.log`, +`batching-preedit-rxdb-node-final-red.log`, +`batching-preedit-opfs-static-red.log` and +`batching-preedit-opfs-static-result.json`. The expanded RxDB run is +`batching-preedit-rxdb32-node-red.log`, +`batching-preedit-opfs-rxdb32-red.log` and +`batching-preedit-opfs-rxdb32-result.json`. Final representation evidence is +`batching-preedit-powersync104-node-red.log`, +`batching-preedit-opfs-transformed104-red.log` and +`batching-preedit-opfs-transformed104-result.json`. + +## Repaired receiving evidence + +The final source pair was integrated only after the pre-edit receiving failures, +complete-result/work GREEN, retained reload and hydration-lifecycle gates: + +| Source | Final SHA256 | +| --- | --- | +| PowerSync | `c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068` | +| RxDB | `c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7` | + +The complete PowerSync startup suite passes 254 tests; RxDB passes 104. These +include both persistence hosts, every fixed history, the unchanged 80-history +fixed/fresh campaigns, constructed states and calibrations. The static actual +Chromium receiver passes PowerSync104/RxDB32, including all required work and +network-first states. Its page errors and remaining OPFS artifacts are empty. +The unchanged retained-reload judgment passes 44 tests per adapter on Node and +24 histories per adapter in actual Chromium. These checks preserve atomic +nonempty reloads and optimistic-overlay observations. + +The [hydration-lifecycle record](2026-11-10-persisted-baseline-lifecycle.md) +records the exact final pair's cleanup/reload/terminal-source-error gates and +post-hydration fence mutants on all three hosts. RxDB uses its existing +Subscription's `closed` value rather than another active flag. A rejected +acknowledgment after real application does not authorize abandoning already +accepted work; the production source does not await full visibility before its +omission comparison. + +Raw results are `batching-candidate-{powersync,rxdb}-node-green.log`, +`batching-candidate-opfs-green-result.json`, +`batching-candidate-{powersync,rxdb}-reload-node-green.log`, and +`batching-candidate-reload-opfs-green-result.json` under +`.review-2367/startup-perf/`. The PowerSync startup run used Node24.19.0 and the +real SDK's better-sqlite3 implementation. Later Node controls used Node24.22.1, +one fork worker, and that same real SDK. These are separate from the benchmark's +explicit node:sqlite source configuration. + +## Authored controls and limits + +Each focused control reuses the owning history, production driver and full +comparison. An immutable overlay supplies the mutated source. A runtime +sentinel verifies its exact SHA256; selected-check counts are enforced. Node +uses controlled persistence and real SQLite; Chromium uses actual worker/OPFS. + +| Control | Node checks | Actual Chromium checks | Outcome | +| --- | ---: | ---: | --- | +| Skip PowerSync omission deletion | 2 | 1 | Startup row comparison rejects all | +| Skip RxDB omission deletion | 2 | 1 | Startup row comparison rejects all | +| Track raw PowerSync provider IDs instead of deserialized Collection keys | 4 | 2 | Startup row comparison rejects all | +| Delete omission batches while iterating accepted rows, PowerSync | 2 | 1 | Survives | +| Delete omission batches while iterating accepted rows, RxDB | 2 | 1 | Survives | + +The last two controls are surviving alternative algorithms, not evidence of a +rejected defect. Deleting yielded Map keys need not skip subsequent keys, and +persistence can queue application. These receiving histories do not demonstrate +an iterator failure. The repair copies omitted keys before commits to keep its +traversal separate from application; no broader necessity claim follows. +Forced replacement is rejected by all pre-edit work runs and the normal +replacement/truncate calibration. A failure to reach a comparison, missing +source sentinel, or zero-test selection is not counted as rejection. + +The omission history has three saved rows, an empty complete result, restore +before response, no optimistic mutation, and PowerSync B=2. The raw-ID history +has an empty baseline, three source rows, response before restore, B=2, and the +prefixed-ID transform, crossing default/network-first initial render. +Browser runs have no page errors or remaining OPFS artifacts. A favicon 404 is +an unrelated resource diagnostic. Node results and source manifests are under +`.review-2367/startup-perf/batching-controls/`; each control has its own logs and +`opfs-result.json`. Earlier zero-selected-test diagnostics are explicitly +unreached and excluded. Post-repair measurements remain separate below. + +## Source-weight tradeoff + +The candidate reuses PowerSync's existing batch-reading loop. One optional +afterRead hook reconciles omissions before the existing source read lock is +released. One optional initial-row hook records the already-deserialized +Collection key. On-demand operation selection keeps its original raw-ID input +and evaluation order. No second load loop or parallel startup state machine is +introduced. RxDB keeps its existing one full-load source commit. + +Both candidates retain source keys and copy only omitted keys before deletion +commits. They avoid retaining every source payload batch for a fresh persisted +startup. They do not promise O(B) total memory: key storage remains O(N), and +existing core/persistence queues can retain source work. Retained nonempty +reloads retain their atomic snapshot path. + +Unconditional snapshot selection is smaller in shipped code, but the pre-edit +work comparisons and earlier measurements expose its startup cost. A generic +wrapper omission sweep would need a complete-result boundary and scope +authority from each source; the wrapper cannot infer those from an ordinary +commit. A skip-equal diff is a further possible optimization, not part of this +repair. Final source size is measured separately; this record makes no +net-negative code-weight claim. + +The final real-path source lint, strict types, formatting and size builds pass. +Against `858c8f0bf`, package gzip/brotli deltas are PowerSync +269/+276 bytes, +RxDB +245/+237, Query +53/+31, TrailBase +71/+69, and core DB zero. The total +is +638/+613 bytes. The representative application changes +58 gzip / -40 +brotli bytes. These are build measurements, not an additive application-cost +prediction. Source hashes above are unchanged after these checks. + +## Post-repair measurement plan, declared before execution + +After the receiving gates and source integration, run 10,000 and 50,000 +matching saved/source rows for both adapters. Cross natural startup and +network-first fallback (25 ms deadline, 350 ms source hold). Keep 256-byte row +payloads and a source read batch of 1,000. Each cell has one discarded warmup +per variant and three measured fresh processes per variant, with alternating +variant order: 64 total observations, 48 measured and 16 warmups. + +The prior-selection comparator uses SHA-guarded adapter source from +`0e86c1fdae85520aff142f141f71ccf46a8ee8bd`, with only its forced-snapshot operand +removed. The repaired variant captures current adapter source before bundling. +Both variants use the same current core and installed dependencies. This +compares repair cost against prior ordinary startup, not against a full main +checkout. Matching input avoids the comparator's known omitted-row defect. +The JSON records each source input and output bundle SHA256. + +```sh +node scripts/bench/persisted-startup.mjs --rows=10000,50000 --saved=true --schedules=natural,fallback --repetitions=3 --warmup=1 --out=.review-2367/startup-perf/repaired-final.json +``` + +Retain source-applied and preload readiness intervals, sampled heap/RSS maxima, +baseline memory and transaction/write/truncate counts. Pause test/build/mutation +workers during these paired measurements. Report all declared cells and their +spread; do not turn a sampled peak into an allocation claim or create a new +numeric product threshold. Preserve the earlier forced-snapshot measurements +as historical evidence rather than pooling them with repaired-source samples. + +## Post-repair measurements completed + +The full declared 64-process matrix completed without failures, with final +source hashes unchanged. The [separate measurement record](2026-11-10-persisted-startup-measurements.md#post-repair-experiment) +reports all eight cells, spreads, sampled heap/RSS and work counts. Every +repaired run has zero initial snapshots/truncates. PowerSync retains 11/51 +source commits at 10,000/50,000 rows; RxDB retains one. Both still send N durable +row writes. Early fallback readiness remains before the 350 ms held source. + +The results are mixed rather than a universal performance-preserved result. +RxDB's 10,000-row natural median rises 7.2 ms and its 50,000-row natural sampled +heap rises 10.7 MiB. PowerSync's 50,000-row medians are lower; its natural sampled +heap rises 2.6 MiB. No threshold or O(B) total-memory claim follows. Raw data is +appended under experiment `repaired-final` in the checked-in CSV, separately +from historical forced-snapshot measurements. diff --git a/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-measurements.md b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-measurements.md index f68adb0327..31d0c1029a 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-measurements.md +++ b/docs/contributing/oracle-reviews/2026-11-10-persisted-startup-measurements.md @@ -1,5 +1,9 @@ # Persisted eager startup: latency and memory +The first experiment below records the earlier forced-snapshot implementation. +The [post-repair experiment](#post-repair-experiment) measures the final batching +repair separately. Historical and repaired-source samples are not pooled. + Measured source: `5feb26616a8357853454c2f9ff2ce0b0adb9c908`. The measurement task is separate from the correctness oracle campaign. It does not establish a new startup-time or memory contract. @@ -190,3 +194,121 @@ HTTP-403 recovery overrides remain uncommitted: query-core 5.107.1 → 5.106.0, fake-indexeddb 6.2.8 → 6.2.7, and tailwindcss/postcss 4.3.5 → 4.3.4. Those packages do not define the compared SDK or SQLite implementation. The exact unrecovered lockfile still lacks a local measurement. + +## Post-repair experiment + +This experiment ran after the shared startup, retained-reload and +hydration-lifecycle receiving gates passed on both Node persistence hosts and +actual Chromium OPFS. The [batching record](2026-11-10-persisted-startup-batching.md) +declared its complete matrix before execution. It preserves ordinary first +startup commits and removes initial truncation; nonempty retained reloads keep +atomic replacement. It does not impose a new timing or memory threshold. + +The exact repaired sources are PowerSync +`c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068` +and RxDB +`c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7`. +The working tree's base head is +`0e86c1fdae85520aff142f141f71ccf46a8ee8bd`; these source hashes identify the +uncommitted repair independently of that base head. + +The comparator captures immutable adapter source from that base head and +removes only its forced-persistence snapshot operand. Its resulting source +hashes are PowerSync +`d4023262e1073beec841a8ed46aed7b3190141a56fed8ecca4ee632b76f6ca27` +and RxDB +`87e1a38f369b2816218f0cd795cac2bb23854f373c0fa72f07a10d2833daa66b`. +Both variants share current core and dependencies. The final minified bundle +hashes are prior-selection +`d60365cae258e633ea8d3b9ce4f09ce6e0cffd6e57e65b421106758c60690a85` +and repaired +`33b298d1370d0d6e3d80f58a751c216a2f852cbe4fa9b39d865269e5bbcdc0b9`. + +This comparison measures repair cost against ordinary prior startup. It is not +a full main-checkout comparison or a direct comparison against the historical +forced-snapshot experiment above. Matching saved/source rows avoid the +comparator's known omitted-row defect. Correctness for changed/omitted rows +belongs to the separate shared oracle evidence. + +The declared matrix completed all **64 fresh processes: 48 measured and 16 +warmups across eight cells**. It crosses both adapters, 10,000/50,000 matching +saved rows, natural startup and network-first fallback. Every cell has one +warmup and three measured processes per variant, alternating variant order. +Payloads are 256 bytes; source read batches are 1,000. Fallback uses a 25 ms +deadline and 350 ms source hold. Source/import preparation is excluded; the +persisted database and adapter are reopened before timing; OS caches stay warm. +The real PowerSync source uses explicit node:sqlite; the second persisted +SQLite database and real RxDB memory-storage source match the harness described +above. Node v24.19.0, Apple M5 Max, 18 logical CPUs and 64 GiB RAM are unchanged. +Team test/build/mutation workers were paused throughout this timed run. + +### Full source result and sampled memory + +Times are source result applied in milliseconds, median [minimum–maximum] of +three processes. Heap/RSS are the median absolute sampled startup maxima in +MiB. Sampling and its lower-bound caveat remain as described above. + +| Adapter | Rows | Schedule | Prior ms [range] | Repaired ms [range] | Prior/repaired heap MiB | Prior/repaired RSS MiB | +| --- | ---: | --- | --- | --- | --- | --- | +| rxdb | 10,000 | natural | 165.3 [165.3–173.4] | 172.5 [171.5–176.6] | 143.8 / 146.5 | 334.3 / 339.7 | +| rxdb | 10,000 | fallback | 495.6 [494.4–510.6] | 504.8 [497.9–519.3] | 139.9 / 143.0 | 338.5 / 335.3 | +| rxdb | 50,000 | natural | 1114.9 [1048.1–1122.2] | 1092.4 [1045.3–1118.7] | 340.8 / 351.5 | 677.9 / 680.3 | +| rxdb | 50,000 | fallback | 1324.4 [1297.9–1355.2] | 1301.9 [1275.6–1386.4] | 342.6 / 352.9 | 679.6 / 679.4 | +| powersync | 10,000 | natural | 134.9 [133.1–159.1] | 135.5 [135.5–137.7] | 117.2 / 117.6 | 411.4 / 414.7 | +| powersync | 10,000 | fallback | 493.6 [492.3–511.9] | 473.3 [467.1–488.0] | 107.7 / 117.9 | 409.8 / 414.6 | +| powersync | 50,000 | natural | 772.5 [657.9–785.9] | 679.2 [619.2–679.8] | 213.9 / 216.5 | 667.5 / 674.2 | +| powersync | 50,000 | fallback | 1008.0 [958.4–1055.5] | 884.1 [881.0–914.7] | 246.0 / 216.4 | 701.7 / 670.2 | + +RxDB's 10,000-row natural median increases 7.2 ms (+4.4%); the sampled ranges +overlap. Its 50,000-row natural sampled heap increases 10.7 MiB (+3.1%), with +2.4 MiB higher sampled RSS. Its 50,000-row timing medians are lower. PowerSync's +10,000-row natural timing is similar; its 50,000-row natural and fallback +medians are lower. These mixed results do not establish a universal +no-regression claim or a noise bound. + +PowerSync's 50,000-row natural heap is 2.6 MiB higher in the repaired variant. +Under fallback it is 29.6 MiB lower, where the prior selection already chooses +one snapshot after saved rows become available. This is a paired observation +within this experiment. Differences from the earlier historical experiment +cannot be attributed solely to the repair. + +### Network-first readiness and work + +Fallback preload fulfills with saved rows before the 350 ms held source in all +measured and warmup processes. The current preload intervals are distinct +from complete source result application: + +| Adapter | Rows | Prior preload ms [range] | Repaired preload ms [range] | +| --- | ---: | --- | --- | +| rxdb | 10,000 | 28.9 [27.0–29.2] | 27.5 [27.2–30.0] | +| rxdb | 50,000 | 118.8 [115.9–125.0] | 121.7 [115.7–125.1] | +| powersync | 10,000 | 24.9 [24.8–33.7] | 34.5 [26.6–34.6] | +| powersync | 50,000 | 127.3 [119.0–131.9] | 130.2 [118.4–137.1] | + +Every repaired observation has zero initial snapshots and zero durable +truncates. PowerSync preserves 11/51 source commits for 10,000/50,000 rows: +10/50 row-bearing batches plus one empty final batch. Persistence receives 10/50 +durable transactions. RxDB preserves one source/durable transaction. Under +fallback the prior PowerSync selection instead uses one snapshot and truncate. + +**Both variants still write all N source rows durably**, including matching +saved rows. This repair removes initial truncation and full source-payload +retention across PowerSync batches. It does not implement a skip-equal diff, +O(B) total memory, or fewer durable row writes than ordinary prior startup. +The listed keys and omitted-key array can take O(N) space. All 64 workers check +the final Collection and durable SQLite row counts after the timed interval. + +The raw [CSV](../../../scripts/bench/persisted-startup-2026-11-10.csv) appends +these 64 observations with experiment `repaired-final`, retaining warmup flags +and baseline memory. Task-local `repaired-final.json`, `repaired-final.log`, +and `repaired-final-summary.json` retain source/bundle hashes and complete +results. These observations are not pooled with earlier data. + +```sh +node scripts/bench/persisted-startup.mjs --rows=10000,50000 --saved=true --schedules=natural,fallback --repetitions=3 --warmup=1 --out=.review-2367/startup-perf/repaired-final.json +``` + +The earlier environmental and measurement limits still apply: local warm-cache +startup on one desktop, three measured repetitions, no UI-paint, cold-device, +remote-replication or browser-memory claim. The correctness oracle separately +covers actual Chromium OPFS and differing source results. diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md index 591cdbc63d..3883f13d27 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-external-review.md @@ -179,6 +179,18 @@ case was correct. The review also found real costs and stale documentation. Its admission-contract, package-consumer, and wrapper-centralization conclusions were overstated, and running no tests left important distinctions unverified. +## Subsequent batching repair + +The R4/R5 cost classification above records the reviewed snapshot implementation. +The maintainer subsequently required preservation of ordinary startup commits. +The [batching extension](2026-11-10-persisted-startup-batching.md) adds a separate +deterministic work judgment, demonstrates RED before edits on both Node hosts +and actual Chromium/OPFS, then preserves PowerSync batch commits and RxDB's +existing source commit. Both adapters reconcile only saved omissions after +hydration. Retained nonempty reloads keep atomic snapshots. This supersedes +the forced-snapshot startup implementation, while preserving the historical +finding and measurements. It does not introduce a total-memory bound. + The review accounting and authorized repairs are complete. Whole-PR readiness is not assessed here. The prior gap CLI audit remains incomplete, and the final amendment still needs CI verification. No elapsed-time performance claim follows diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md new file mode 100644 index 0000000000..e1fd9728a7 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md @@ -0,0 +1,108 @@ +# PR 2394 integrated validation + +This record follows merge head +`0e86c1fdae85520aff142f141f71ccf46a8ee8bd`, with target main +`858c8f0bf747e80f07d2fd6b10d8050bd0c84f8b`. Exact repaired source SHA256: + +| Source | SHA256 | +| --- | --- | +| Query | `1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b` | +| PowerSync | `c4960f0c00aae28400de1811285458995c5b61591bd7ce42353f99ad6ab47068` | +| RxDB | `c78f7409f9e539a6bf575718f36a91309c191b18638be29a7d87b481914e65f7` | +| TrailBase | `a0608813edaa2bc966ca6fbdbeb747b9fc6fbd8dd08a9c6edabbfe75288d2cd7` | + +The [first review](2026-11-10-pr-2394-external-review.md) retains all ten raw +findings: five fixed, two refuted and three accepted under the contract then +in force. The [re-review](2026-11-10-pr-2394-rereview.md) retains all ten raw +findings: six fixed, two refuted and two accepted. Those historical verdicts +and counts remain intact. Maintainer steering subsequently required ordinary +startup commits, superseding the earlier accepted forced-snapshot cost. + +The [batching extension](2026-11-10-persisted-startup-batching.md) demonstrates +work-law RED before source edits, then correct rows and bounded commits on +controlled persistence, native SQLite and actual Chromium worker/OPFS. The +[Query extension](2026-11-10-query-startup-receiving-gaps.md) separately exposes +and fixes a same-run direct value missing from the saved baseline. Both fixes +have generated histories, receiving evidence and original-defect controls. +The [hydration lifecycle extension](2026-11-10-persisted-baseline-lifecycle.md) +preserves cleanup, reload, terminal failure and accepted-work obligations across +the new continuation. These are distinct laws and share no production-derived +expected result. + +## Final local checks + +The full Query package oracle command passes 1,663 tests in 38 files, with +no skips or type errors. The command is: + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/query-db-collection test:oracles --maxWorkers=1 +``` + +The final metadata check passes all four selected registry/owner/directory +assertions in `packages/db/tests/oracle-replay.test.ts`. The other 114 tests +are outside this selector; the earlier complete 118-test run remains separate. +Both primary PowerSync/RxDB replay commands execute exactly one requested +property, campaign seed `2118` and path `0`, through `oracle-replay.ts` using Node 24.22.1 +and one fork. Both return the matching owner, coordinates and nonzero witness. +These replays do not replace their normal campaigns in the linked records. + +Strict TypeScript checks pass for PowerSync, RxDB, adapter conformance and +browser persistence. Core DB also passes with `--rootDir .` from the repository +root: its included Vite config imports a root-level build script. The initial +package-root check reports TS6059 for that config import; no source edit is +needed for the repository-root check. Source ESLint, production builds, `pnpm lint:changed`, +the shrinking legacy-campaign check and `git diff --check` pass. The legacy +check reports no additions. Formatting repairs affect only presentation. +The new browser receivers have a separate E2E matrix group so their runtime +does not consume the existing browser group's timeout. + +Production size comparison against main `858c8f0bf` reports core DB zero, +Query +53 gzip bytes, TrailBase +71, RxDB +245 and PowerSync +269. Combined +growth is 638 gzip / 613 brotli bytes. The typical app changes +58 gzip / -40 brotli +bytes. The implementation reuses existing source loops and lifecycle fences; +scope/exhaustion information prevents a wrapper-only omission sweep. The +batching record explains the remaining source-weight cost. Test and document +growth does not ship in these bundles. + +The [post-repair benchmark](2026-11-10-persisted-startup-measurements.md#post-repair-experiment) +completes 64 fresh processes, 48 measured and 16 warmup, with no competing team workers. +An independent CSV recomputation confirms all eight cells and their medians, +and zero repaired startup snapshots/truncates. The earlier 112 observations +remain a separate historical experiment. The results retain measured small +overheads and do not establish universal performance preservation. + +## Mutation evidence and unresolved decisions + +The [conversion audit](2026-11-10-startup-property-gap-retention.md) retains all +55 earlier stable kills. The [primary eager conversion](2026-11-10-eager-source-primary-conversions.md) +retains twelve selected RxDB/PowerSync kills. The Query changed-callback audit +records 24 kills and 11 survivors; separate receiving extensions subsequently reject +the four retired-position mutations and the missing sync-write operation. +That additive evidence does not rewrite the original CLI score. + +Six anchored Query survivors remain: M1314, M1327, M1328/M1330/M1332 and M1474, +as described in the [callback inventory](2026-11-10-query-changed-callback-gap-audit.md). +TrailBase M151 remains the post-hydration cancellation guard survivor. The +authored PowerSync/RxDB live-iterator alternatives also survive on all three +hosts; their copying alternative is not proved necessary by these histories. +No maintainer allowance is presumed. A coverage-map entry or an equivalence +argument does not close these gaps under AGENTS.md. + +The [final changed-adapter audit](2026-11-10-final-adapter-changed-code-gap-audit.md) +completed 52 of 110 selected automatic operators before discovery stopped: +22 kills, 18 survivors, one crash, ten timeouts and one unreached operator. +The remaining 58 are unrun or interrupted. Eight authored controls are killed; +the two authored live-iterator alternatives remain survivors. These outcomes +are separate from the normal campaigns and prior retained kills. No new +production defect is proved by the supplementary sweep. + +Together with the six Query and one TrailBase survivors above, the final audit +leaves 27 anchored survivors requiring a maintainer decision or additional +enforcement evidence. Runtime failures and unexecuted operators are not kills +or allowances. Pushed-head CI and the explicit gap decision remain separate +gates. This record makes no PR-readiness claim while those gates are open. + +Local installation recovery remains uncommitted: query-core 5.107.1→5.106.0, +fake-indexeddb 6.2.8→6.2.7 and tailwindcss/postcss 4.3.5→4.3.4. Workspace override +and lockfile hashes remain `ef167cca76c028d75a96818c987373ef213c8fff152ae4ff5aa43da3adea79b5` +and `597c414dc296075582b34c76d10e71caac19d57a33b81a97f74f79ec10b225e1`. diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-rereview.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-rereview.md index 54c88d00c0..759d482bb9 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-rereview.md +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-rereview.md @@ -6,6 +6,15 @@ no install, and added no dependency overrides. This record is self-review with executable verification. The earlier review's silence on a finding supplies no verification; the affected neighboring laws are rerun here. +This record retains the evaluated head's evidence. The later +[batching repair](2026-11-10-persisted-startup-batching.md) supersedes RR4's +forced-snapshot startup implementation after maintainer steering. Its additive +work judgment rejects that path before edits, then verifies preserved source +commits and omission-only deletion on both Node hosts and actual Chromium/OPFS. +The [current Query receiving record](2026-11-10-query-startup-receiving-gaps.md) +also documents a subsequently discovered direct-value defect and its repair. +Historical totals below are not final-head totals. + ## Laws and experiments A complete eager result determines the rows and returned values in its scope. diff --git a/docs/contributing/oracle-reviews/2026-11-10-primary-eager-conversion-retention.json b/docs/contributing/oracle-reviews/2026-11-10-primary-eager-conversion-retention.json new file mode 100644 index 0000000000..517784d1f0 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-primary-eager-conversion-retention.json @@ -0,0 +1,576 @@ +{ + "head": "777935fd68daa497fdd3c9db533845b710be68a1", + "parent": "c4c00b20b6c91549a2b9fe502096d0841520f3fe", + "seed": 2367, + "requiredStableOperatorKills": 12, + "retainedStableOperatorKills": 12, + "adapters": { + "rxdb": { + "run": { + "id": "5f5866b66af7", + "inputs": "1 source files, 4 oracle files, 1 ordinary test files, 1 authored mutants, seed 2367, pool forks; a partial run (--only or --sample)" + }, + "requiredStableOperatorKills": 6, + "retainedStableOperatorKills": 6, + "stableOperatorComparison": [ + { + "key": "src/rxdb.ts#sync:binary-swap:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "beforeId": "rxdb.ts:M3", + "afterId": "rxdb.ts:M3", + "afterOutcome": "killed" + }, + { + "key": "src/rxdb.ts#sync:value-undefined:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "beforeId": "rxdb.ts:M4", + "afterId": "rxdb.ts:M4", + "afterOutcome": "killed" + }, + { + "key": "src/rxdb.ts#initialFetch:field-drop:begin ( { snapshot : persistence !== null || ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done , } )", + "beforeId": "rxdb.ts:M7", + "afterId": "rxdb.ts:M7", + "afterOutcome": "killed" + }, + { + "key": "src/rxdb.ts#initialFetch:binary-swap:begin ( { snapshot : persistence !== null || ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done , } )", + "beforeId": "rxdb.ts:M8", + "afterId": "rxdb.ts:M8", + "afterOutcome": "killed" + }, + { + "key": "src/rxdb.ts#initialFetch:binary-swap:begin ( { snapshot : persistence !== null || ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done , } )~2", + "beforeId": "rxdb.ts:M9", + "afterId": "rxdb.ts:M9", + "afterOutcome": "killed" + }, + { + "key": "src/rxdb.ts#initialFetch:drop-not:begin ( { snapshot : persistence !== null || ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done , } )", + "beforeId": "rxdb.ts:M10", + "afterId": "rxdb.ts:M10", + "afterOutcome": "killed" + } + ], + "excluded": [], + "nondeterministic": [], + "outcomes": { + "killed": 6, + "does-not-apply": 1 + }, + "adaptedSemanticControl": { + "run": { + "id": "d48451ac1f6a", + "inputs": "1 source files, 4 oracle files, 1 ordinary test files, 1 authored mutants, seed 2367, pool forks; a partial run (--only or --sample)" + }, + "results": [ + { + "id": "authored:pending-baseline-is-not-snapshot", + "key": "src/rxdb.ts#authored:pending-baseline-is-not-snapshot", + "outcome": "killed", + "by": [ + "persisted startup conformance: RxDB > adapter.rxdb.persisted-startup through controlled > adapter.rxdb.persisted-startup > fixed seed 2367" + ] + } + ], + "excluded": [], + "nondeterministic": [] + } + }, + "powersync": { + "run": { + "id": "96e1e23c1c70", + "inputs": "1 source files, 4 oracle files, 10 ordinary test files, 1 authored mutants, seed 2367, pool forks; a partial run (--only or --sample)" + }, + "requiredStableOperatorKills": 6, + "retainedStableOperatorKills": 6, + "stableOperatorComparison": [ + { + "key": "src/powersync.ts#sync:binary-swap:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "beforeId": "powersync.ts:M35", + "afterId": "powersync.ts:M35", + "afterOutcome": "killed" + }, + { + "key": "src/powersync.ts#sync:value-undefined:const persistence = params . metadata === undefined ? null : validateSyncPersistenceCapability ( params . metadata . persistence )", + "beforeId": "powersync.ts:M36", + "afterId": "powersync.ts:M36", + "afterOutcome": "killed" + }, + { + "key": "src/powersync.ts#runEagerSync:field-drop:await establishTracking ( { snapshot : persistence !== null || ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "beforeId": "powersync.ts:M162", + "afterId": "powersync.ts:M162", + "afterOutcome": "killed" + }, + { + "key": "src/powersync.ts#runEagerSync:binary-swap:await establishTracking ( { snapshot : persistence !== null || ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "beforeId": "powersync.ts:M163", + "afterId": "powersync.ts:M163", + "afterOutcome": "killed" + }, + { + "key": "src/powersync.ts#runEagerSync:binary-swap:await establishTracking ( { snapshot : persistence !== null || ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )~2", + "beforeId": "powersync.ts:M164", + "afterId": "powersync.ts:M164", + "afterOutcome": "killed" + }, + { + "key": "src/powersync.ts#runEagerSync:drop-not:await establishTracking ( { snapshot : persistence !== null || ! collection . _state . acceptedSyncedEntries ( ) . next ( ) . done , when : { [ DiffTriggerOperation . INSERT ] : `TRUE` , [ DiffTriggerOperation . UPDATE ] : `TRUE` , [ DiffTriggerOperation . DELETE ] : `TRUE` , } , writeType : ( _rowId : string ) => `insert` , batchQuery : ( lockContext : LockContext , batchSize : number , cursor : number , ) => lockContext . getAll < TableType > ( sanitizeSQL `SELECT * FROM ${ viewName } LIMIT ? OFFSET ? `,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )", + "beforeId": "powersync.ts:M165", + "afterId": "powersync.ts:M165", + "afterOutcome": "killed" + } + ], + "excluded": [], + "nondeterministic": [], + "outcomes": { + "killed": 6, + "does-not-apply": 1 + }, + "adaptedSemanticControl": { + "state": "not rerun: identical receiving inputs retain prior evidence", + "originalRun": "79ea5f23bdad", + "originalOutcome": "killed", + "receivingInputComparison": { + "semanticControlRun": "79ea5f23bdad", + "controlOutcome": "killed", + "receivingUnit": "/private/tmp/tanstack-db-2394-conversion-audit-4b71d9d/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "capturedLocalSourceInputs": [ + "packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "packages/powersync-db-collection/tests/persisted-startup-driver.ts", + "packages/powersync-db-collection/tests/persisted-startup-common.ts", + "packages/db-adapter-conformance/tests/persisted-startup-oracle.ts", + "packages/db-sqlite-persistence-core/tests/reload-class-oracle.ts", + "packages/db/.oracle-gaps/bundle-4792d1a926ef4e18.mjs", + "packages/powersync-db-collection/src/index.ts", + "packages/powersync-db-collection/src/powersync.ts", + "packages/powersync-db-collection/tests/test-db-implementation.ts", + "packages/db-sqlite-persistence-core/src/index.ts", + "packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts", + "packages/powersync-db-collection/src/PowerSyncTransactor.ts", + "packages/db-sqlite-persistence-core/src/persisted.ts", + "packages/db/tests/oracle-config.ts", + "packages/db-sqlite-persistence-core/src/sqlite-core-adapter.ts", + "packages/db-adapter-conformance/tests/persisted-startup-host.ts", + "packages/db-ivm/.oracle-gaps/bundle-24cb8e3a22ece0b1.mjs", + "packages/db/tests/oracle-declaration.ts", + "packages/powersync-db-collection/src/PendingOperationStore.ts", + "packages/db-adapter-conformance/tests/persisted-startup-driver.ts", + "packages/db-sqlite-persistence-core/src/remote-subset-wire.ts", + "packages/db-sqlite-persistence-core/src/errors.ts", + "packages/powersync-db-collection/src/sqlite-compiler.ts", + "packages/browser-db-sqlite-persistence/src/wa-sqlite-driver.ts", + "packages/powersync-db-collection/src/schema.ts", + "packages/powersync-db-collection/src/serialization.ts", + "packages/db-sqlite-persistence-core/src/sqlite-value.ts", + "packages/powersync-db-collection/src/definitions.ts", + "packages/powersync-db-collection/src/helpers.ts", + "packages/db-adapter-conformance/tests/memory-persistence.ts", + "packages/db/tests/oracle-cleanup.ts", + "packages/db-sqlite-persistence-core/src/remote-subset-owner.ts", + "packages/db-sqlite-persistence-core/src/committed-tx-anchor.ts" + ], + "changedReceivingInputs": [], + "conversionOnlyPaths": [ + "packages/db-adapter-conformance/src/eager-live-source-oracle.ts", + "packages/db-adapter-conformance/tests/eager-live-source-framework-oracle.ts", + "packages/db-adapter-conformance/tests/eager-live-source-oracle-suite.ts", + "packages/powersync-db-collection/tests/eager-live-source-history-oracle.ts", + "packages/rxdb-db-collection/tests/eager-live-source-history-oracle.ts", + "packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts", + "packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts" + ], + "note": "Application source and persisted-startup receiving law are unchanged. This receiver killed the adapted semantic control before the seven-file primary conversion; repeat only if a changed receiving input requires distinct proof.", + "comparedInputHashes": { + "packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts": { + "before": "7568d4ea2858b7ab98b046c4ecb289f7b4e9389025e0f86027989978dbb41e30", + "after": "7568d4ea2858b7ab98b046c4ecb289f7b4e9389025e0f86027989978dbb41e30" + }, + "packages/powersync-db-collection/tests/persisted-startup-driver.ts": { + "before": "de56d4941d2992534e5bfecf99e46d7b67ba0b5d0c5df8601cdf6c795144463c", + "after": "de56d4941d2992534e5bfecf99e46d7b67ba0b5d0c5df8601cdf6c795144463c" + }, + "packages/powersync-db-collection/tests/persisted-startup-common.ts": { + "before": "d0a41302b4665d082748ef50d17cb8b2b82a3b041f3caca1a8321f2a844513c2", + "after": "d0a41302b4665d082748ef50d17cb8b2b82a3b041f3caca1a8321f2a844513c2" + }, + "packages/db-adapter-conformance/tests/persisted-startup-oracle.ts": { + "before": "ec7671a2cf02b0d6330a647a19c0b7deec66a103d8787fd9f0451f32fcbb9281", + "after": "ec7671a2cf02b0d6330a647a19c0b7deec66a103d8787fd9f0451f32fcbb9281" + }, + "packages/db-sqlite-persistence-core/tests/reload-class-oracle.ts": { + "before": "87ff35044401acb2ec0e175b2a0b5a575f078d9978f9e92033ea41bf52dfd707", + "after": "87ff35044401acb2ec0e175b2a0b5a575f078d9978f9e92033ea41bf52dfd707" + }, + "packages/db/.oracle-gaps/bundle-4792d1a926ef4e18.mjs": { + "before": "9427ecb491f93b5246ab19211d25b457137087459dc697150abc27212e4989ec", + "after": "9427ecb491f93b5246ab19211d25b457137087459dc697150abc27212e4989ec" + }, + "packages/powersync-db-collection/src/index.ts": { + "before": "38ae6d53f204ad1120830a3218802fba253812da56e1f3dffe09f3f418a8ee6f", + "after": "38ae6d53f204ad1120830a3218802fba253812da56e1f3dffe09f3f418a8ee6f" + }, + "packages/powersync-db-collection/src/powersync.ts": { + "before": "d945eb99bff416fc9c7ba041a802fa842072e5cf03a71d02dd2ed214610df408", + "after": "d945eb99bff416fc9c7ba041a802fa842072e5cf03a71d02dd2ed214610df408" + }, + "packages/powersync-db-collection/tests/test-db-implementation.ts": { + "before": "d78e942503c13f3cbb67744cefb186f203053ef09ebbf1bf273e457558008f4f", + "after": "d78e942503c13f3cbb67744cefb186f203053ef09ebbf1bf273e457558008f4f" + }, + "packages/db-sqlite-persistence-core/src/index.ts": { + "before": "b8dfb2bca10830d4a88d87e6c481b2eebd15021d72c19be89dd566deed105c4a", + "after": "b8dfb2bca10830d4a88d87e6c481b2eebd15021d72c19be89dd566deed105c4a" + }, + "packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts": { + "before": "4616fecfbfe811338003197706b3e3839e911a55c6402ce21c63b5893f5a031a", + "after": "4616fecfbfe811338003197706b3e3839e911a55c6402ce21c63b5893f5a031a" + }, + "packages/powersync-db-collection/src/PowerSyncTransactor.ts": { + "before": "62fc82ef7d0647651973429e289a7b12f2245f340ebf3e102ff0095ddecfb98a", + "after": "62fc82ef7d0647651973429e289a7b12f2245f340ebf3e102ff0095ddecfb98a" + }, + "packages/db-sqlite-persistence-core/src/persisted.ts": { + "before": "96d8ed9ff3ef38e9c8862ff576e8bc16c8977daef492778e1a102eaddd542f04", + "after": "96d8ed9ff3ef38e9c8862ff576e8bc16c8977daef492778e1a102eaddd542f04" + }, + "packages/db/tests/oracle-config.ts": { + "before": "1485c940fe28b4da67fc0416aa65e10eb303e2a269833d733aa60afae04707c7", + "after": "1485c940fe28b4da67fc0416aa65e10eb303e2a269833d733aa60afae04707c7" + }, + "packages/db-sqlite-persistence-core/src/sqlite-core-adapter.ts": { + "before": "406e4058b85055fc7972c18786f9461856aa46c90932f6346dc6faccd1106a0c", + "after": "406e4058b85055fc7972c18786f9461856aa46c90932f6346dc6faccd1106a0c" + }, + "packages/db-adapter-conformance/tests/persisted-startup-host.ts": { + "before": "181bdd316e64db99c0c9a779eb3d712817dfc47c7d1f92d8b51ad6bd18741e0a", + "after": "181bdd316e64db99c0c9a779eb3d712817dfc47c7d1f92d8b51ad6bd18741e0a" + }, + "packages/db-ivm/.oracle-gaps/bundle-24cb8e3a22ece0b1.mjs": { + "before": "709480d0f96995f2f9e5eee132ae926e9e2f404bc03a8160801e7726b15b1916", + "after": "709480d0f96995f2f9e5eee132ae926e9e2f404bc03a8160801e7726b15b1916" + }, + "packages/db/tests/oracle-declaration.ts": { + "before": "43171d5c71763a99d6bfcf04a06ff1e3cd4735ec3af19a7214ddcc96c73ad76f", + "after": "43171d5c71763a99d6bfcf04a06ff1e3cd4735ec3af19a7214ddcc96c73ad76f" + }, + "packages/powersync-db-collection/src/PendingOperationStore.ts": { + "before": "1990ef80299439fc9acb7b86aecd1051e1c71d2230e9ac5f05c69e1e3425e36c", + "after": "1990ef80299439fc9acb7b86aecd1051e1c71d2230e9ac5f05c69e1e3425e36c" + }, + "packages/db-adapter-conformance/tests/persisted-startup-driver.ts": { + "before": "e38e3a1635f6f6cfe9cdf9e3ec403da69d2b8b610d0249416c9f31163917e21b", + "after": "e38e3a1635f6f6cfe9cdf9e3ec403da69d2b8b610d0249416c9f31163917e21b" + }, + "packages/db-sqlite-persistence-core/src/remote-subset-wire.ts": { + "before": "81227e40c2dc3a3e775c8b58e02085e02ded6689a5030e683c5755644d131f57", + "after": "81227e40c2dc3a3e775c8b58e02085e02ded6689a5030e683c5755644d131f57" + }, + "packages/db-sqlite-persistence-core/src/errors.ts": { + "before": "04ff7138695d80d4b49abd230b3efd03a560ac91b37993e5dafb6cd4e3d3baa2", + "after": "04ff7138695d80d4b49abd230b3efd03a560ac91b37993e5dafb6cd4e3d3baa2" + }, + "packages/powersync-db-collection/src/sqlite-compiler.ts": { + "before": "a0e4ab4f72345e4b63cfb22c5144404686e51471be69b76bb2e30155fa05aa9b", + "after": "a0e4ab4f72345e4b63cfb22c5144404686e51471be69b76bb2e30155fa05aa9b" + }, + "packages/browser-db-sqlite-persistence/src/wa-sqlite-driver.ts": { + "before": "97b8778f1cef558623326185afdbd3ffd4618f37a4e21e6e75ab5d5c7ad7f1f3", + "after": "97b8778f1cef558623326185afdbd3ffd4618f37a4e21e6e75ab5d5c7ad7f1f3" + }, + "packages/powersync-db-collection/src/schema.ts": { + "before": "656a6077b5c8b5393a9d4d1d2402797a2b01b8ac42626b384ec13a80f8e7935c", + "after": "656a6077b5c8b5393a9d4d1d2402797a2b01b8ac42626b384ec13a80f8e7935c" + }, + "packages/powersync-db-collection/src/serialization.ts": { + "before": "5cfbf26939e7de565e39b92d836edd2adeba5193e2c5a327cf1c54ea7fb9bc7b", + "after": "5cfbf26939e7de565e39b92d836edd2adeba5193e2c5a327cf1c54ea7fb9bc7b" + }, + "packages/db-sqlite-persistence-core/src/sqlite-value.ts": { + "before": "0ab554d0aec2743c271e0355926681b9290feef0948d2816a5e7f5c2799ad69f", + "after": "0ab554d0aec2743c271e0355926681b9290feef0948d2816a5e7f5c2799ad69f" + }, + "packages/powersync-db-collection/src/definitions.ts": { + "before": "27c13705a302ab0aba2dbc0b776b3e74bdee90c4fb4487a63bc10e1fe6c11d2f", + "after": "27c13705a302ab0aba2dbc0b776b3e74bdee90c4fb4487a63bc10e1fe6c11d2f" + }, + "packages/powersync-db-collection/src/helpers.ts": { + "before": "76406cce7f2b9bfe8500c6d4858319e6db4e5e7a12276c0c035f3a4363602146", + "after": "76406cce7f2b9bfe8500c6d4858319e6db4e5e7a12276c0c035f3a4363602146" + }, + "packages/db-adapter-conformance/tests/memory-persistence.ts": { + "before": "a1c13629ada7e230b3038d9e55d50069ca6b2f7b003c3cc116f81cccc559cbcc", + "after": "a1c13629ada7e230b3038d9e55d50069ca6b2f7b003c3cc116f81cccc559cbcc" + }, + "packages/db/tests/oracle-cleanup.ts": { + "before": "5c617b535e57195128ef9efc4a627b78227563af38bb40e5f932c35dfce009ce", + "after": "5c617b535e57195128ef9efc4a627b78227563af38bb40e5f932c35dfce009ce" + }, + "packages/db-sqlite-persistence-core/src/remote-subset-owner.ts": { + "before": "487aefab574711b922b8bc7d3095af8ef8fe5caad859e6c935b575e03b08e07e", + "after": "487aefab574711b922b8bc7d3095af8ef8fe5caad859e6c935b575e03b08e07e" + }, + "packages/db-sqlite-persistence-core/src/committed-tx-anchor.ts": { + "before": "4320bed340a5c83d42e351979db034a44f59de23138ccd8f796789aef30312ee", + "after": "4320bed340a5c83d42e351979db034a44f59de23138ccd8f796789aef30312ee" + } + }, + "controlHashes": { + "control.json": { + "before": "4dd227a3d7816031726119d806607076900ce536f01fd1753eb9634888e95d58", + "after": "4dd227a3d7816031726119d806607076900ce536f01fd1753eb9634888e95d58" + } + }, + "runtimeSha256": "eb96bdabfd1f6705345bbd459f9308d140c011fa55865efdb4fa8f61d048f55d" + } + } + } + }, + "integrity": { + "head": "777935fd68daa497fdd3c9db533845b710be68a1", + "checkedCounts": { + "sources": 3730, + "buildArtifacts": 1092, + "workspaceBundles": 0, + "controls": 4, + "instruments": 0, + "instrumentFiles": 15 + }, + "changedFrozenFiles": [], + "originalCheckoutSourceLeaks": [] + }, + "conversionPaths": [ + "packages/db-adapter-conformance/src/eager-live-source-oracle.ts", + "packages/db-adapter-conformance/tests/eager-live-source-framework-oracle.ts", + "packages/db-adapter-conformance/tests/eager-live-source-oracle-suite.ts", + "packages/powersync-db-collection/tests/eager-live-source-history-oracle.ts", + "packages/rxdb-db-collection/tests/eager-live-source-history-oracle.ts", + "packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts", + "packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts" + ], + "productionSourceHashes": { + "packages/powersync-db-collection/src/powersync.ts": "d945eb99bff416fc9c7ba041a802fa842072e5cf03a71d02dd2ed214610df408", + "packages/rxdb-db-collection/src/rxdb.ts": "2dca19cff8bb602b2aa59f46c7635035e49d0f1cda0a2222e1917f6d2eb8aa62" + }, + "commands": { + "powersync": [ + "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node", + "/private/tmp/tanstack-db-2394-gap-cli-e1fb-integrity/oracle-gaps/index.mjs", + "--env-prefix", + "TANSTACK_DB_ORACLE", + "--package", + "packages/powersync-db-collection", + "--source", + "src/powersync.ts", + "--diff-base", + "caa936d5af5332a6eca64335d291ba8e42c8cf37", + "--seed", + "2367", + "--contexts", + "1", + "--only", + "powersync.ts:M35,powersync.ts:M36,powersync.ts:M162,powersync.ts:M163,powersync.ts:M164,powersync.ts:M165,authored:pending-baseline-is-not-snapshot", + "--mutants", + "/private/tmp/tanstack-db-2394-primary-eager-retention/.retention-audit/controls-powersync", + "--out", + "/private/tmp/tanstack-db-2394-primary-eager-retention/.retention-audit/powersync", + "--pool", + "forks" + ], + "rxdb": [ + "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node", + "/private/tmp/tanstack-db-2394-gap-cli-e1fb-integrity/oracle-gaps/index.mjs", + "--env-prefix", + "TANSTACK_DB_ORACLE", + "--package", + "packages/rxdb-db-collection", + "--source", + "src/rxdb.ts", + "--diff-base", + "caa936d5af5332a6eca64335d291ba8e42c8cf37", + "--seed", + "2367", + "--contexts", + "2", + "--only", + "rxdb.ts:M3,rxdb.ts:M4,rxdb.ts:M7,rxdb.ts:M8,rxdb.ts:M9,rxdb.ts:M10,authored:pending-baseline-is-not-snapshot", + "--mutants", + "/private/tmp/tanstack-db-2394-primary-eager-retention/.retention-audit/controls-rxdb", + "--out", + "/private/tmp/tanstack-db-2394-primary-eager-retention/.retention-audit/rxdb", + "--pool", + "forks" + ], + "rxdb-semantic-control": [ + "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node", + "/private/tmp/tanstack-db-2394-gap-cli-e1fb-integrity/oracle-gaps/index.mjs", + "--env-prefix", + "TANSTACK_DB_ORACLE", + "--package", + "packages/rxdb-db-collection", + "--source", + "src/rxdb.ts", + "--diff-base", + "caa936d5af5332a6eca64335d291ba8e42c8cf37", + "--seed", + "2367", + "--contexts", + "1", + "--only", + "authored:pending-baseline-is-not-snapshot", + "--mutants", + "/private/tmp/tanstack-db-2394-primary-eager-retention/.retention-audit/controls-rxdb-semantic-control", + "--out", + "/private/tmp/tanstack-db-2394-primary-eager-retention/.retention-audit/rxdb-semantic-control", + "--pool", + "forks" + ] + }, + "runtime": { + "path": "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node", + "version": "v24.22.1", + "pool": "forks" + }, + "cleanPasses": { + "powersync": { + "first": { + "tests": 265, + "failed": [], + "units": [ + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts", + "tests": 89, + "durationSeconds": 180.98364345800002, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts", + "tests": 44, + "durationSeconds": 76.95766595900001, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/powersync-db-collection/tests/persisted-capability-conformance-oracle.test.ts", + "tests": 46, + "durationSeconds": 7.5836148329999995, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "tests": 86, + "durationSeconds": 115.353795625, + "setupErrors": [], + "unhandled": [] + } + ] + }, + "second": { + "tests": 265, + "failed": [], + "units": [ + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts", + "tests": 89, + "durationSeconds": 169.60518699900004, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/powersync-db-collection/tests/eager-reload-conformance-oracle.property.test.ts", + "tests": 44, + "durationSeconds": 59.541002166999995, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/powersync-db-collection/tests/persisted-capability-conformance-oracle.test.ts", + "tests": 46, + "durationSeconds": 7.2377974169999995, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "tests": 86, + "durationSeconds": 152.68622466699998, + "setupErrors": [], + "unhandled": [] + } + ] + } + }, + "rxdb": { + "first": { + "tests": 305, + "failed": [], + "units": [ + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts", + "tests": 129, + "durationSeconds": 64.750506251, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/rxdb-db-collection/tests/persisted-capability-conformance-oracle.test.ts", + "tests": 46, + "durationSeconds": 1.4942143749999999, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts", + "tests": 44, + "durationSeconds": 7.912163875999999, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "tests": 86, + "durationSeconds": 14.086647666000001, + "setupErrors": [], + "unhandled": [] + } + ] + }, + "second": { + "tests": 305, + "failed": [], + "units": [ + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts", + "tests": 129, + "durationSeconds": 58.656682040999996, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/rxdb-db-collection/tests/persisted-capability-conformance-oracle.test.ts", + "tests": 46, + "durationSeconds": 0.787929541, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/rxdb-db-collection/tests/eager-reload-conformance-oracle.property.test.ts", + "tests": 44, + "durationSeconds": 6.085587791999999, + "setupErrors": [], + "unhandled": [] + }, + { + "file": "/private/tmp/tanstack-db-2394-primary-eager-retention/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts", + "tests": 86, + "durationSeconds": 12.93134825, + "setupErrors": [], + "unhandled": [] + } + ] + } + } + }, + "limits": "These are repeat stable-key retention checks of the original12 PowerSync/RxDB kills, not12 new distinct kills. They cover the7-file primary oracle conversion with unchanged c4 production; later startup batching/performance source changes need their own evidence." +} diff --git a/docs/contributing/oracle-reviews/2026-11-10-query-changed-callback-gap-audit.json b/docs/contributing/oracle-reviews/2026-11-10-query-changed-callback-gap-audit.json new file mode 100644 index 0000000000..8866da4c9b --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-query-changed-callback-gap-audit.json @@ -0,0 +1,1314 @@ +{ + "snapshot": "/private/tmp/tanstack-db-2394-query-delta-11d6ed3", + "head": "c0b65d926ca72ca04b96a205013845a42fbb21fb", + "source": "packages/query-db-collection/src/query.ts", + "sourceSha256": "1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b", + "run": { + "id": "89f398bd4b86", + "inputs": "1 source files, 24 oracle files, 21 ordinary test files, 1 authored mutants, seed 2367, pool forks; a partial run (--only or --sample)" + }, + "seed": 2367, + "scope": { + "changedCallback": { + "lineStart": 2737, + "lineEnd": 2758 + }, + "callbackOperators": 29, + "mappedPreviousTargets": 5, + "authoredControls": 1, + "wholeSourceCampaign": false + }, + "knownTargetMapping": [ + { + "old": "query.ts:M563", + "key": "src/query.ts#getHydratedOwnedRowsForQueryBaseline:value-undefined:if ( syncMode === `eager` && persistence !== null && ! unownedDirectWritePositions . has ( rowKey ) ) ownedRows . add ( rowKey )", + "current": "query.ts:M563" + }, + { + "old": "query.ts:M1468", + "key": "src/query.ts#needsPersistedBaseline:binary-swap:const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )~2", + "current": "query.ts:M1469" + }, + { + "old": "query.ts:M1469", + "key": "src/query.ts#needsPersistedBaseline:binary-swap:const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )~3", + "current": "query.ts:M1470" + }, + { + "old": "query.ts:M1473", + "key": "src/query.ts#needsPersistedBaseline:value-undefined:const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )~2", + "current": "query.ts:M1474" + }, + { + "old": "query.ts:M1794", + "key": "src/query.ts#handleQueryResult:value-undefined:enqueueResultApplication ( hashedQueryKey , result , ( signal , token ) => needsPersistedBaseline ( hashedQueryKey ) ? reconcileSuccessfulResult ( queryKey , result , token , signal , undefined , fetchStartGenerations . get ( hashedQueryKey ) ?? directWriteGeneration , ) : applySuccessfulResult ( queryKey , result , token , undefined , signal , ) , )~3", + "current": "query.ts:M1795" + } + ], + "command": [ + "/Users/kyle.mathews/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node", + "/private/tmp/tanstack-db-2394-gap-cli-e1fb-titles/oracle-gaps/index.mjs", + "--env-prefix", + "TANSTACK_DB_ORACLE", + "--package", + "packages/query-db-collection", + "--source", + "src/query.ts", + "--diff-base", + "c4c00b20b6c91549a2b9fe502096d0841520f3fe", + "--seed", + "2367", + "--contexts", + "3", + "--pool", + "forks", + "--only", + "query.ts:M1307,query.ts:M1308,query.ts:M1309,query.ts:M1310,query.ts:M1311,query.ts:M1312,query.ts:M1313,query.ts:M1314,query.ts:M1315,query.ts:M1316,query.ts:M1317,query.ts:M1318,query.ts:M1319,query.ts:M1320,query.ts:M1321,query.ts:M1322,query.ts:M1323,query.ts:M1324,query.ts:M1325,query.ts:M1326,query.ts:M1327,query.ts:M1328,query.ts:M1329,query.ts:M1330,query.ts:M1331,query.ts:M1332,query.ts:M1333,query.ts:M1334,query.ts:M1335,query.ts:M563,query.ts:M1469,query.ts:M1470,query.ts:M1474,query.ts:M1795,authored:original-value-defect", + "--mutants", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/.retention-audit/controls", + "--out", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/.retention-audit/query" + ], + "cleanPasses": { + "first": { + "units": 24, + "tests": 1747, + "failed": [] + }, + "second": { + "units": 24, + "tests": 1747, + "failed": [] + } + }, + "outcomes": { + "killed": 24, + "survived": 11 + }, + "excluded": [], + "nondeterministic": [], + "integrity": { + "head": "c0b65d926ca72ca04b96a205013845a42fbb21fb", + "checkedCounts": { + "sources": 3749, + "buildArtifacts": 1092, + "workspaceBundles": 0, + "controls": 1, + "instruments": 13, + "instrumentFiles": 0 + }, + "changedFrozenFiles": [], + "originalCheckoutSourceLeaks": [] + }, + "keyCaveat": "CLI report occurrence suffixes can differ from assignKeys(all-source) selection suffixes. Match source SHA, operator, exact original span offset, anchor and replacement; both contextual keys are retained.", + "results": [ + { + "id": "query.ts:M563", + "source": "src/query.ts", + "line": 1688, + "position": 57583, + "operator": "value-undefined", + "function": "getHydratedOwnedRowsForQueryBaseline", + "original": "unownedDirectWritePositions.has(rowKey)", + "replacement": "undefined", + "anchor": "if ( syncMode === `eager` && persistence !== null && ! unownedDirectWritePositions . has ( rowKey ) ) ownedRows . add ( rowKey )", + "outcome": "killed", + "by": [ + "controlled > query.persisted-direct-ownership > fixed seed 563" + ], + "laws": [ + "direct row ownership" + ], + "selectionGlobalKey": "src/query.ts#getHydratedOwnedRowsForQueryBaseline:value-undefined:if ( syncMode === `eager` && persistence !== null && ! unownedDirectWritePositions . has ( rowKey ) ) ownedRows . add ( rowKey )", + "actualReportKey": "src/query.ts#getHydratedOwnedRowsForQueryBaseline:value-undefined:if ( syncMode === `eager` && persistence !== null && ! unownedDirectWritePositions . has ( rowKey ) ) ownedRows . add ( rowKey )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1307", + "source": "src/query.ts", + "line": 2739, + "position": 96878, + "operator": "binary-swap", + "function": "applySuccessfulResult", + "original": "??", + "replacement": "&&", + "anchor": "const oldItem = persistedBaseline ?. get ( key ) ?. value ?? currentSyncedItems . get ( key )", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:binary-swap:const oldItem = persistedBaseline ?. get ( key ) ?. value ?? currentSyncedItems . get ( key )", + "actualReportKey": "src/query.ts#applySuccessfulResult:binary-swap:const oldItem = persistedBaseline ?. get ( key ) ?. value ?? currentSyncedItems . get ( key )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1308", + "source": "src/query.ts", + "line": 2739, + "position": 96881, + "operator": "value-undefined", + "function": "applySuccessfulResult", + "original": "currentSyncedItems.get(key)", + "replacement": "undefined", + "anchor": "const oldItem = persistedBaseline ?. get ( key ) ?. value ?? currentSyncedItems . get ( key )", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:value-undefined:const oldItem = persistedBaseline ?. get ( key ) ?. value ?? currentSyncedItems . get ( key )", + "actualReportKey": "src/query.ts#applySuccessfulResult:value-undefined:const oldItem = persistedBaseline ?. get ( key ) ?. value ?? currentSyncedItems . get ( key )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1309", + "source": "src/query.ts", + "line": 2740, + "position": 96935, + "operator": "value-undefined", + "function": "applySuccessfulResult", + "original": "newItemsMap.get(key)", + "replacement": "undefined", + "anchor": "const newItem = newItemsMap . get ( key )", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:value-undefined:const newItem = newItemsMap . get ( key )", + "actualReportKey": "src/query.ts#applySuccessfulResult:value-undefined:const newItem = newItemsMap . get ( key )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1310", + "source": "src/query.ts", + "line": 2741, + "position": 96970, + "operator": "if-true", + "function": "applySuccessfulResult", + "original": "!newItem", + "replacement": "true", + "anchor": "if ( ! newItem ) { const directPosition = unownedDirectWritePositions . get ( key ) if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition ) const owners = getPersistedOwners ( key ) owners . delete ( hashedQueryKey ) setPersistedOwners ( key , owners ) const needToRemove = removeRowOwner ( key , hashedQueryKey ) if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) } } else if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:if-true:if ( ! newItem ) { const directPosition = unownedDirectWritePositions . get ( key ) if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition ) const owners = getPersistedOwners ( key ) owners . delete ( hashedQueryKey ) setPersistedOwners ( key , owners ) const needToRemove = removeRowOwner ( key , hashedQueryKey ) if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) } } else if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:if-true:if ( ! newItem ) { const directPosition = unownedDirectWritePositions . get ( key ) if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition ) const owners = getPersistedOwners ( key ) owners . delete ( hashedQueryKey ) setPersistedOwners ( key , owners ) const needToRemove = removeRowOwner ( key , hashedQueryKey ) if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) } } else if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1311", + "source": "src/query.ts", + "line": 2741, + "position": 96970, + "operator": "if-false", + "function": "applySuccessfulResult", + "original": "!newItem", + "replacement": "false", + "anchor": "if ( ! newItem ) { const directPosition = unownedDirectWritePositions . get ( key ) if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition ) const owners = getPersistedOwners ( key ) owners . delete ( hashedQueryKey ) setPersistedOwners ( key , owners ) const needToRemove = removeRowOwner ( key , hashedQueryKey ) if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) } } else if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:if-false:if ( ! newItem ) { const directPosition = unownedDirectWritePositions . get ( key ) if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition ) const owners = getPersistedOwners ( key ) owners . delete ( hashedQueryKey ) setPersistedOwners ( key , owners ) const needToRemove = removeRowOwner ( key , hashedQueryKey ) if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) } } else if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:if-false:if ( ! newItem ) { const directPosition = unownedDirectWritePositions . get ( key ) if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition ) const owners = getPersistedOwners ( key ) owners . delete ( hashedQueryKey ) setPersistedOwners ( key , owners ) const needToRemove = removeRowOwner ( key , hashedQueryKey ) if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) } } else if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1312", + "source": "src/query.ts", + "line": 2741, + "position": 96970, + "operator": "drop-not", + "function": "applySuccessfulResult", + "original": "!", + "replacement": "", + "anchor": "if ( ! newItem ) { const directPosition = unownedDirectWritePositions . get ( key ) if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition ) const owners = getPersistedOwners ( key ) owners . delete ( hashedQueryKey ) setPersistedOwners ( key , owners ) const needToRemove = removeRowOwner ( key , hashedQueryKey ) if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) } } else if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:drop-not:if ( ! newItem ) { const directPosition = unownedDirectWritePositions . get ( key ) if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition ) const owners = getPersistedOwners ( key ) owners . delete ( hashedQueryKey ) setPersistedOwners ( key , owners ) const needToRemove = removeRowOwner ( key , hashedQueryKey ) if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) } } else if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:drop-not:if ( ! newItem ) { const directPosition = unownedDirectWritePositions . get ( key ) if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition ) const owners = getPersistedOwners ( key ) owners . delete ( hashedQueryKey ) setPersistedOwners ( key , owners ) const needToRemove = removeRowOwner ( key , hashedQueryKey ) if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) } } else if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1313", + "source": "src/query.ts", + "line": 2742, + "position": 97017, + "operator": "value-undefined", + "function": "applySuccessfulResult", + "original": "unownedDirectWritePositions.get(key)", + "replacement": "undefined", + "anchor": "const directPosition = unownedDirectWritePositions . get ( key )", + "outcome": "survived", + "class": "no-observed-difference", + "changedTests": [], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:value-undefined:const directPosition = unownedDirectWritePositions . get ( key )~2", + "actualReportKey": "src/query.ts#applySuccessfulResult:value-undefined:const directPosition = unownedDirectWritePositions . get ( key )", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1314", + "source": "src/query.ts", + "line": 2743, + "position": 97070, + "operator": "if-true", + "function": "applySuccessfulResult", + "original": "directPosition !== undefined", + "replacement": "true", + "anchor": "if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition )", + "outcome": "survived", + "class": "no-observed-difference", + "changedTests": [], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:if-true:if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition )", + "actualReportKey": "src/query.ts#applySuccessfulResult:if-true:if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition )", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1315", + "source": "src/query.ts", + "line": 2743, + "position": 97070, + "operator": "if-false", + "function": "applySuccessfulResult", + "original": "directPosition !== undefined", + "replacement": "false", + "anchor": "if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition )", + "outcome": "survived", + "class": "no-observed-difference", + "changedTests": [], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:if-false:if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition )", + "actualReportKey": "src/query.ts#applySuccessfulResult:if-false:if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition )", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1316", + "source": "src/query.ts", + "line": 2743, + "position": 97085, + "operator": "binary-swap", + "function": "applySuccessfulResult", + "original": "!==", + "replacement": "===", + "anchor": "if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition )", + "outcome": "survived", + "class": "no-observed-difference", + "changedTests": [], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:binary-swap:if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition )", + "actualReportKey": "src/query.ts#applySuccessfulResult:binary-swap:if ( directPosition !== undefined ) settledDirectPositions . set ( key , directPosition )", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1317", + "source": "src/query.ts", + "line": 2744, + "position": 97114, + "operator": "delete-call", + "function": "applySuccessfulResult", + "original": "settledDirectPositions.set(key, directPosition)", + "replacement": ";", + "anchor": "settledDirectPositions . set ( key , directPosition )", + "outcome": "survived", + "class": "no-observed-difference", + "changedTests": [], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:delete-call:settledDirectPositions . set ( key , directPosition )", + "actualReportKey": "src/query.ts#applySuccessfulResult:delete-call:settledDirectPositions . set ( key , directPosition )", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1318", + "source": "src/query.ts", + "line": 2745, + "position": 97189, + "operator": "value-undefined", + "function": "applySuccessfulResult", + "original": "getPersistedOwners(key)", + "replacement": "undefined", + "anchor": "const owners = getPersistedOwners ( key )", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:value-undefined:const owners = getPersistedOwners ( key )", + "actualReportKey": "src/query.ts#applySuccessfulResult:value-undefined:const owners = getPersistedOwners ( key )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1319", + "source": "src/query.ts", + "line": 2746, + "position": 97225, + "operator": "delete-call", + "function": "applySuccessfulResult", + "original": "owners.delete(hashedQueryKey)", + "replacement": ";", + "anchor": "owners . delete ( hashedQueryKey )", + "outcome": "killed", + "by": [ + "persisted startup conformance: Query > query.startup-ownership through controlled > query.startup-ownership > fixed seed 2367" + ], + "laws": [ + "persisted startup" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:delete-call:owners . delete ( hashedQueryKey )", + "actualReportKey": "src/query.ts#applySuccessfulResult:delete-call:owners . delete ( hashedQueryKey )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1320", + "source": "src/query.ts", + "line": 2747, + "position": 97267, + "operator": "delete-call", + "function": "applySuccessfulResult", + "original": "setPersistedOwners(key, owners)", + "replacement": ";", + "anchor": "setPersistedOwners ( key , owners )", + "outcome": "killed", + "by": [ + "persisted startup conformance: Query > query.startup-ownership through controlled > query.startup-ownership > fixed seed 2367" + ], + "laws": [ + "persisted startup" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:delete-call:setPersistedOwners ( key , owners )", + "actualReportKey": "src/query.ts#applySuccessfulResult:delete-call:setPersistedOwners ( key , owners )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1321", + "source": "src/query.ts", + "line": 2748, + "position": 97332, + "operator": "value-undefined", + "function": "applySuccessfulResult", + "original": "removeRowOwner(key, hashedQueryKey)", + "replacement": "undefined", + "anchor": "const needToRemove = removeRowOwner ( key , hashedQueryKey )", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:value-undefined:const needToRemove = removeRowOwner ( key , hashedQueryKey )", + "actualReportKey": "src/query.ts#applySuccessfulResult:value-undefined:const needToRemove = removeRowOwner ( key , hashedQueryKey )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1322", + "source": "src/query.ts", + "line": 2751, + "position": 97533, + "operator": "if-true", + "function": "applySuccessfulResult", + "original": "needToRemove", + "replacement": "true", + "anchor": "if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) }", + "outcome": "killed", + "by": [ + "query-db.ownership.reacquisition > {\"staleTime\":0,\"refetch\":\"empty\",\"settles\":\"while-held\",\"peers\":1,\"rows\":1}" + ], + "laws": [ + "lease ownership" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:if-true:if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:if-true:if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) }", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1323", + "source": "src/query.ts", + "line": 2751, + "position": 97533, + "operator": "if-false", + "function": "applySuccessfulResult", + "original": "needToRemove", + "replacement": "false", + "anchor": "if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) }", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:if-false:if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:if-false:if ( needToRemove ) { write ( { type : `delete` , key } ) removedRows . add ( key ) }", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1324", + "source": "src/query.ts", + "line": 2752, + "position": 97563, + "operator": "delete-call", + "function": "applySuccessfulResult", + "original": "write({ type: `delete`, key })", + "replacement": ";", + "anchor": "write ( { type : `delete` , key } )", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:delete-call:write ( { type : `delete` , key } )", + "actualReportKey": "src/query.ts#applySuccessfulResult:delete-call:write ( { type : `delete` , key } )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1325", + "source": "src/query.ts", + "line": 2752, + "position": 97571, + "operator": "field-drop", + "function": "applySuccessfulResult", + "original": "type: `delete`", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "outcome": "killed", + "by": [ + "query-db.ownership.reacquisition > {\"staleTime\":0,\"refetch\":\"empty\",\"settles\":\"while-held\",\"peers\":0,\"rows\":1}" + ], + "laws": [ + "lease ownership" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `delete` , key } )", + "actualReportKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `delete` , key } )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1326", + "source": "src/query.ts", + "line": 2752, + "position": 97587, + "operator": "field-drop", + "function": "applySuccessfulResult", + "original": "key", + "replacement": "", + "anchor": "write ( { type : `delete` , key } )", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `delete` , key } )~2", + "actualReportKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `delete` , key } )~2", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1327", + "source": "src/query.ts", + "line": 2753, + "position": 97608, + "operator": "delete-call", + "function": "applySuccessfulResult", + "original": "removedRows.add(key)", + "replacement": ";", + "anchor": "removedRows . add ( key )", + "outcome": "survived", + "class": "no-observed-difference", + "changedTests": [], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:delete-call:removedRows . add ( key )", + "actualReportKey": "src/query.ts#applySuccessfulResult:delete-call:removedRows . add ( key )", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1328", + "source": "src/query.ts", + "line": 2755, + "position": 97664, + "operator": "if-true", + "function": "applySuccessfulResult", + "original": "oldItem && !deepEquals(oldItem, newItem)", + "replacement": "true", + "anchor": "if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "outcome": "survived", + "class": "comparison-gap", + "changedTests": [ + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed seed 563", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > random seed", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 1", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 3", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 6", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 10", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 13", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 15", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 17", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 18", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 19", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 21", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 22", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 23", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 25", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 27", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 30", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 34", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 37", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 39", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 41", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 42", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 43", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 45", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 46", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 47", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 49", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 51", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 54", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 58", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 61", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 63", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 65", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 66", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 67", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 69", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 70", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 71", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 73", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 75", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 78", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 82", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 85", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 87", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 89", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 90", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 91", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 93", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 94", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 95", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state raw", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state derived", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state retained", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state no-retention", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state before-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state after-fetch-start", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state source-includes-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state source-omits-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state no-retention-direct-after-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state retained-direct-before-returned-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state overlapping-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state distinct-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state returned-value-equals-saved-after-direct-change", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: claims direct row from older result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: claims direct row from older result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: preserves product failure before browser diagnostics", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > determinism", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed seed 563", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > random seed", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 1", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 3", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 6", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 10", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 13", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 15", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 17", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 18", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 19", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 21", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 22", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 23", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 25", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 27", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 30", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 34", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 37", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 39", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 41", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 42", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 43", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 45", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 46", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 47", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 49", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 51", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 54", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 58", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 61", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 63", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 65", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 66", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 67", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 69", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 70", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 71", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 73", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 75", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 78", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 82", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 85", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 87", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 89", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 90", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 91", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 93", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 94", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 95", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state raw", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state derived", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state retained", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state no-retention", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state before-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state after-fetch-start", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state source-includes-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state source-omits-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state no-retention-direct-after-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state retained-direct-before-returned-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state overlapping-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state distinct-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state returned-value-equals-saved-after-direct-change", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: claims direct row from older result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: claims direct row from older result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: preserves product failure before browser diagnostics", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > determinism" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:if-true:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:if-true:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1329", + "source": "src/query.ts", + "line": 2755, + "position": 97664, + "operator": "if-false", + "function": "applySuccessfulResult", + "original": "oldItem && !deepEquals(oldItem, newItem)", + "replacement": "false", + "anchor": "if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:if-false:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:if-false:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1330", + "source": "src/query.ts", + "line": 2755, + "position": 97672, + "operator": "binary-swap", + "function": "applySuccessfulResult", + "original": "&&", + "replacement": "||", + "anchor": "if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "outcome": "survived", + "class": "comparison-gap", + "changedTests": [ + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed seed 563", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > random seed", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 1", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 3", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 6", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 10", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 13", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 15", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 17", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 18", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 19", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 21", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 22", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 23", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 25", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 27", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 30", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 34", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 37", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 39", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 41", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 42", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 43", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 45", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 46", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 47", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 49", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 51", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 54", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 58", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 61", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 63", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 65", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 66", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 67", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 69", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 70", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 71", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 73", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 75", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 78", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 82", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 85", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 87", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 89", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 90", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 91", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 93", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 94", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 95", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state raw", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state derived", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state retained", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state no-retention", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state before-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state after-fetch-start", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state source-includes-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state source-omits-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state no-retention-direct-after-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state retained-direct-before-returned-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state overlapping-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state distinct-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state returned-value-equals-saved-after-direct-change", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: claims direct row from older result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: claims direct row from older result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: preserves product failure before browser diagnostics", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > determinism", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed seed 563", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > random seed", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 1", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 3", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 6", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 10", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 13", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 15", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 17", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 18", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 19", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 21", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 22", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 23", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 25", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 27", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 30", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 34", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 37", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 39", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 41", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 42", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 43", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 45", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 46", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 47", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 49", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 51", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 54", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 58", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 61", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 63", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 65", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 66", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 67", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 69", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 70", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 71", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 73", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 75", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 78", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 82", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 85", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 87", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 89", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 90", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 91", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 93", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 94", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 95", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state raw", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state derived", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state retained", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state no-retention", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state before-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state after-fetch-start", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state source-includes-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state source-omits-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state no-retention-direct-after-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state retained-direct-before-returned-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state overlapping-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state distinct-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state returned-value-equals-saved-after-direct-change", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: claims direct row from older result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: claims direct row from older result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: preserves product failure before browser diagnostics", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > determinism" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:binary-swap:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:binary-swap:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1331", + "source": "src/query.ts", + "line": 2755, + "position": 97675, + "operator": "drop-not", + "function": "applySuccessfulResult", + "original": "!", + "replacement": "", + "anchor": "if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:drop-not:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:drop-not:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1332", + "source": "src/query.ts", + "line": 2755, + "position": 97676, + "operator": "value-undefined", + "function": "applySuccessfulResult", + "original": "deepEquals(oldItem, newItem)", + "replacement": "undefined", + "anchor": "if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "outcome": "survived", + "class": "comparison-gap", + "changedTests": [ + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed seed 563", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > random seed", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 1", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 3", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 6", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 10", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 13", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 15", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 17", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 18", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 19", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 21", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 22", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 23", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 25", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 27", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 30", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 34", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 37", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 39", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 41", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 42", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 43", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 45", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 46", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 47", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 49", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 51", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 54", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 58", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 61", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 63", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 65", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 66", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 67", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 69", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 70", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 71", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 73", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 75", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 78", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 82", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 85", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 87", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 89", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 90", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 91", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 93", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 94", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 95", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state raw", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state derived", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state retained", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state no-retention", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state before-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state after-fetch-start", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state source-includes-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state source-omits-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state no-retention-direct-after-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state retained-direct-before-returned-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state overlapping-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state distinct-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state returned-value-equals-saved-after-direct-change", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: claims direct row from older result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: claims direct row from older result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: preserves product failure before browser diagnostics", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > determinism", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed seed 563", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > random seed", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 1", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 3", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 6", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 10", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 13", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 15", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 17", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 18", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 19", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 21", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 22", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 23", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 25", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 27", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 30", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 34", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 37", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 39", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 41", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 42", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 43", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 45", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 46", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 47", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 49", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 51", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 54", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 58", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 61", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 63", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 65", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 66", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 67", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 69", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 70", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 71", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 73", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 75", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 78", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 82", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 85", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 87", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 89", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 90", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 91", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 93", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 94", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 95", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state raw", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state derived", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state retained", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state no-retention", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state before-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state after-fetch-start", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state source-includes-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state source-omits-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state no-retention-direct-after-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state retained-direct-before-returned-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state overlapping-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state distinct-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state returned-value-equals-saved-after-direct-change", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: claims direct row from older result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: claims direct row from older result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: preserves product failure before browser diagnostics", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > determinism" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:value-undefined:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "actualReportKey": "src/query.ts#applySuccessfulResult:value-undefined:if ( oldItem && ! deepEquals ( oldItem , newItem ) ) { write ( { type : `update` , value : newItem } ) }", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1333", + "source": "src/query.ts", + "line": 2756, + "position": 97720, + "operator": "delete-call", + "function": "applySuccessfulResult", + "original": "write({ type: `update`, value: newItem })", + "replacement": ";", + "anchor": "write ( { type : `update` , value : newItem } )", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:delete-call:write ( { type : `update` , value : newItem } )", + "actualReportKey": "src/query.ts#applySuccessfulResult:delete-call:write ( { type : `update` , value : newItem } )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1334", + "source": "src/query.ts", + "line": 2756, + "position": 97728, + "operator": "field-drop", + "function": "applySuccessfulResult", + "original": "type: `update`", + "replacement": "", + "anchor": "write ( { type : `update` , value : newItem } )", + "outcome": "survived", + "class": "comparison-gap", + "changedTests": [ + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed seed 563", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > random seed", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 2", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 3", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 8", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 9", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 10", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 11", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 26", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 27", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 32", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 33", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 34", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 35", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 50", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 51", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 56", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 57", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 58", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 59", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 74", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 75", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 80", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 81", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 82", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > fixed history: direct ownership structural history 83", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state raw", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state derived", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state retained", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state no-retention", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state before-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state after-fetch-start", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state source-includes-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state source-omits-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state no-retention-direct-after-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state retained-direct-before-returned-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state overlapping-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state distinct-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: omits constructed state returned-value-equals-saved-after-direct-change", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: claims direct row from older result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: claims direct row from older result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > calibration: preserves product failure before browser diagnostics", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > determinism", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed seed 563", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > random seed", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 2", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 3", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 8", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 9", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 10", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 11", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 26", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 27", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 32", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 33", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 34", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 35", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 50", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 51", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 56", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 57", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 58", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 59", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 74", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 75", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 80", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 81", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 82", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > fixed history: direct ownership structural history 83", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state raw", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state derived", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state retained", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state no-retention", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state before-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state after-fetch-start", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state source-includes-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state source-omits-direct", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state no-retention-direct-after-fetch", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state retained-direct-before-returned-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state overlapping-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state distinct-saved-direct-key", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: omits constructed state returned-value-equals-saved-after-direct-change", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: claims direct row from older result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: claims direct row from older result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at first", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: keeps direct value after later returned result at firstDurable", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > calibration: preserves product failure before browser diagnostics", + "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > determinism" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `update` , value : newItem } )", + "actualReportKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `update` , value : newItem } )", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1335", + "source": "src/query.ts", + "line": 2756, + "position": 97744, + "operator": "field-drop", + "function": "applySuccessfulResult", + "original": "value: newItem", + "replacement": "", + "anchor": "write ( { type : `update` , value : newItem } )", + "outcome": "killed", + "by": [ + "cursor adapter through production pagination > refreshes changed backend rows before growing the window false/2" + ], + "laws": [ + "(unnamed)" + ], + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `update` , value : newItem } )~2", + "actualReportKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `update` , value : newItem } )~2", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1469", + "source": "src/query.ts", + "line": 2893, + "position": 103648, + "operator": "binary-swap", + "function": "needsPersistedBaseline", + "original": "&&", + "replacement": "||", + "anchor": "const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )", + "outcome": "killed", + "by": [ + "query.immediate-eager-materialization > fixed seed 1468" + ], + "laws": [ + "immediate eager materialization" + ], + "selectionGlobalKey": "src/query.ts#needsPersistedBaseline:binary-swap:const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )~2", + "actualReportKey": "src/query.ts#needsPersistedBaseline:binary-swap:const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1470", + "source": "src/query.ts", + "line": 2892, + "position": 103616, + "operator": "binary-swap", + "function": "needsPersistedBaseline", + "original": "&&", + "replacement": "||", + "anchor": "const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )", + "outcome": "killed", + "by": [ + "query.immediate-eager-materialization > fixed seed 1468" + ], + "laws": [ + "immediate eager materialization" + ], + "selectionGlobalKey": "src/query.ts#needsPersistedBaseline:binary-swap:const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )~3", + "actualReportKey": "src/query.ts#needsPersistedBaseline:binary-swap:const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )~2", + "pendingDisposition": "killed by oracle" + }, + { + "id": "query.ts:M1474", + "source": "src/query.ts", + "line": 2894, + "position": 103660, + "operator": "value-undefined", + "function": "needsPersistedBaseline", + "original": "queryToRows.has(queryHash)", + "replacement": "undefined", + "anchor": "const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )", + "outcome": "survived", + "class": "no-observed-difference", + "changedTests": [], + "selectionGlobalKey": "src/query.ts#needsPersistedBaseline:value-undefined:const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )~2", + "actualReportKey": "src/query.ts#needsPersistedBaseline:value-undefined:const needsPersistedBaseline = ( queryHash : string ) => retainedQueriesPendingRevalidation . has ( queryHash ) || ( syncMode === `eager` && persistence !== null && ! queryToRows . has ( queryHash ) )", + "pendingDisposition": "open: needs receiving witness or explicit maintainer allowance" + }, + { + "id": "query.ts:M1795", + "source": "src/query.ts", + "line": 3326, + "position": 121475, + "operator": "value-undefined", + "function": "handleQueryResult", + "original": "fetchStartGenerations.get(hashedQueryKey)", + "replacement": "undefined", + "anchor": "enqueueResultApplication ( hashedQueryKey , result , ( signal , token ) => needsPersistedBaseline ( hashedQueryKey ) ? reconcileSuccessfulResult ( queryKey , result , token , signal , undefined , fetchStartGenerations . get ( hashedQueryKey ) ?? directWriteGeneration , ) : applySuccessfulResult ( queryKey , result , token , undefined , signal , ) , )", + "outcome": "killed", + "by": [ + "controlled > query.persisted-rejected-write-cutoff > fixed seed 1794" + ], + "laws": [ + "fetched cache authority" + ], + "selectionGlobalKey": "src/query.ts#handleQueryResult:value-undefined:enqueueResultApplication ( hashedQueryKey , result , ( signal , token ) => needsPersistedBaseline ( hashedQueryKey ) ? reconcileSuccessfulResult ( queryKey , result , token , signal , undefined , fetchStartGenerations . get ( hashedQueryKey ) ?? directWriteGeneration , ) : applySuccessfulResult ( queryKey , result , token , undefined , signal , ) , )~3", + "actualReportKey": "src/query.ts#handleQueryResult:value-undefined:enqueueResultApplication ( hashedQueryKey , result , ( signal , token ) => needsPersistedBaseline ( hashedQueryKey ) ? reconcileSuccessfulResult ( queryKey , result , token , signal , undefined , fetchStartGenerations . get ( hashedQueryKey ) ?? directWriteGeneration , ) : applySuccessfulResult ( queryKey , result , token , undefined , signal , ) , )", + "pendingDisposition": "killed by oracle" + }, + { + "id": "authored:original-value-defect", + "source": "src/query.ts", + "line": 2739, + "operator": "authored", + "function": "applySuccessfulResult", + "original": "persistedBaseline?.get(key)?.value ?? currentSyncedItems.get(key)", + "replacement": "shouldUsePersistedBaseline ? persistedBaseline.get(key)?.value : currentSyncedItems.get(key)", + "outcome": "killed", + "by": [ + "controlled > query.persisted-direct-ownership > fixed seed 563" + ], + "laws": [ + "direct row value" + ], + "authored": true, + "intent": "restore the original missing current-value fallback when an accepted direct row is absent from durable baseline", + "selectionGlobalKey": null, + "actualReportKey": "src/query.ts#authored:original-value-defect", + "pendingDisposition": "killed by oracle" + } + ], + "evidence": { + "fullResult": "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/.retention-audit/query/results.json", + "selection": "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/.retention-audit/query-delta-selection.json", + "frozenManifest": "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/.retention-audit/frozen-snapshot.json", + "finalIntegrity": "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/.retention-audit/final-integrity.json", + "independentIntegrity": "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/.retention-audit/independent-integrity.json", + "excludedPendingTests": "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/.retention-audit/excluded-pending-work-tests.json", + "priorRefusal": "/private/tmp/tanstack-db-2394-query-delta-11d6ed3/.retention-delta-work-tests-refused" + } +} diff --git a/docs/contributing/oracle-reviews/2026-11-10-query-changed-callback-gap-audit.md b/docs/contributing/oracle-reviews/2026-11-10-query-changed-callback-gap-audit.md new file mode 100644 index 0000000000..05e92d9048 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-query-changed-callback-gap-audit.md @@ -0,0 +1,96 @@ +# Final Query changed-callback gap audit + +Run `89f398bd4b86` completed with **24 kills and 11 survivors** from 35 selected +candidates, with zero nondeterministic and zero excluded tests. Both unmutated +passes completed all 1,747 tests. The authored original missing-value-fallback +defect is killed. This is a gap inventory, not a closure claim for survivors. + +The immutable snapshot is `c0b65d926ca72ca04b96a205013845a42fbb21fb` at +`/private/tmp/tanstack-db-2394-query-delta-11d6ed3`. Query source SHA-256 is +`1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b`. +The selected scope is all 29 operators in the changed previously-owned-row +callback, five mapped previous targets, and the authored original-defect +control. It is not a whole-source mutation campaign. Exact commands, source +span offsets, operators, replacements, both contextual keys, outcomes, changed +test identities and kills are retained in the adjacent +[JSON evidence](2026-11-10-query-changed-callback-gap-audit.json). + +## Surviving candidates and required work + +| Candidates | Effect under investigation | Current disposition | +| --- | --- | --- | +| M1313, M1315, M1316, M1317 | Omit retirement of a settled ownerless direct-write position. | Open. Investigate scope-local omission followed by retained-cache reacquisition; the existing final-row checkpoint misses retired-key work and may miss the later public consequence. | +| M1314 | Force the direct-position presence guard true. Undefined then compares equal to an absent entry and deletes an absent key at retirement. A later numeric position remains protected. | Equivalent-behavior argument only; no allowance granted. Preserve the useful guard rather than add temporary undefined map entries solely for mutation score. | +| M1327 | Omit the removed-row tracking used to retire accepted row positions. | Open. Investigate retained-cache reacquisition followed by an earlier pending fetch that should replace the reacquired value. | +| M1328, M1330, M1332 | Write an update even when old and returned rows are equal. | Open comparison gaps. Commit-write diagnostics differ; final-row laws pass. Evaluate independent work or publication authority before changing comparisons. | +| M1334 | Omit the explicit update operation type from an adapter sync write. | Open comparison gap. The public SyncConfig.write input requires a declared operation; investigate protocol-boundary validation independently of final rows. | +| M1474 | Treat absence of the queryToRows.has result as true after negation, so later eager persisted results also take baseline reconciliation. | Open. Extra reconciliation remains observationally indistinguishable under this campaign; an equivalent-effect claim does not close the gap. | + +The first four cleanup mutations have the same suspected missing distinction; +they need a shared generated lifecycle witness. The proposed public consequence +and the redundant-guard argument above are diagnoses to test, not verified +closure. Every survivor still needs an oracle kill or an explicit maintainer +allowance under AGENTS.md. Recording these traces does not grant one. + +## Mapped previous targets + +| Original ID | Current ID | Result | +| --- | --- | --- | +| M563 | M563 | Killed: direct row ownership | +| M1468 | M1469 | Killed: immediate eager materialization | +| M1469 | M1470 | Killed: immediate eager materialization | +| M1473 | M1474 | Survived; open | +| M1794 | M1795 | Killed: fetched cache authority | + +The exact original-value-fallback control restores +`shouldUsePersistedBaseline ? persistedBaseline.get(key)?.value : currentSyncedItems.get(key)` +and is rejected by the direct-row-value law. + +## Instrument and scope integrity + +The run uses Node 24.22.1 with forks, seed 2367, and the isolated e1fb CLI copy +with the verified expected-failure, complete-capture and full-title-identity +repairs. The installed CLI remains unchanged. Independent verification found no +changes to 3,749 tracked snapshot files, 1,092 copied workspace build outputs, +the control, runtime or 13 instrument modules, and no live-checkout source +imports. The original 55-kill conversion-retention experiment is separate and +completed with all 55 stable operator kills retained; see the +[retention audit](2026-11-10-startup-property-gap-retention.md). + +The initial delta snapshot accidentally included in-progress PowerSync batching +calibration changes. Its clean baseline refused before mutation because a +Query receiver without a batch-size history could not reject that calibration. +The refusal is preserved under `.retention-delta-work-tests-refused`; it is not +a product verdict. The completed c0b snapshot restores those five unrelated +pending-work files to c4 while retaining final Query source and its receiving +witnesses. It therefore establishes this Query delta, not final integrated +PowerSync/RxDB readiness. + +There is also a key-identity caveat: the CLI final report can assign duplicate +anchor suffixes after selection, while the selection script assigns keys over +the whole source. For example M1313 is `~2` in the global selection and has no +suffix in this report. The evidence retains both strings and the exact source +SHA, original span offset, anchor, operator and replacement. Do not equate +contextual strings from different selections without those anchors. + + +## Additive scope-omission closure experiment (2026-11-10) + +The original audit above remains unchanged at 24 kills and 11 survivors. The +[scope-omission receiving extension](2026-11-10-query-scope-omission-gap.md) +adds generated retained-cache next-use histories and rejects M1313, M1315, +M1316 and M1317 on controlled/native SQLite, plus M1313 on Chromium OPFS, +at the `acquired` row comparison. The clean integrated suites pass 36 Node +and 18 Chromium tests. These are additive executed results, not a replacement +CLI score. M1314 and the separate M1327 contract proposal are not claimed +closed by this extension. + +## M1334 receiving comparison supplement + +The later [sync-write protocol evidence](2026-11-10-query-sync-write-protocol.md) +closes M1334 by observing the actual raw operation at the adapter write boundary. +The exact field deletion fails the shared comparison on controlled storage, +native Node SQLite, and actual Chromium/OPFS; the same clean replays pass. +The original audit result above remains a record of the earlier comparison. +This supplement changes no production source and grants no allowance for any +other survivor. diff --git a/docs/contributing/oracle-reviews/2026-11-10-query-rejected-write-cutoff-gap.md b/docs/contributing/oracle-reviews/2026-11-10-query-rejected-write-cutoff-gap.md new file mode 100644 index 0000000000..fa4c9b4538 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-query-rejected-write-cutoff-gap.md @@ -0,0 +1,222 @@ +# Query first-result cutoff gap evidence + +This receiving extension closes a source-mutation gap; it found no clean-production +cutoff defect. Keep the production cutoff lookup. The first experiment used +immutable candidate inputs; the final section records the integrated receiver +against the separately repaired Query source. + +## Law and authority + +A missing-key direct update/delete rejects without an accepted row edit. A stale +observer notification does not turn an unchanged successful fetched response +into a local cache edit. A new on-demand peer with `staleTime: Infinity`, no +invalidation and no accepted direct write can acquire that cached response +without a new provider request. + +Authority is the Query guide's **Understanding the Data Stores**, item 1 and +**Direct Writes and Query Sync**, together with its `staleTime` option and the +primary ownership owner's existing `runDirectWriteOrderHistory` judgment: +`cacheStillFetched` permits acquisition without a request. The guide describes +update as editing an existing item, distinguishes accepted writes from fetched +responses, and keeps retained response reuse under `staleTime`/`gcTime`. This +candidate extends that established law across a rejected write and an observer's +own stale timeout. It does not derive expected behavior from the cutoff Map. + +The independent model has one fetched response, no accepted direct write, and +one peer acquisition. It predicts the authored first row, one source owner, and +exactly one physical request. Production clocks, cache markers and branch +classifiers do not appear in the model. + +## History and observation cuts + +Histories generate raw/direct-wrapped response arrays, missing-key update/delete +rejection before fetch or during hydration, and a no-write neighbor. Values and +finite stale deadlines vary independently. Nine constructed builders retain +random neighboring values and enforce actual entered states. + +1. Create an eager persisted Collection with a disabled observer and empty saved baseline. +2. Start a physical fetch through QueryClient after sync installed its cache listener. +3. Hold `hydrateBaseline()` after its real work completed; reject the authored write at its selected cut. +4. Release hydration and await first preload. Compare public/durable rows and source ownership, rejection and fetch count. +5. Deliver the actual scheduled Query stale callback through the explicit Query timer provider. Cross a native host task, await every returned public applied receipt until the list stays stable, and compare public/durable rows and unchanged fetch freshness. +6. Acquire a new on-demand peer with `staleTime: Infinity`. Compare peer rows/owners, source rows/durable rows and exact fetch count. + +The clock abstraction changes `Date.now` and only Query Core's timer provider. +SQLite, worker and host timers remain real. It executes Query's actual scheduled +callback, not an invented observer update. A stale-only notification owes no +extra sync commit: commits/receipts are recorded as settlement evidence and +count diagnostics, with no positive-count product obligation. Cleanup restores +the provider after client cleanup and rejects leaked Query timers. + +Product observations are judged in trace order. Missing terminal observations +are judged after recorded facts. The browser envelope forwards raw facts before +rethrowing reconstructed failure diagnostics. Detached page errors use a cursor; +errors that arrive between histories are consumed once by the next history. +The selected primary difference retains secondary browser diagnostics. + +## Exact mutation and result + +Frozen source SHA-256: +`6bbf1542e6daa38f33315445c5bb0ae3fbbf90d907cd388cb5d13f41c3b37cc5`. +Head when the final source SHA was checked: `0e86c1fdae85520aff142f141f71ccf46a8ee8bd`. +The source bytes are unchanged from the earlier frozen head. + +Exact `query.ts:M1794`, value-undefined at frozen line 3327/position 121514, +replaces `fetchStartGenerations.get(hashedQueryKey)` with `undefined`, retaining +the direct-write fallback. Applied mutant SHA: +`8b1e6da9ea67aa7f5db33725a9ccab07481849748372b31f423f06f524a99150`. +The immutable transform verifies the original anchored substring, logs the +served SHA, uses distinct plugin/cache identities, and disables fsModuleCache. +No production file is overwritten. + +The mutation loses the stored physical cutoff after a rejected write during +hydration. It leaves an older cutoff in the Map. The stale observer notification +then takes an unnecessary merge path and marks the still-fetched cache as a +local direct patch. The new peer refetches. Clean production makes one request; +the exact mutation makes two. + +- Final controlled + actual Node SQLite: **30 checks GREEN**, `oracle-clean-final-4.log`. +- Final actual Chromium worker/OPFS: **15 checks GREEN**, `browser-clean-final-2.log`. +- Actual Chromium receiving-fidelity controls: **11 GREEN**, `browser-fidelity-final-2.log`. +- Exact Node mutation replay: **2 product assertion RED**, `oracle-M1794-replay-final-2.log`; base ID, seed1794, path `0:1:1:1`. +- Exact browser mutation replay: **product assertion RED**, `browser-M1794-replay-final.log`; distinct `.chromium-opfs` ID, same seed/path. +- Clean browser exact counterpart: **1 GREEN**, `browser-clean-replay-final.log`. +- Clean Node exact counterpart: **2 GREEN**, `oracle-clean-replay-final.log`. +- Generated-only missing-reach control: **2 RED** at `constructed states`, `generated-omission-replay-final.log`; base ID, seed1794/path `7`. +- Clean exact omission counterpart: **2 GREEN**, `generated-omission-clean-replay-final.log`. +- Strict TypeScript and typed ESLint: **GREEN**, `typescript-final.log`, `eslint-final.log`. + +The final browser mutation's first violation is `peerFetches: expected 1, +observed 2`. It also retains a secondary closed-worker persistence diagnostic +from cleanup after the mutation's extra work. This is a recorded product +assertion RED with separate secondary runtime evidence. It is not a runtime-only +kill, and that diagnostic is not presented as a second independently diagnosed +production defect. + +A prior clean browser rerun had 13 passing checks and one OPFS cleanup +`NoModificationAllowedError` (`browser-clean-final.log`). The final fixture +retries only that exact busy exception after awaited database close across real +host tasks, retaining each retry in `opfsCleanupRetry`; persistent failures still +throw and final artifact absence is required. Actual-browser controls calibrate +transient success, permanent failure, and preservation of an earlier product +mismatch through permanent cleanup failure. The original failed run remains +retained as cleanup evidence. + +## Reach + +Each campaign runs 20 generated histories plus nine randomized constructed +histories. Every state is enforced independently per host/campaign. Counts below +come verbatim from final logs; fixed histories are not used for reach credit. + +- **controlled/fixed1794**: `{"before-rejected-write":9,"delete-rejection":8,"during-rejected-write":11,"no-write":9,"peer-acquired-fetched-cache":29,"raw":16,"stale-callback-delivered":29,"update-rejection":12,"wrapped":13}` +- **controlled/random**: `{"before-rejected-write":7,"delete-rejection":10,"during-rejected-write":12,"no-write":10,"peer-acquired-fetched-cache":29,"raw":17,"stale-callback-delivered":29,"update-rejection":9,"wrapped":12}` +- **node-sqlite/fixed1794**: `{"before-rejected-write":9,"delete-rejection":8,"during-rejected-write":11,"no-write":9,"peer-acquired-fetched-cache":29,"raw":16,"stale-callback-delivered":29,"update-rejection":12,"wrapped":13}` +- **node-sqlite/random**: `{"before-rejected-write":8,"delete-rejection":9,"during-rejected-write":17,"no-write":4,"peer-acquired-fetched-cache":29,"raw":15,"stale-callback-delivered":29,"update-rejection":16,"wrapped":14}` +- **chromium-opfs/fixed1794**: `{"before-rejected-write":9,"delete-rejection":8,"during-rejected-write":11,"no-write":9,"peer-acquired-fetched-cache":29,"raw":16,"stale-callback-delivered":29,"update-rejection":12,"wrapped":13}` +- **chromium-opfs/random**: `{"before-rejected-write":11,"delete-rejection":6,"during-rejected-write":12,"no-write":6,"peer-acquired-fetched-cache":29,"raw":14,"stale-callback-delivered":29,"update-rejection":17,"wrapped":15}` + + +## Limits + +One eager empty-baseline startup, one successful row response and one later +on-demand peer share a QueryClient. The fetched result is raw or an array +exposed directly by select. The peer remains unpersisted while the source uses +controlled storage, actual Node SQLite or actual browser worker/OPFS. There is +no arbitrary failed-I/O, accepted direct mutation, limit/cursor, concurrent tab, +remote server or every-scheduling-path claim. OPFS handle-release retries concern +fixture teardown, not a new database.close product law. A bounded source mutant +rejection closes this anchored gap; it is not a universal equivalence or whole-PR +readiness claim. Parent owns integration and a regenerated gap CLI report. + + +## Integrated current-source verification + +The candidate was integrated into normal package commands after the independent +review. The source now includes the separate direct-insert/later-returned-value +repair. At working head `0e86c1fdae85520aff142f141f71ccf46a8ee8bd`, the tested +Query source SHA-256 is +`1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b`. +This section supplements the frozen-source evidence; it does not rewrite it. + +The first normal Node run revealed that the package defaults to server runtime: +Query schedules no stale timeout there. The receiving wrapper now explicitly +uses Vitest jsdom, matching the controlled client premise. The Node SQLite +storage remains native. The first normal browser fidelity run revealed a +missing ready-function argument after reuse of the generic Chromium helper: +it waited for the default TrailBase entry. The bootstrap control now names +`runCutoffCandidate`. Neither repair changes the model or comparison. +An intermediate unprivileged browser attempt failed setup with loopback `EPERM`; +the authorized loopback-server runs below are the receiving evidence. + +Normal commands, from the worktree unless otherwise noted: + +```sh +# From packages/query-db-collection: +node ../../node_modules/vitest/vitest.mjs run --config vitest.config.ts tests/persisted-rejected-write-cutoff-oracle.test.ts --coverage.enabled=false +# From worktree root: +node node_modules/vitest/vitest.mjs run --config packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-oracle.browser.test.ts packages/browser-db-sqlite-persistence/tests/query-rejected-write-cutoff-browser-fidelity.test.ts +``` + +- Normal controlled + native Node SQLite: **30 GREEN**, `integration-node-2.log`. +- Normal actual Chromium worker/OPFS plus fidelity: **26 GREEN** (15 oracle and 11 controls), `integration-browser-3.log`. +- Strict scoped TypeScript: **GREEN**, `integration-typescript.log`. +- Typed ESLint after relative-import ordering: **GREEN**, `integration-eslint-2.log`. + +The exact operator was reanchored at current line 3326/position 121475. It still +replaces only `fetchStartGenerations.get(hashedQueryKey)` with `undefined`. +Both immutable receivers logged applied mutation SHA-256 +`59922ebe9a534c8272fabe9503676c495830aeeed4db0a3d5cc85eff914502e6`. +They retain separate plugin/cache identities and `fsModuleCache: false`. +The earlier old-anchor attempt `oracle-M1794-replay-final-3.log` is a setup +anchor mismatch, not a kill. + +Current-source direct replay commands: + +```sh +QUERY_CUTOFF_CANDIDATE_MUTANT=query.ts:M1794 TANSTACK_DB_ORACLE_PROPERTY=query.persisted-rejected-write-cutoff TANSTACK_DB_ORACLE_SEED=1794 TANSTACK_DB_ORACLE_PATH=0:1:1:1 node node_modules/vitest/vitest.mjs run --config /private/tmp/tanstack-db-2394-query-cutoff-candidate/integration-mutant.config.ts +QUERY_CUTOFF_INTEGRATION_RECEIVER=browser QUERY_CUTOFF_CANDIDATE_MUTANT=query.ts:M1794 TANSTACK_DB_ORACLE_PROPERTY=query.persisted-rejected-write-cutoff.chromium-opfs TANSTACK_DB_ORACLE_SEED=1794 TANSTACK_DB_ORACLE_PATH=0:1:1:1 node node_modules/vitest/vitest.mjs run --config /private/tmp/tanstack-db-2394-query-cutoff-candidate/integration-mutant.config.ts +``` + +Remove `QUERY_CUTOFF_CANDIDATE_MUTANT` for each exact clean counterpart. + +- Integrated Node mutant: **2 assertion RED**, `integration-node-M1794.log`; both fail `peerFetches`, expected 1/observed 2. +- Exact clean Node counterpart: **2 GREEN**, `integration-node-clean-replay.log`. +- Integrated real Chromium mutant: **1 assertion RED**, `integration-browser-M1794-2.log`; same checkpoint and difference. +- Exact clean Chromium counterpart: **1 GREEN**, `integration-browser-clean-replay-2.log`. + +The current browser mutant also preserves the secondary closed-worker +persistence diagnostic. The first failure remains the recorded physical-request +count assertion. This is not a runtime-only mutation rejection. + +Current normal campaign reach, verbatim: + +**controlled and node-sqlite** + +```text +query.persisted-rejected-write-cutoff fixed seed 1794 reach {"before-rejected-write":9,"delete-rejection":8,"during-rejected-write":11,"no-write":9,"peer-acquired-fetched-cache":29,"raw":16,"stale-callback-delivered":29,"update-rejection":12,"wrapped":13} +query.persisted-rejected-write-cutoff random seed reach {"before-rejected-write":8,"delete-rejection":8,"during-rejected-write":13,"no-write":8,"peer-acquired-fetched-cache":29,"raw":16,"stale-callback-delivered":29,"update-rejection":13,"wrapped":13} +query.persisted-rejected-write-cutoff fixed seed 1794 reach {"before-rejected-write":9,"delete-rejection":8,"during-rejected-write":11,"no-write":9,"peer-acquired-fetched-cache":29,"raw":16,"stale-callback-delivered":29,"update-rejection":12,"wrapped":13} +query.persisted-rejected-write-cutoff random seed reach {"before-rejected-write":9,"delete-rejection":9,"during-rejected-write":12,"no-write":8,"peer-acquired-fetched-cache":29,"raw":14,"stale-callback-delivered":29,"update-rejection":12,"wrapped":15} +``` + +**chromium-opfs** + +```text +query.persisted-rejected-write-cutoff.chromium-opfs fixed seed 1794 reach {"before-rejected-write":9,"delete-rejection":8,"during-rejected-write":11,"no-write":9,"peer-acquired-fetched-cache":29,"raw":16,"stale-callback-delivered":29,"update-rejection":12,"wrapped":13} +query.persisted-rejected-write-cutoff.chromium-opfs random seed reach {"before-rejected-write":13,"delete-rejection":7,"during-rejected-write":14,"no-write":2,"peer-acquired-fetched-cache":29,"raw":11,"stale-callback-delivered":29,"update-rejection":20,"wrapped":18} +``` + +Integrated file SHA-256 values after import-order lint repairs: + +```text +5213eab0a015540fa56cb4b52ced862c428aa494d963b4b63ad3de2b08e8706e packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.ts +7125e59272f591c940335e59fcefc4faaba687deaea7b160269c8c6d0af4d8d9 packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.test.ts +f2d47cad12d8b0892714f0c756b88403a064296217cda1054b93a13729bd0287 packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-oracle.browser.test.ts +2d9e94072d125e911c3d511739f040f9380a04868f58272d272721a5328b5309 packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.ts +bc217494ee30536e8942b4ee398411fc5f2720fb52c636c90c5a9ea52b86b014 packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-cleanup.ts +b24f2dc1c266395a35f1ccd4277b16f4a6ed0dc264bc072fb20ee1f2fb4269c5 packages/browser-db-sqlite-persistence/tests/query-rejected-write-cutoff-browser-fidelity.test.ts +``` + +The stated limits remain in effect. This closes the anchored M1794 gap; it does +not establish whole-PR readiness. The parent owns the regenerated gap report, +coverage-map integration, and branch-wide lint. diff --git a/docs/contributing/oracle-reviews/2026-11-10-query-scope-omission-gap.md b/docs/contributing/oracle-reviews/2026-11-10-query-scope-omission-gap.md new file mode 100644 index 0000000000..b77a9613fb --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-query-scope-omission-gap.md @@ -0,0 +1,207 @@ +# Query scope omission and retained-cache next use + +Reviewed base HEAD: `0e86c1fdae85520aff142f141f71ccf46a8ee8bd`, with the current +uncommitted Query source SHA-256 +`1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b`. +Integrated oracle SHA-256: `13834157b4318778b0600890f20c015511583212377d9c658c4482e0eae602dc`. +This additive coverage experiment changes no production source. It does not +replace the original delta audit's immutable 24-kill/11-survivor result. + +## Law and ownership + +The primary owner is Query's ownership-lifecycle oracle. Its receiving extension +[scope-omission-reacquisition-oracle.ts](../../../packages/query-db-collection/tests/scope-omission-reacquisition-oracle.ts) +uses the existing independent direct/fetch overlay judgment. +Authority is [Direct Writes and Query Sync](../../collections/query-collection.md#direct-writes-and-query-sync): +a later successful complete unbounded matching response can remove an ownerless +direct row; that absence belongs only to its scope. A distinct retained Query +remains available under normal stale/gc rules. Acquiring it after the direct row +was removed starts a new row lifetime from that retained result. If A returns +x instead, A's later physical fetch supplies the continuously owned value. + +The model predicts rows from authored successful responses and accepted writes. +It contains no direct-write positions, generation counters, cache classifiers, +or production transition helpers. Cache order changes the legal history, not +its expected terminal answer. + +## Grammar, path and cuts + +A distinct real peer Collection retains scope B (`id == x`) with a successful +Query result. The tested Collection accepts ownerless `writeInsert` or +`writeUpsert`. Scope A (`category == shared`) later completes and returns or +omits x, optionally alongside y. Then the tested Collection acquires B without +a new fetch. Raw arrays and a derived `edges[].node` projection, B fetch before +or after the write, and independent integer values from -10 through 10 are +generated. A is a proper scoped predicate, so full-source reuse cannot bypass +B's distinct cache entry. No `setQueryData` seeds the retained premise. + +The same production driver and check run with controlled persistence, native +Node SQLite, and real Chromium worker/OPFS persistence. Public and durable rows +are compared after the direct write, after A, and after acquiring B. The peer's +retained values and exact provider request count (two) are also compared. +Durable observations use the wrapping capability's `scanPersistedRows`, which +addresses the actual managed on-demand cache generation. + +Every campaign has 30 arbitrary histories and eight randomized constructed +histories. Each builder makes a fresh object and records its obligation in a +WeakMap. Both seeded and generated builder outputs must contain their required +actual reach fact. Eight missing-state calibrations and a separate control +that feeds generated inputs through every builder and the real receiving path +reject omitted facts. The latter runs on each of the three hosts. + +The eight states are measured from actual cache shape, accepted write rows, +queryFn/write event order, A's delivered rows and the observed row cuts. In the +normal integrated fixed-seed campaign, each host recorded: + +| State | Histories | +| --- | ---: | +| raw | 23 | +| derived | 15 | +| insert | 20 | +| upsert | 18 | +| old-cache-after-omission | 13 | +| new-cache-after-omission | 11 | +| returned-direct-key | 14 | +| nonempty-omission | 17 | + +The fresh integrated campaigns recorded these counts in the same order: +controlled `[19,19,22,16,11,13,14,10]`, native SQLite +`[21,17,16,22,14,8,16,13]`, Chromium OPFS +`[19,19,25,13,10,10,18,9]`. Each required state has an enforced minimum of one +randomized construction per host/campaign; fixed histories supply none of it. + +Grammar controls: the minimized reported omission witness is reconstructible +with raw/insert/before-write/omitted/empty and zero values. Removing either +cache order misses a causal position; removing either shape misses a cache +projection path; removing insert/upsert misses an admission path. Returned x +opposes an unconditional reacquisition overwrite; sibling y opposes clearing +all A rows. Empty and nonempty responses, equal and unequal values, and both +integer endpoints are in the bounded domain. A limited/cursor response is not +in this grammar because it cannot establish complete matching omission. + +## Executed results + +Normal repository commands passed: + +```sh +node node_modules/vitest/vitest.mjs run --config packages/query-db-collection/vitest.config.ts packages/query-db-collection/tests/scope-omission-reacquisition-oracle.test.ts +# 36 passed, including both campaigns and generated-construction controls per Node host +node node_modules/vitest/vitest.mjs run --config packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts packages/browser-db-sqlite-persistence/e2e/query-scope-omission-oracle.browser.test.ts +# 18 passed, including fixed/fresh campaigns and generated-construction control +``` + +Both Node hosts and Chromium use the same grammar, model and check. The normal +browser host disables HMR and file watching. Mutation browser runs additionally +use an immutable source transform whose plugin/cache identity includes candidate +SHA-256; they require a positive transform and zero watched paths before any +history. Source and original-span guards reject drift before mutation. + +The exact replay coordinates are seed `1313`, path `0:0:0:0:0:0`: + +```json +{"shape":"raw","operation":"insert","cacheOrder":"before-write","includesDirect":false,"sibling":false,"bValue":0,"directValue":0,"sourceValue":0,"siblingValue":0} +``` + +M1313 returns undefined instead of reading the direct position in complete-scope +omission cleanup (line 2742, stable anchor +`src/query.ts#applySuccessfulResult:value-undefined:const directPosition = unownedDirectWritePositions . get ( key )`). +The global-selection identity adds `~2`; this delta report identity does not. +The exact SHA/span/operator, rather than that suffix alone, identifies it. +M1315 makes the cleanup guard false; M1316 changes its inequality to equality; +M1317 removes the settled-position registration. Each produced **two assertion +failures**, one per Node host, at `acquired`: expected `{x:0}`, observed `{}`. +M1313 additionally produced **one assertion failure on real Chromium OPFS** at +the same cut. Browser claims for M1315/16/17 were not executed. + +| Operator | Frozen candidate SHA-256 | +| --- | --- | +| M1313 | `35b2b4e1a974530e73051e65f15cb6bbc86c318cf0084280b0b0b9fbb1162b7f` | +| M1315 | `fcbe54b7e59bce34228227e31530846d6e9971d96505de2831e3bcefa24b1bbc` | +| M1316 | `ed4ccb39f1ab4c489bd274cad3a510c65e29544dd35b9abbeb6fb9acc800016b` | +| M1317 | `b53d34ec12a0da52a6cdafa23ec19d9208de2303a11981f216961ed4625ac100` | + +Exact clean replay counterparts passed (two Node, one Chromium). These are +assertion kills at the intended next-use checkpoint. They are not timeout, +bootstrap, cleanup, or runtime-error classifications. Temporary mutation command: + +```sh +QUERY_SCOPE_MUTANT=query.ts:M1313 TANSTACK_DB_ORACLE_PROPERTY=query.scope-omission-reacquisition TANSTACK_DB_ORACLE_SEED=1313 TANSTACK_DB_ORACLE_PATH=0:0:0:0:0:0 node node_modules/vitest/vitest.mjs run --config /private/tmp/tanstack-db-2394-scope-omission/integrated.config.ts +# Chromium: property suffix .chromium-opfs and browser.config.ts +# Omit QUERY_SCOPE_MUTANT for the exact clean counterpart. +``` + +Scoped TypeScript, typed ESLint and branch-wide +`pnpm --config.verifyDepsBeforeRun=false lint:changed` passed. The integrated wrappers expose the +shared check inline for oracle lint; no rule is disabled. An independent +instrument reviewer cleared the bounded law/model/path and inspected the +isolated Chromium mutant/clean logs. + +## Calibration and discarded diagnoses + +The shared check compares recorded product facts before diagnosing missing +later cuts, false completion, a missing browser marker or detached browser +errors. Controls reject wrong rows in a truncated prefix, wrong rows followed +by false completion, wrong rows without the browser marker, missing durable +observations, missing constructed reach, and loss of the reacquired row. +Browser diagnostics remain secondary beside an earlier product violation. +Cleanup uses the shared failure-preserving helpers and verifies OPFS removal. + +Early scratch histories used `{}` for A. That is the full-source path, which can +legally satisfy B without B's distinct cached result. Their failures were +wrong-path diagnoses and are not product bugs or closure evidence. The final +scoped A predicate fixes the premise. Earlier browser runs used the copied host +without its later HMR/watch fences; they are superseded by the isolated rerun. +After central registration, two scratch browser replay attempts refused during +setup because the overlay appended the newly registered IDs twice. Their +zero-test failures are setup failures. Deduplicating that scratch registry +restored the exact clean GREEN and M1313 assertion RED on the final shared +module; those failures are not mutation kills. + +A three-cache M1327 proposal asked one old cached value to overwrite another +old cached value acquired during a later row lifetime. Clean production did not +do that. The guide's later-fetch precedence covers continuous ownership, and +does not clearly order these pre-lifetime cached acquisitions. That proposal +remains an unaccepted stronger law in scratch; its failures are not product +bugs, and the model was not changed to match them. M1314's always-true guard +remains a separate allowance question, not a kill from this experiment. + +## Rubric and limits + +ORC-001–007: contract/limits, independent overlay judgment, literate layers, +grammar controls, three real receiving paths/cuts, calibrations/mutation results, +and identical fixed/fresh campaigns with tested direct replay are above. +ORC-008: no new stateful model; stateless authored-response projection is enough. +ORC-009: A/B are exact subset demands and `acquired` is load applied settlement; +row lifetime is the guide's ownership interval, not an internal generation. +ORC-010: prefix/secondary-error calibrations plus shared cleanup preserve failure. +ORC-011: no named shared semantic-fault hypothesis required another formulation; +the M1327 competing proposal is explicitly unresolved contract design. +ORC-012–015: versioned source/receiving boundary, returned-versus-omitted and +old-versus-new-cache witnesses, real-host handoff, actual constructed reach and +per-host generated-output controls are recorded. ORC-016: ordinary examples +are absent; all comparisons and controls invoke the same declared check. + +This closes four concrete cleanup-family mutant gaps on the two Node receiving +paths and M1313 on Chromium OPFS. It does not close every ownership or source +ordering law. Limits are successful I/O; one same-run direct row; two fixed +matching exact predicates in the stated A/B order; raw/one derived encoding; +numeric values; no stale refetch, window/cursor, external cache edit, concurrent +writer, arbitrary schedule, multi-cache conflict policy, service or wire claim. +Those broader ownership histories remain with the primary ownership oracle; +no new exclusion or maintainer allowance is created here. + +## Final package integration + +The normal Query oracle command now includes this receiver. Its first full run +passed 1,663 runtime/type checks and reported one skipped collector artifact. +A targeted JSON report identified that artifact as the type collector's +unexpanded `host` registration of the generated-construction calibration. +Both actual controlled/SQLite registrations passed, as did all 36 runtime +checks in the targeted run. + +The two Query configs now exclude this runtime registration file from the +type-test collector, as they do the other `oracleProperty` wrappers. The file +remains in the TypeScript program. The final normal command +`pnpm --config.verifyDepsBeforeRun=false test:oracles --maxWorkers=1` passes +**1,663/1,663 checks**, with no skipped checks or type errors, in 66.25 seconds. +No product history, comparison, calibration or run budget was removed. diff --git a/docs/contributing/oracle-reviews/2026-11-10-query-startup-receiving-gaps.md b/docs/contributing/oracle-reviews/2026-11-10-query-startup-receiving-gaps.md new file mode 100644 index 0000000000..283ebb997e --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-query-startup-receiving-gaps.md @@ -0,0 +1,262 @@ +# Query startup receiving gaps + +This evidence concerns PR #2394's Query startup and direct-write receiving +extensions. It does not establish whole-PR readiness. The source under test is +`packages/query-db-collection/src/query.ts`, SHA256 +`1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b`. +The before-repair source was +`6bbf1542e6daa38f33315445c5bb0ae3fbbf90d907cd388cb5d13f41c3b37cc5`. + +## Laws and ownership + +The primary owner is +`packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts`. +The receiving extensions reuse its independent +`mergeDirectWritesOverFetch` model for values. They add a small metadata +projection and a synchronous creation checkpoint; they do not copy Query's +fetch generations, retained-baseline classifier or observer state machine. + +- **Direct-write ordering:** a complete physical fetch that starts after an + accepted direct write replaces that row's earlier value. A result from an + earlier fetch preserves the accepted write. Authority: the Query guide's + **Direct Writes and Query Sync** and the primary direct-order law. +- **Direct-row ownership:** a new direct insert infers no subset owner. An + update or upsert of an already-owned row preserves its established owners. + A later complete physical fetch can claim or remove the row. Authority: + the same guide section and the primary direct-row-lifetime law, which + preserves public values and persisted owners through direct writes. +- **Immediate materialization:** fresh nonpersisted eager `initialData` and + exact-key cached data materialize rows and readiness when synchronous + `createCollection({startSync:true})` returns. Existing cached data suppresses + the initializer; a disabled fresh Query starts no request. Authority: the + guide's **Initial Data** section and the existing cached-query immediate-read + receiver in `query.test.ts`. + +`query.persisted-direct-ownership` owns the controlled and real Node SQLite +receivers. `query.persisted-direct-ownership.chromium-opfs` executes the same +model, grammar and comparison in real Chromium with worker SQLite/OPFS. +`query.immediate-eager-materialization` and its `.chromium-opfs` receiver +share the immediate-materialization judgment. Each receiving path constructs +its own histories and reach-obligation ledger. The registry and replay +manifest name all four executing owners separately. + +## Histories and observations + +The direct grammar crosses raw/derived responses, retained/unretained saved +baselines, direct writes before fetch or after fetch starts, legal insert/upsert, +returned/omitted source keys, distinct/overlapping saved and direct keys, and +independent numeric saved/direct/source values. Overlap uses upsert because +inserting an existing key is illegal. A dedicated construction forces a +retained distinct saved baseline, an earlier direct write, a returned direct +key and differing values. Another forces saved/direct overlap where the +fetched value equals the saved value after an intervening direct upsert. +The 13 declared states have 12 constructors. Each campaign runs 40 generated +histories plus its required constructions. The 96 fixed input fixtures replay +the same model, production driver and comparison; they are not claimed to be +96 behaviorally distinct cases. `keepsSaved` is irrelevant when the saved and +direct keys overlap. + +The production driver uses a real QueryClient, accepted direct writes, an +explicit source-response gate and applied source commit receipts. At `first` +and `firstDurable`, it compares public rows and persisted rows including exact +source-owner metadata. It waits for the public refetch fulfillment when that +operation exists. The after-cleanup durable scan is diagnostic, not a stronger +cleanup or publication promise. + +The immediate grammar crosses initialData/cache, raw/derived documents, +literal/function initialization and empty/nonempty unique numeric rows. +Its eight states have eight constructors; each campaign runs 40 generated +histories plus required constructions, with 16 additional fixed inputs. +The driver copies rows, readiness, source-call count and initializer-call count +at `creation-return`, before its first await. + +Both judgments compare available product prefixes before requiring completion. +Empty or truncated normal returns fail at the unrecorded `completed` +checkpoint. The framework preserves an original driver error when that missing +checkpoint was never recorded, and preserves an observed product mismatch +before later cleanup failure. Detached browser diagnostics remain secondary +to a product or required-reach mismatch. A transported cleanup error retains +both its original diagnostics and detached page errors. + +## RED before the production repair + +Before source changes, the shared direct property failed on controlled +persistence, real Node SQLite and actual Chromium/OPFS. Seed `563`, path +`15:0:0:0:0:0` reaches a retained distinct saved baseline, an accepted direct +insert `x=0`, and a later physical response `x=1`. Query cache holds `x=1` and +the source's applied receipt completes, but public and durable rows remain +`x=0`. The comparison rejects `first` / `direct row value`. + +The missing distinction was a same-run direct key absent from the retained +persisted baseline. The first source result added that key to its prior-owned +set, but its diff had no old value and skipped the update. The repair reads +accepted rows when the persisted entry is absent: + +```ts +const oldItem = + persistedBaseline?.get(key)?.value ?? currentSyncedItems.get(key) +``` + +The saved/direct overlap and equal-to-saved returned value remain generated +neighbors. Their receiving controls passed before the repair, so their model +was not weakened to match the new output. + +## GREEN and reach + +The normal Node package configuration, including its typecheck collector, +passed **279 tests in four reported files**, with no type errors (19.70s). +The command selected the three new runtime receiver/control files; the extra +reported file is the ordinary control file's typecheck collector. + +```sh +pnpm_config_verify_deps_before_run=false pnpm --filter @tanstack/query-db-collection exec vitest run --coverage.enabled=false \ + tests/persisted-direct-ownership-oracle.test.ts \ + tests/immediate-eager-materialization-oracle.test.ts \ + tests/query-receiving-fidelity.test.ts +``` + +The complete normal Chromium suite passed **153 tests in two files** +(395.33s), including all fixed inputs, fixed/fresh generated campaigns, +checker calibrations and determinism controls. + +```sh +pnpm_config_verify_deps_before_run=false pnpm --filter @tanstack/browser-db-sqlite-persistence exec vitest run --config vitest.adapter-oracles.config.ts \ + e2e/query-persisted-direct-ownership-oracle.browser.test.ts \ + e2e/query-immediate-cache-oracle.browser.test.ts +``` + +Each direct column is one receiving path and one complete generated campaign. +Fixed seed is `563`; fresh campaigns use the seed recorded by the runner. + +| Declared state | Controlled fixed/fresh | Node SQLite fixed/fresh | Chromium fixed/fresh | +| --- | ---: | ---: | ---: | +| raw | 26 / 30 | 26 / 30 | 26 / 26 | +| derived | 26 / 22 | 26 / 22 | 26 / 26 | +| retained | 25 / 21 | 25 / 28 | 25 / 30 | +| no-retention | 27 / 31 | 27 / 24 | 27 / 22 | +| before-fetch | 32 / 24 | 32 / 26 | 32 / 27 | +| after-fetch-start | 20 / 28 | 20 / 26 | 20 / 25 | +| source-includes-direct | 31 / 29 | 31 / 29 | 31 / 36 | +| source-omits-direct | 21 / 23 | 21 / 23 | 21 / 16 | +| no-retention-direct-after-fetch | 13 / 19 | 13 / 16 | 13 / 14 | +| retained-direct-before-returned-key | 7 / 6 | 7 / 6 | 7 / 12 | +| overlapping-saved-direct-key | 23 / 16 | 23 / 25 | 23 / 20 | +| distinct-saved-direct-key | 29 / 36 | 29 / 27 | 29 / 32 | +| returned-value-equals-saved-after-direct-change | 2 / 2 | 2 / 3 | 2 / 3 | + +Immediate reach, fixed seed `1468`: + +| Declared state | Node fixed/fresh | Chromium fixed/fresh | +| --- | ---: | ---: | +| initialData | 20 / 23 | 20 / 16 | +| cache | 28 / 25 | 28 / 32 | +| raw | 18 / 25 | 18 / 25 | +| derived | 30 / 23 | 30 / 23 | +| empty | 13 / 14 | 13 / 10 | +| nonempty | 35 / 34 | 35 / 38 | +| function-initializer | 25 / 25 | 25 / 31 | +| cache-precedence | 28 / 25 | 28 / 32 | + +Scoped strict TypeScript and ESLint checks passed on the receiving models, +drivers, wrappers, host and host fidelity fixture. +The normal Node run also passed its configured typecheck collector. + +## Static receiving host and calibration cost + +An earlier full Chromium attempt completed its generated campaigns but then +received a Vite reload after a neighboring agent edited an imported startup +helper. That run was interrupted and is excluded from GREEN evidence. The +receiving server now disables HMR and sets `server.watch = null` in a resolved +config hook. An inline null is insufficient because Vite's file-config merge +discards it. A temporary-file browser fidelity fixture positively loads v1, +checks the actual watcher inventory is empty, edits an imported file to v2, +and checks the loaded page still has v1 and its original identity. This proves +stability after the page has loaded; it is not universal bootstrap-byte +immutability. An immutable watch-enabled fixture control fails the watcher +assertion. The final focused clean run passed one selected test (2.78s); the +other three tests in that ordinary file were outside that selector. Both the +clean fixture and its control ran without swapping a live production module. The host also accepts optional Vite source plugins for +immutable receiving controls; its default is empty, and its isolation plugin +runs last. After that seam was added, the same focused static fixture passed +again (1.97s), with helper ESLint and scoped TypeScript checks clean. + +The fixed input identities now receive the same independently authored +reach obligations as their history predicates. This lets a missing-state +checker calibration fail on an early eligible fixed history instead of +replaying the entire fixed corpus first. Independent review checked each +composite predicate against the real receipt-cut recorder. No fixed input, +generated budget, constructor, or production observation was removed. The +supplemental completion controls use fresh clones to isolate their intended +comparison from fixed-identity obligations. + +## Mutants and calibration + +All four immutable source controls were rejected at their intended assertions +on controlled persistence, Node SQLite and actual Chromium/OPFS. Every mutant +run guarded the fixed source SHA and exact span, recorded its transformed SHA, +and logged that the transformed module actually executed. No live source file +was swapped, and no setup error or unapplied mutant was counted as a kill. +All four Node and all four Chromium controls were rerun after the +fixed-input obligation and watch/HMR isolation changes. A sandboxed rerun first failed +to bind its localhost fixture (`listen EPERM`); those outputs are preserved as +`*.listen-denied` and excluded. The authorized local-browser rerun reached all +four intended assertions with source-application logs. + +| Control | Witness | Rejection | +| --- | --- | --- | +| Restore the old retained-baseline-only oldItem expression | seed563/path `15:0:0:0:0:0` | `first` / `direct row value`, expected x1 versus x0 | +| M563: replace the baseline helper's `unownedDirectWritePositions.has(rowKey)` with `undefined` | shared structural replay 12 | `first` / `direct row ownership`, expected no owner versus source owner | +| M1468: change the persistence conjunction in needsPersistedBaseline from `&&` to `||` | seed1468/path `0:0:0` | `creation-return` / `immediate eager materialization`, expected ready versus loading | +| M1469: change its eager-mode conjunction from `&&` to `||` | same seed/path | same readiness assertion | + +Initial immutable runs also tested the instrument. The first M563 attempt +stopped at a mutation-span uniqueness guard because the same expression occurs +elsewhere; its rerun anchors the original operator at offset 57583 under the +fixed source SHA. The first M1468/M1469 attempts exited green without a +source-applied log because module caching bypassed the transform. These are +unapplied controls, not survivors or kills. Setting `fsModuleCache:false` in +the temporary immutable runner and rerunning produced the intended assertion +failures with source-application logs. Their initial outputs are retained as +`*.initial-unapplied` beside the final control logs. The temporary calibration +progress run was interrupted after establishing progress and is not claimed +as another passing campaign. + +The repaired-source transformed SHA256 values are: + +- Old value defect: `81767f463428171dfa9746d60c4bf67978c7f78732899a995425dd25adb29c3b`. +- M563: `cabaa8d8b47a98f5d141f6bda6c150f1c4b757f0378790bb52bce430bf5e2e4e`. +- M1468: `355db31625b3051173346d32fd3d6444b9a93afedad41de56267e33d18f99abf`. +- M1469: `93a3e0b1199b190694e72962985bc7d48abd929f4162521b8df7ca6bf1d0178c`. + +Generated-only reach omission was separately checked before the production +repair. Direct seed563/path `12` and immediate seed1468/path `8:8` select +constructed outputs absent from all seeded constructions. Their clean exact +replays pass; suppressing only their required reach fails `constructed states` +/ `constructed reach`. The direct witness runs on both controlled and Node +SQLite receivers. The framework's seeded constructions alone therefore cannot +satisfy the factory's fresh-output obligation ledger. + +Three finite existing-history controls also check empty/truncated returns, +product failure before missing completion, simultaneous product/page-error +diagnostics and cleanup/page-error transport. They pass without adding another +product model or an unregistered generated campaign. + +## Execution limits + +This extension covers one successful first eager response, one saved row and +one accepted insert/upsert, bounded numeric values and raw/derived envelopes. +It does not claim arbitrary optimistic histories, peers, concurrent writers, +failed I/O, arbitrary source protocols, or cross-client cache provenance. Those +remain with their primary owners. The QueryClient and persistence hosts are +real; source responses are controlled finite inputs, not a claim about a +particular remote service. + +Normal pnpm commands first attempted its automatic dependency preflight and +failed before any test. The final normal runs used the pnpm package commands above with dependency +preflight disabled. No dependency recovery or lockfile edit was performed. +The existing local install-workaround hashes remained +`ef167cca76c028d75a96818c987373ef213c8fff152ae4ff5aa43da3adea79b5` +(pnpm-workspace.yaml) and +`597c414dc296075582b34c76d10e71caac19d57a33b81a97f74f79ec10b225e1` +(pnpm-lock.yaml). Set `pnpm_config_verify_deps_before_run=false` for package +wrapper smoke commands in this local installation. diff --git a/docs/contributing/oracle-reviews/2026-11-10-query-sync-write-protocol.json b/docs/contributing/oracle-reviews/2026-11-10-query-sync-write-protocol.json new file mode 100644 index 0000000000..697e478d9e --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-query-sync-write-protocol.json @@ -0,0 +1,377 @@ +{ + "date": "2026-11-10", + "head": "0e86c1fdae85520aff142f141f71ccf46a8ee8bd", + "workingFileSha256": { + "packages/query-db-collection/src/query.ts": "1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b", + "packages/query-db-collection/tests/persisted-direct-ownership-oracle.ts": "3f4441cbda41c5f8b71b957c7cd3fd483bd949ae1bcb86213be4bc97b4063d90", + "packages/query-db-collection/tests/persisted-direct-ownership-driver.ts": "41165f9a7c6e10814a673427ccbed8ecd981f4d551843a098688ad8737558ed6", + "packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts": "24eb442d095112ecc40b8548392d247a7e65eb20196aabbb0e219a6cd4497e4f", + "packages/browser-db-sqlite-persistence/e2e/query-persisted-direct-ownership-oracle.browser.test.ts": "c48a2ef8bf027048cbd11f87a6d3fab89e6c5742b20ac027566e1b5ca69ea702", + "packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts": "9f3a993e200c54a013840102bc49baecf69bb8f2d89df069e9bfb2dec1e35ea7", + "packages/browser-db-sqlite-persistence/tests/adapter-chromium-host.ts": "e0e1e06b91f388cff7c87bdc5883ec486c3ec88a6cc551a2e5f15ea27b012f5f" + }, + "originalAudit": "docs/contributing/oracle-reviews/2026-11-10-query-changed-callback-gap-audit.json", + "mutant": { + "id": "query.ts:M1334", + "source": "packages/query-db-collection/src/query.ts", + "function": "applySuccessfulResult", + "line": 2756, + "position": 97728, + "operator": "field-drop", + "original": "type: `update`", + "replacement": "", + "selectionGlobalKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `update` , value : newItem } )", + "actualReportKey": "src/query.ts#applySuccessfulResult:field-drop:write ( { type : `update` , value : newItem } )", + "runnerRemovalIncludingSeparator": "type: `update`, ", + "sourceSha256": "1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b", + "transformedSha256": "8a1ffc1dbc0f37839b81a9faab2b21e0f253bc8accbc649b92117de5934216e0" + }, + "immutableRunner": "import { createHash } from 'node:crypto'\nimport { readFileSync, writeFileSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { defineConfig, mergeConfig } from 'vitest/config'\nimport nodeBase from '/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/vitest.config'\nimport browserBase from '/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config'\nconst repo = '/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367'\nconst evidence = '/private/tmp/tanstack-db-2394-sync-write-protocol'\nconst target = resolve(repo, 'packages/query-db-collection/src/query.ts')\nconst source = readFileSync(target, 'utf8')\nconst sha = (text: string) => createHash('sha256').update(text).digest('hex')\nif (sha(source) !== '1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b') throw new Error('Final Query source changed')\nconst mutant = process.env.QUERY_FINAL_MUTANT ?? 'clean'\nlet position = -1\nlet original = ''\nlet replacement = ''\nif (mutant === 'query.ts:M1334') {\n original = 'type: `update`, '\n replacement = ''\n position = 97728\n} else if (mutant === 'original-value-defect') {\n original = 'persistedBaseline?.get(key)?.value ?? currentSyncedItems.get(key)'\n replacement = 'shouldUsePersistedBaseline ? persistedBaseline.get(key)?.value : currentSyncedItems.get(key)'\n position = source.indexOf(original)\n} else if (mutant === 'query.ts:M563') {\n original = 'unownedDirectWritePositions.has(rowKey)'\n replacement = 'undefined'\n position = 57583\n} else if (mutant === 'query.ts:M1468' || mutant === 'query.ts:M1469') {\n const anchor = 'const needsPersistedBaseline = (queryHash: string) =>'\n const start = source.indexOf(anchor)\n const end = source.indexOf('const reconcileSuccessfulResult', start)\n if (start < 0 || end < 0) throw new Error('Baseline predicate moved')\n const conjunctions = [...source.slice(start, end).matchAll(/&&/g)]\n if (conjunctions.length !== 2) throw new Error('Predicate shape changed')\n position = start + conjunctions[mutant.endsWith('1469') ? 0 : 1].index!\n original = '&&'\n replacement = '||'\n} else if (mutant !== 'clean') throw new Error('Unknown immutable mutant ' + mutant)\nif (mutant !== 'clean' && (position < 0 || source.slice(position, position + original.length) !== original)) throw new Error('Immutable source span failed')\nif (mutant === 'original-value-defect' && source.indexOf(original, position + 1) >= 0) throw new Error('Mutation span is not unique')\nconst transformed = mutant === 'clean' ? source : source.slice(0, position) + replacement + source.slice(position + original.length)\nconst browser = process.env.QUERY_FINAL_BROWSER === '1'\nconst metadata = { mutant, source: sha(source), transformed: sha(transformed), position, line: source.slice(0, Math.max(0, position)).split('\\n').length, original, replacement, browser }\nconsole.info('FINAL_QUERY_SOURCE', JSON.stringify(metadata))\nwriteFileSync(resolve(evidence, 'source-' + mutant.replaceAll(':', '-') + '.json'), JSON.stringify(metadata, null, 2))\nexport default mergeConfig(browser ? browserBase : nodeBase, defineConfig({\n root: resolve(repo, browser ? 'packages/browser-db-sqlite-persistence' : 'packages/query-db-collection'),\n cacheDir: resolve(evidence, '.cache-' + (browser ? 'browser-' : 'node-') + mutant.replaceAll(':', '-')),\n plugins: [{\n name: 'final-query-immutable-evidence',\n enforce: 'pre',\n transform(code, id) {\n if (id.split('?')[0] === target) {\n console.info('FINAL_QUERY_APPLIED', mutant)\n return { code: transformed, map: null }\n }\n if (id.split('?')[0] === resolve(repo, 'packages/browser-db-sqlite-persistence/tests/adapter-chromium-host.ts')) {\n const anchor = 'const server = await createServer({'\n if (!code.includes(anchor)) throw new Error('Chromium host seam moved')\n const injected = `cacheDir:${JSON.stringify(resolve(evidence, '.vite-' + mutant.replaceAll(':', '-')))},`\n const plugin = `{name:'final-query-browser-source',enforce:'pre',transform(code,id){if(id.split('?')[0]===${JSON.stringify(target)}){console.info('FINAL_QUERY_BROWSER_APPLIED',${JSON.stringify(mutant)});return {code:${JSON.stringify(transformed)},map:null}}}},`\n if (!code.includes('plugins: [')) throw new Error('Static receiving host plugin seam moved')\n return { code: code.replace('plugins: [', 'plugins: [' + plugin).replace('optimizeDeps: {', 'optimizeDeps: {force:true,').replace(anchor, anchor + injected), map: null }\n }\n },\n }],\n test: {\n coverage: { enabled: false },\n fileParallelism: false,\n fsModuleCache: false,\n include: browser ? ['e2e/query-persisted-direct-ownership-oracle.browser.test.ts', 'e2e/query-immediate-cache-oracle.browser.test.ts'] : ['tests/persisted-direct-ownership-oracle.test.ts', 'tests/immediate-eager-materialization-oracle.test.ts'],\n },\n}))\n", + "executions": [ + { + "log": "node-clean-final.log", + "cwd": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection", + "environment": {}, + "command": [ + "pnpm", + "--config.verifyDepsBeforeRun=false", + "exec", + "vitest", + "run", + "--config", + "/private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts", + "--maxWorkers=2", + "--typecheck.enabled=false", + "tests/persisted-direct-ownership-oracle.test.ts" + ], + "exitCode": 0, + "classification": "green", + "reach": [ + { + "campaign": "query.persisted-direct-ownership fixed seed 563", + "counts": { + "after-fetch-start": 20, + "before-fetch": 32, + "derived": 26, + "distinct-saved-direct-key": 29, + "fetched-row-write": 31, + "no-retention": 27, + "no-retention-direct-after-fetch": 13, + "overlapping-saved-direct-key": 23, + "raw": 26, + "retained": 25, + "retained-direct-before-returned-key": 7, + "returned-value-equals-saved-after-direct-change": 2, + "source-includes-direct": 31, + "source-omits-direct": 21 + } + }, + { + "campaign": "query.persisted-direct-ownership random seed", + "counts": { + "after-fetch-start": 24, + "before-fetch": 28, + "derived": 28, + "distinct-saved-direct-key": 30, + "fetched-row-write": 27, + "no-retention": 26, + "no-retention-direct-after-fetch": 14, + "overlapping-saved-direct-key": 22, + "raw": 24, + "retained": 26, + "retained-direct-before-returned-key": 5, + "returned-value-equals-saved-after-direct-change": 2, + "source-includes-direct": 28, + "source-omits-direct": 24 + } + }, + { + "campaign": "query.persisted-direct-ownership fixed seed 563", + "counts": { + "after-fetch-start": 20, + "before-fetch": 32, + "derived": 26, + "distinct-saved-direct-key": 29, + "fetched-row-write": 31, + "no-retention": 27, + "no-retention-direct-after-fetch": 13, + "overlapping-saved-direct-key": 23, + "raw": 26, + "retained": 25, + "retained-direct-before-returned-key": 7, + "returned-value-equals-saved-after-direct-change": 2, + "source-includes-direct": 31, + "source-omits-direct": 21 + } + }, + { + "campaign": "query.persisted-direct-ownership random seed", + "counts": { + "after-fetch-start": 20, + "before-fetch": 32, + "derived": 23, + "distinct-saved-direct-key": 36, + "fetched-row-write": 22, + "no-retention": 24, + "no-retention-direct-after-fetch": 10, + "overlapping-saved-direct-key": 16, + "raw": 29, + "retained": 28, + "retained-direct-before-returned-key": 6, + "returned-value-equals-saved-after-direct-change": 4, + "source-includes-direct": 28, + "source-omits-direct": 24 + } + } + ], + "stdoutAndStderr": "FINAL_QUERY_SOURCE {\"mutant\":\"clean\",\"source\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"transformed\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"position\":-1,\"line\":1,\"original\":\"\",\"replacement\":\"\",\"browser\":false}\n(!) Your Vite config uses features that are unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite:\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/vitest.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:5:22). Add the file extension\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:6:25). Add the file extension\nSet `VITE_CONFIG_NATIVE_IGNORE_WARNING=true` to suppress this warning.\n\n RUN v5.1.0 /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection\n\nFINAL_QUERY_APPLIED clean\nquery.persisted-direct-ownership fixed seed 563 reach {\"after-fetch-start\":20,\"before-fetch\":32,\"derived\":26,\"distinct-saved-direct-key\":29,\"fetched-row-write\":31,\"no-retention\":27,\"no-retention-direct-after-fetch\":13,\"overlapping-saved-direct-key\":23,\"raw\":26,\"retained\":25,\"retained-direct-before-returned-key\":7,\"returned-value-equals-saved-after-direct-change\":2,\"source-includes-direct\":31,\"source-omits-direct\":21}\nquery.persisted-direct-ownership random seed reach {\"after-fetch-start\":24,\"before-fetch\":28,\"derived\":28,\"distinct-saved-direct-key\":30,\"fetched-row-write\":27,\"no-retention\":26,\"no-retention-direct-after-fetch\":14,\"overlapping-saved-direct-key\":22,\"raw\":24,\"retained\":26,\"retained-direct-before-returned-key\":5,\"returned-value-equals-saved-after-direct-change\":2,\"source-includes-direct\":28,\"source-omits-direct\":24}\nquery.persisted-direct-ownership fixed seed 563 reach {\"after-fetch-start\":20,\"before-fetch\":32,\"derived\":26,\"distinct-saved-direct-key\":29,\"fetched-row-write\":31,\"no-retention\":27,\"no-retention-direct-after-fetch\":13,\"overlapping-saved-direct-key\":23,\"raw\":26,\"retained\":25,\"retained-direct-before-returned-key\":7,\"returned-value-equals-saved-after-direct-change\":2,\"source-includes-direct\":31,\"source-omits-direct\":21}\nquery.persisted-direct-ownership random seed reach {\"after-fetch-start\":20,\"before-fetch\":32,\"derived\":23,\"distinct-saved-direct-key\":36,\"fetched-row-write\":22,\"no-retention\":24,\"no-retention-direct-after-fetch\":10,\"overlapping-saved-direct-key\":16,\"raw\":29,\"retained\":28,\"retained-direct-before-returned-key\":6,\"returned-value-equals-saved-after-direct-change\":4,\"source-includes-direct\":28,\"source-omits-direct\":24}\n\n Test Files 1 passed (1)\n Tests 244 passed (244)\n Start at 15:50:01\n Duration 5.64s (tests 83%, transform 14%, import 3%)\n\n" + }, + { + "log": "node-M1334-final-red.log", + "cwd": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection", + "environment": { + "QUERY_FINAL_MUTANT": "query.ts:M1334", + "TANSTACK_DB_ORACLE_PROPERTY": "query.persisted-direct-ownership", + "TANSTACK_DB_ORACLE_SEED": "563", + "TANSTACK_DB_ORACLE_PATH": "15:0:0:0:0:0" + }, + "command": [ + "pnpm", + "--config.verifyDepsBeforeRun=false", + "exec", + "vitest", + "run", + "--config", + "/private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts", + "--maxWorkers=2", + "--typecheck.enabled=false", + "tests/persisted-direct-ownership-oracle.test.ts" + ], + "exitCode": 1, + "classification": "assertion failure at declared sync-write operation", + "reach": [], + "stdoutAndStderr": "FINAL_QUERY_SOURCE {\"mutant\":\"query.ts:M1334\",\"source\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"transformed\":\"8a1ffc1dbc0f37839b81a9faab2b21e0f253bc8accbc649b92117de5934216e0\",\"position\":97728,\"line\":2756,\"original\":\"type: `update`, \",\"replacement\":\"\",\"browser\":false}\n(!) Your Vite config uses features that are unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite:\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/vitest.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:5:22). Add the file extension\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:6:25). Add the file extension\nSet `VITE_CONFIG_NATIVE_IGNORE_WARNING=true` to suppress this warning.\n\n RUN v5.1.0 /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection\n\nFINAL_QUERY_APPLIED query.ts:M1334\nstdout | tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > random seed\nORACLE_SEED {\"property\":\"query.persisted-direct-ownership\",\"seed\":563}\n\nstdout | tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > random seed\nORACLE_SEED {\"property\":\"query.persisted-direct-ownership\",\"seed\":563}\n\n \u276f tests/persisted-direct-ownership-oracle.test.ts (244 tests | 2 failed | 242 skipped) 32ms\n \u276f controlled (122)\n \u276f query.persisted-direct-ownership (122)\n \u00d7 random seed 21ms\n \u276f node-sqlite (122)\n \u276f query.persisted-direct-ownership (122)\n \u00d7 random seed 10ms\n\n\u23af\u23af\u23af\u23af\u23af\u23af\u23af Failed Tests 2 \u23af\u23af\u23af\u23af\u23af\u23af\u23af\n\n FAIL tests/persisted-direct-ownership-oracle.test.ts > controlled > query.persisted-direct-ownership > random seed\n FAIL tests/persisted-direct-ownership-oracle.test.ts > node-sqlite > query.persisted-direct-ownership > random seed\nError: Property failed after 1 tests\n{ seed: 563, path: \"15:0:0:0:0:0\", endOnFailure: true }\nCounterexample: [{\"shape\":\"raw\",\"retained\":true,\"write\":\"before-fetch\",\"operation\":\"insert\",\"includesDirect\":true,\"keepsSaved\":false,\"directValue\":0,\"sourceValue\":1,\"savedValue\":0,\"overlapsSaved\":false}]\nShrunk 0 time(s)\n\nHint: Enable verbose mode in order to have the list of all failing values encountered during the run\n \u276f buildError ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:2469:16\n \u276f asyncThrowIfFailed ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:2481:8\n\nCaused by: OracleViolation: query.persisted-direct-ownership violated at checkpoint \"sync-write\" (law: declared sync-write operation).\nLaw: A later complete physical fetch replaces and claims a direct row; an older result preserves it without claiming source ownership. Query adapter sync writes declare an insert, update, or delete operation.\nDifference: SyncConfig.write requires insert, update, or delete; observed undefined\nHistory: {\"shape\":\"raw\",\"retained\":true,\"write\":\"before-fetch\",\"operation\":\"insert\",\"includesDirect\":true,\"keepsSaved\":false,\"directValue\":0,\"sourceValue\":1,\"savedValue\":0,\"overlapsSaved\":false}\nAuthority: docs/collections/query-collection.md: Understanding the Data Stores item1 and Direct Writes and Query Sync; primary ownership direct-order/direct-row-lifetime laws; docs/reference/interfaces/SyncConfig.md and ChangeMessage.md, ChangeMessageOrDeleteKeyMessage and OperationType API types.\nKnown limits: First eager result, one saved row and one same-run insert/upsert, numeric values, successful I/O. Public/durable cuts follow applied receipts; cleanup is diagnostic. No arbitrary mutations, peers, concurrent writers or eager unload claim.\nRerun: TANSTACK_DB_ORACLE_PROPERTY=query.persisted-direct-ownership with the seed and path that fast-check reports\n \u276f failure ../../node_modules/.pnpm/@tanstack+oracles@https+++pkg.pr.new+TanStack+oracles+@tanstack+oracles@e1fb504_fast-ch_339bc51621ec5fc7d94a6c5beca357fd/node_modules/@tanstack/oracles/dist/esm/oracle-property.js:707:17\n \u276f ../../node_modules/.pnpm/@tanstack+oracles@https+++pkg.pr.new+TanStack+oracles+@tanstack+oracles@e1fb504_fast-ch_339bc51621ec5fc7d94a6c5beca357fd/node_modules/@tanstack/oracles/dist/esm/oracle-property.js:806:13\n \u276f AsyncProperty.run ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:1217:19\n \u276f asyncPropertyExecution ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:2564:14\n \u276f asyncRunIt ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:2557:15\n \u276f runCampaign ../../node_modules/.pnpm/@tanstack+oracles@https+++pkg.pr.new+TanStack+oracles+@tanstack+oracles@e1fb504_fast-ch_339bc51621ec5fc7d94a6c5beca357fd/node_modules/@tanstack/oracles/dist/esm/oracle-property.js:836:5\n\n\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af[1/2]\u23af\n\n\n Test Files 1 failed (1)\n Tests 2 failed | 242 skipped (244)\n Start at 15:50:29\n Duration 1.17s (transform 82%, import 15%, tests 3%)\n\n" + }, + { + "log": "node-clean-final-replay.log", + "cwd": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection", + "environment": { + "TANSTACK_DB_ORACLE_PROPERTY": "query.persisted-direct-ownership", + "TANSTACK_DB_ORACLE_SEED": "563", + "TANSTACK_DB_ORACLE_PATH": "15:0:0:0:0:0" + }, + "command": [ + "pnpm", + "--config.verifyDepsBeforeRun=false", + "exec", + "vitest", + "run", + "--config", + "/private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts", + "--maxWorkers=2", + "--typecheck.enabled=false", + "tests/persisted-direct-ownership-oracle.test.ts" + ], + "exitCode": 0, + "classification": "green", + "reach": [ + { + "campaign": "query.persisted-direct-ownership random seed", + "counts": { + "after-fetch-start": 0, + "before-fetch": 3, + "derived": 0, + "distinct-saved-direct-key": 3, + "fetched-row-write": 3, + "no-retention": 0, + "no-retention-direct-after-fetch": 0, + "overlapping-saved-direct-key": 0, + "raw": 3, + "retained": 3, + "retained-direct-before-returned-key": 3, + "returned-value-equals-saved-after-direct-change": 0, + "source-includes-direct": 3, + "source-omits-direct": 0 + } + }, + { + "campaign": "query.persisted-direct-ownership random seed", + "counts": { + "after-fetch-start": 0, + "before-fetch": 3, + "derived": 0, + "distinct-saved-direct-key": 3, + "fetched-row-write": 3, + "no-retention": 0, + "no-retention-direct-after-fetch": 0, + "overlapping-saved-direct-key": 0, + "raw": 3, + "retained": 3, + "retained-direct-before-returned-key": 3, + "returned-value-equals-saved-after-direct-change": 0, + "source-includes-direct": 3, + "source-omits-direct": 0 + } + } + ], + "stdoutAndStderr": "FINAL_QUERY_SOURCE {\"mutant\":\"clean\",\"source\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"transformed\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"position\":-1,\"line\":1,\"original\":\"\",\"replacement\":\"\",\"browser\":false}\n(!) Your Vite config uses features that are unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite:\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/vitest.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:5:22). Add the file extension\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:6:25). Add the file extension\nSet `VITE_CONFIG_NATIVE_IGNORE_WARNING=true` to suppress this warning.\n\n RUN v5.1.0 /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection\n\nFINAL_QUERY_APPLIED clean\nquery.persisted-direct-ownership random seed reach {\"after-fetch-start\":0,\"before-fetch\":3,\"derived\":0,\"distinct-saved-direct-key\":3,\"fetched-row-write\":3,\"no-retention\":0,\"no-retention-direct-after-fetch\":0,\"overlapping-saved-direct-key\":0,\"raw\":3,\"retained\":3,\"retained-direct-before-returned-key\":3,\"returned-value-equals-saved-after-direct-change\":0,\"source-includes-direct\":3,\"source-omits-direct\":0}\nquery.persisted-direct-ownership random seed reach {\"after-fetch-start\":0,\"before-fetch\":3,\"derived\":0,\"distinct-saved-direct-key\":3,\"fetched-row-write\":3,\"no-retention\":0,\"no-retention-direct-after-fetch\":0,\"overlapping-saved-direct-key\":0,\"raw\":3,\"retained\":3,\"retained-direct-before-returned-key\":3,\"returned-value-equals-saved-after-direct-change\":0,\"source-includes-direct\":3,\"source-omits-direct\":0}\n\n Test Files 1 passed (1)\n Tests 2 passed | 242 skipped (244)\n Start at 15:50:55\n Duration 1.49s (transform 81%, import 14%, tests 4%)\n\n" + }, + { + "log": "browser-clean-final.log", + "cwd": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence", + "environment": { + "QUERY_FINAL_BROWSER": "1" + }, + "command": [ + "pnpm", + "--config.verifyDepsBeforeRun=false", + "exec", + "vitest", + "run", + "--config", + "/private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts", + "--maxWorkers=2", + "e2e/query-persisted-direct-ownership-oracle.browser.test.ts" + ], + "exitCode": 0, + "classification": "green", + "reach": [ + { + "campaign": "query.persisted-direct-ownership.chromium-opfs fixed seed 563", + "counts": { + "after-fetch-start": 20, + "before-fetch": 32, + "derived": 26, + "distinct-saved-direct-key": 29, + "fetched-row-write": 31, + "no-retention": 27, + "no-retention-direct-after-fetch": 13, + "overlapping-saved-direct-key": 23, + "raw": 26, + "retained": 25, + "retained-direct-before-returned-key": 7, + "returned-value-equals-saved-after-direct-change": 2, + "source-includes-direct": 31, + "source-omits-direct": 21 + } + }, + { + "campaign": "query.persisted-direct-ownership.chromium-opfs random seed", + "counts": { + "after-fetch-start": 22, + "before-fetch": 30, + "derived": 30, + "distinct-saved-direct-key": 37, + "fetched-row-write": 20, + "no-retention": 30, + "no-retention-direct-after-fetch": 16, + "overlapping-saved-direct-key": 15, + "raw": 22, + "retained": 22, + "retained-direct-before-returned-key": 5, + "returned-value-equals-saved-after-direct-change": 3, + "source-includes-direct": 28, + "source-omits-direct": 24 + } + } + ], + "stdoutAndStderr": "FINAL_QUERY_SOURCE {\"mutant\":\"clean\",\"source\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"transformed\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"position\":-1,\"line\":1,\"original\":\"\",\"replacement\":\"\",\"browser\":true}\n(!) Your Vite config uses features that are unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite:\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/vitest.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:5:22). Add the file extension\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:6:25). Add the file extension\nSet `VITE_CONFIG_NATIVE_IGNORE_WARNING=true` to suppress this warning.\n\n RUN v5.1.0 /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence\n\nquery.persisted-direct-ownership.chromium-opfs fixed seed 563 reach {\"after-fetch-start\":20,\"before-fetch\":32,\"derived\":26,\"distinct-saved-direct-key\":29,\"fetched-row-write\":31,\"no-retention\":27,\"no-retention-direct-after-fetch\":13,\"overlapping-saved-direct-key\":23,\"raw\":26,\"retained\":25,\"retained-direct-before-returned-key\":7,\"returned-value-equals-saved-after-direct-change\":2,\"source-includes-direct\":31,\"source-omits-direct\":21}\nquery.persisted-direct-ownership.chromium-opfs random seed reach {\"after-fetch-start\":22,\"before-fetch\":30,\"derived\":30,\"distinct-saved-direct-key\":37,\"fetched-row-write\":20,\"no-retention\":30,\"no-retention-direct-after-fetch\":16,\"overlapping-saved-direct-key\":15,\"raw\":22,\"retained\":22,\"retained-direct-before-returned-key\":5,\"returned-value-equals-saved-after-direct-change\":3,\"source-includes-direct\":28,\"source-omits-direct\":24}\n\n Test Files 1 passed (1)\n Tests 122 passed (122)\n Start at 15:50:21\n Duration 146.67s (tests 99%)\n\n" + }, + { + "log": "browser-M1334-final-red.log", + "cwd": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence", + "environment": { + "QUERY_FINAL_BROWSER": "1", + "QUERY_FINAL_MUTANT": "query.ts:M1334", + "TANSTACK_DB_ORACLE_PROPERTY": "query.persisted-direct-ownership.chromium-opfs", + "TANSTACK_DB_ORACLE_SEED": "563", + "TANSTACK_DB_ORACLE_PATH": "15:0:0:0:0:0" + }, + "command": [ + "pnpm", + "--config.verifyDepsBeforeRun=false", + "exec", + "vitest", + "run", + "--config", + "/private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts", + "--maxWorkers=2", + "e2e/query-persisted-direct-ownership-oracle.browser.test.ts" + ], + "exitCode": 1, + "classification": "assertion failure at declared sync-write operation", + "reach": [], + "stdoutAndStderr": "FINAL_QUERY_SOURCE {\"mutant\":\"query.ts:M1334\",\"source\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"transformed\":\"8a1ffc1dbc0f37839b81a9faab2b21e0f253bc8accbc649b92117de5934216e0\",\"position\":97728,\"line\":2756,\"original\":\"type: `update`, \",\"replacement\":\"\",\"browser\":true}\n(!) Your Vite config uses features that are unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite:\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/vitest.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:5:22). Add the file extension\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:6:25). Add the file extension\nSet `VITE_CONFIG_NATIVE_IGNORE_WARNING=true` to suppress this warning.\n\n RUN v5.1.0 /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence\n\nstdout | e2e/query-persisted-direct-ownership-oracle.browser.test.ts > query.persisted-direct-ownership.chromium-opfs > random seed\nORACLE_SEED {\"property\":\"query.persisted-direct-ownership.chromium-opfs\",\"seed\":563}\n\nstdout | e2e/query-persisted-direct-ownership-oracle.browser.test.ts\n3:53:17 PM [vite] (client) Forced re-optimization of dependencies\n\nstdout | e2e/query-persisted-direct-ownership-oracle.browser.test.ts\nFINAL_QUERY_BROWSER_APPLIED query.ts:M1334\n\nstdout | e2e/query-persisted-direct-ownership-oracle.browser.test.ts\n3:53:18 PM [vite] (client) dependencies optimized: @journeyapps/wa-sqlite, @journeyapps/wa-sqlite/dist/wa-sqlite.mjs, @journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js\n\n \u276f |adapter-oracles-chromium| e2e/query-persisted-direct-ownership-oracle.browser.test.ts (122 tests | 1 failed | 121 skipped) 1218ms\n \u276f query.persisted-direct-ownership.chromium-opfs (122)\n \u00d7 random seed 349ms\n\n\u23af\u23af\u23af\u23af\u23af\u23af\u23af Failed Tests 1 \u23af\u23af\u23af\u23af\u23af\u23af\u23af\n\n FAIL |adapter-oracles-chromium| e2e/query-persisted-direct-ownership-oracle.browser.test.ts > query.persisted-direct-ownership.chromium-opfs > random seed\nError: Property failed after 1 tests\n{ seed: 563, path: \"15:0:0:0:0:0\", endOnFailure: true }\nCounterexample: [{\"shape\":\"raw\",\"retained\":true,\"write\":\"before-fetch\",\"operation\":\"insert\",\"includesDirect\":true,\"keepsSaved\":false,\"directValue\":0,\"sourceValue\":1,\"savedValue\":0,\"overlapsSaved\":false}]\nShrunk 0 time(s)\n\nHint: Enable verbose mode in order to have the list of all failing values encountered during the run\n \u276f buildError ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:2469:16\n \u276f asyncThrowIfFailed ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:2481:8\n\nCaused by: OracleViolation: query.persisted-direct-ownership.chromium-opfs violated at checkpoint \"sync-write\" (law: declared sync-write operation).\nLaw: A later complete physical fetch replaces and claims a direct row; an older result preserves it without claiming source ownership. Query adapter sync writes declare an insert, update, or delete operation.\nDifference: SyncConfig.write requires insert, update, or delete; observed undefined\nHistory: {\"shape\":\"raw\",\"retained\":true,\"write\":\"before-fetch\",\"operation\":\"insert\",\"includesDirect\":true,\"keepsSaved\":false,\"directValue\":0,\"sourceValue\":1,\"savedValue\":0,\"overlapsSaved\":false}\nAuthority: docs/collections/query-collection.md: Understanding the Data Stores item1 and Direct Writes and Query Sync; primary ownership direct-order/direct-row-lifetime laws; docs/reference/interfaces/SyncConfig.md and ChangeMessage.md, ChangeMessageOrDeleteKeyMessage and OperationType API types.\nKnown limits: First eager result, one saved row and one same-run insert/upsert, numeric values, successful I/O. Public/durable cuts follow applied receipts; cleanup is diagnostic. No arbitrary mutations, peers, concurrent writers or eager unload claim.\nRerun: TANSTACK_DB_ORACLE_PROPERTY=query.persisted-direct-ownership.chromium-opfs with the seed and path that fast-check reports\n \u276f failure ../../node_modules/.pnpm/@tanstack+oracles@https+++pkg.pr.new+TanStack+oracles+@tanstack+oracles@e1fb504_fast-ch_339bc51621ec5fc7d94a6c5beca357fd/node_modules/@tanstack/oracles/dist/esm/oracle-property.js:707:17\n \u276f ../../node_modules/.pnpm/@tanstack+oracles@https+++pkg.pr.new+TanStack+oracles+@tanstack+oracles@e1fb504_fast-ch_339bc51621ec5fc7d94a6c5beca357fd/node_modules/@tanstack/oracles/dist/esm/oracle-property.js:806:13\n \u276f AsyncProperty.run ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:1217:19\n \u276f asyncPropertyExecution ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:2564:14\n \u276f asyncRunIt ../../node_modules/.pnpm/fast-check@4.10.2/node_modules/fast-check/lib/fast-check.js:2557:15\n \u276f runCampaign ../../node_modules/.pnpm/@tanstack+oracles@https+++pkg.pr.new+TanStack+oracles+@tanstack+oracles@e1fb504_fast-ch_339bc51621ec5fc7d94a6c5beca357fd/node_modules/@tanstack/oracles/dist/esm/oracle-property.js:836:5\n\n\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af\u23af[1/1]\u23af\n\n\n Test Files 1 failed (1)\n Tests 1 failed | 121 skipped (122)\n Start at 15:53:15\n Duration 2.33s (tests 54%, import 29%, environment 13%, transform 3%)\n\n" + }, + { + "log": "browser-clean-final-replay.log", + "cwd": "/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence", + "environment": { + "QUERY_FINAL_BROWSER": "1", + "TANSTACK_DB_ORACLE_PROPERTY": "query.persisted-direct-ownership.chromium-opfs", + "TANSTACK_DB_ORACLE_SEED": "563", + "TANSTACK_DB_ORACLE_PATH": "15:0:0:0:0:0" + }, + "command": [ + "pnpm", + "--config.verifyDepsBeforeRun=false", + "exec", + "vitest", + "run", + "--config", + "/private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts", + "--maxWorkers=2", + "e2e/query-persisted-direct-ownership-oracle.browser.test.ts" + ], + "exitCode": 0, + "classification": "green", + "reach": [ + { + "campaign": "query.persisted-direct-ownership.chromium-opfs random seed", + "counts": { + "after-fetch-start": 0, + "before-fetch": 3, + "derived": 0, + "distinct-saved-direct-key": 3, + "fetched-row-write": 3, + "no-retention": 0, + "no-retention-direct-after-fetch": 0, + "overlapping-saved-direct-key": 0, + "raw": 3, + "retained": 3, + "retained-direct-before-returned-key": 3, + "returned-value-equals-saved-after-direct-change": 0, + "source-includes-direct": 3, + "source-omits-direct": 0 + } + } + ], + "stdoutAndStderr": "FINAL_QUERY_SOURCE {\"mutant\":\"clean\",\"source\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"transformed\":\"1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b\",\"position\":-1,\"line\":1,\"original\":\"\",\"replacement\":\"\",\"browser\":true}\n(!) Your Vite config uses features that are unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite:\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/vitest.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:5:22). Add the file extension\n - import \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config\" without a file extension (../../../../../../../../private/tmp/tanstack-db-2394-sync-write-protocol/immutable.config.ts:6:25). Add the file extension\nSet `VITE_CONFIG_NATIVE_IGNORE_WARNING=true` to suppress this warning.\n\n RUN v5.1.0 /Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence\n\nquery.persisted-direct-ownership.chromium-opfs random seed reach {\"after-fetch-start\":0,\"before-fetch\":3,\"derived\":0,\"distinct-saved-direct-key\":3,\"fetched-row-write\":3,\"no-retention\":0,\"no-retention-direct-after-fetch\":0,\"overlapping-saved-direct-key\":0,\"raw\":3,\"retained\":3,\"retained-direct-before-returned-key\":3,\"returned-value-equals-saved-after-direct-change\":0,\"source-includes-direct\":3,\"source-omits-direct\":0}\n\n Test Files 1 passed (1)\n Tests 1 passed | 121 skipped (122)\n Start at 15:53:43\n Duration 3.08s (tests 61%, import 23%, environment 13%, transform 2%)\n\n" + } + ], + "staticChecks": { + "typescript": { + "exitCode": 0, + "command": [ + "pnpm", + "--config.verifyDepsBeforeRun=false", + "exec", + "tsc", + "--project", + "/private/tmp/tanstack-db-2394-query-final/tsconfig.json", + "--noEmit" + ], + "config": "{\n \"extends\": \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/tsconfig.json\",\n \"compilerOptions\": {\n \"module\": \"ESNext\",\n \"target\": \"ES2022\",\n \"types\": [\n \"node\",\n \"vitest/globals\"\n ],\n \"paths\": {\n \"@tanstack/db\": [\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/db/src\"\n ],\n \"@tanstack/db-ivm\": [\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/db-ivm/src\"\n ],\n \"@tanstack/db-sqlite-persistence-core\": [\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/db-sqlite-persistence-core/src\"\n ],\n \"@tanstack/query-core\": [\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/node_modules/@tanstack/query-core\"\n ]\n }\n },\n \"include\": [\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/e2e/query-immediate-cache-oracle.browser.test.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/e2e/query-ownership-browser-support.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/e2e/query-persisted-direct-ownership-oracle.browser.test.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/tests/immediate-eager-materialization-driver.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/tests/immediate-eager-materialization-oracle.test.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/tests/immediate-eager-materialization-oracle.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/tests/persisted-direct-ownership-driver.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/tests/persisted-direct-ownership-oracle.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/query-db-collection/tests/query-receiving-fidelity.test.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/tests/adapter-chromium-host.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/src/vite-env.d.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/src/wa-sqlite-examples.d.ts\",\n \"/Users/kyle.mathews/programs/tanstack-db/.worktrees/codex-evaluate-review-2367/packages/browser-db-sqlite-persistence/tests/trailbase-browser-fidelity.test.ts\"\n ],\n \"exclude\": []\n}\n", + "output": "" + }, + "eslint": { + "exitCode": 0, + "files": [ + "packages/query-db-collection/tests/persisted-direct-ownership-oracle.ts", + "packages/query-db-collection/tests/persisted-direct-ownership-driver.ts" + ], + "output": "" + } + }, + "limits": "Partial operation-value validation in the retained first-eager direct-write grammar across controlled storage, native SQLite and actual Chromium/OPFS. No write count/order, own-property rule, all-field validation, arbitrary mutations, failed I/O, peers or concurrent writers. Other original audit survivors remain separate." +} diff --git a/docs/contributing/oracle-reviews/2026-11-10-query-sync-write-protocol.md b/docs/contributing/oracle-reviews/2026-11-10-query-sync-write-protocol.md new file mode 100644 index 0000000000..911c6f631b --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-query-sync-write-protocol.md @@ -0,0 +1,140 @@ +# Query sync-write protocol comparison + +The final changed-callback audit's M1334 comparison gap is closed by observing +the actual Query adapter sync-write input. Production source was unchanged +through this experiment. This record supplements the original +[gap audit](2026-11-10-query-changed-callback-gap-audit.md); it does not rewrite +that audit's historical survivor result or close its other candidates. + +The working tree's base HEAD was `0e86c1fdae85520aff142f141f71ccf46a8ee8bd`. +The adjacent [JSON evidence](2026-11-10-query-sync-write-protocol.json) records +the exact source and receiving-file hashes, runner, commands, reach counts, +and complete output for the six final executions below. + +## Law and boundary + +`SyncConfig.sync` receives `write(message: ChangeMessageOrDeleteKeyMessage)`. +`ChangeMessage.type` is required and `OperationType` is `insert | update | +delete`; the delete-key form also requires `type: 'delete'`. Authority: +[SyncConfig](../../reference/interfaces/SyncConfig.md), +[ChangeMessage](../../reference/interfaces/ChangeMessage.md), +[ChangeMessageOrDeleteKeyMessage](../../reference/type-aliases/ChangeMessageOrDeleteKeyMessage.md), +and [OperationType](../../reference/type-aliases/OperationType.md). + +The comparison checks each raw `message.type` observed at the Query adapter's +actual `write` invocation, before forwarding the same message to persistence. +Only the three API operation values pass. It does not infer an operation from +the row result, normalize a missing value, or require an own property. A +receiver that happens to produce correct rows from an undeclared operation +does not make that producer input valid. + +The existing primary ownership judgment and its merge model are unchanged. +The added judgment is a small protocol predicate; it copies no production +diff or ownership machinery. It requires no number or order of writes. +Existing public/durable row comparisons remain in the same ordered trace. + +## Generated reach + +The new declared state `fetched-row-write` means that a row-bearing write for +the direct key crossed this boundary after the physical fetch returned. It +does not require a particular operation: a legal delete/insert strategy may +reach it too. The protocol comparison independently judges the operation. + +The existing retained construction guarantees a distinct saved key, an accepted +direct write before fetch, a returned direct key, and differing direct/source +values. Its fresh objects now owe this additional observed state. The per-object +ledger also enforces generated constructions, and missing-state calibrations +reject omission of each declared state. The 96 fixed inputs, 12 constructions, +40-history run budget, other builders, and receiving hosts are retained. +Fixed inputs receive no generated reach credit. + +Each column below is a full normal campaign. Fixed campaign seed is `563`. + +| Declared state | Controlled fixed/fresh | Node SQLite fixed/fresh | Chromium OPFS fixed/fresh | +| --- | ---: | ---: | ---: | +| raw | 26 / 24 | 26 / 29 | 26 / 22 | +| derived | 26 / 28 | 26 / 23 | 26 / 30 | +| retained | 25 / 26 | 25 / 28 | 25 / 22 | +| no-retention | 27 / 26 | 27 / 24 | 27 / 30 | +| before-fetch | 32 / 28 | 32 / 32 | 32 / 30 | +| after-fetch-start | 20 / 24 | 20 / 20 | 20 / 22 | +| source-includes-direct | 31 / 28 | 31 / 28 | 31 / 28 | +| source-omits-direct | 21 / 24 | 21 / 24 | 21 / 24 | +| no-retention-direct-after-fetch | 13 / 14 | 13 / 10 | 13 / 16 | +| retained-direct-before-returned-key | 7 / 5 | 7 / 6 | 7 / 5 | +| fetched-row-write | 31 / 27 | 31 / 22 | 31 / 20 | +| overlapping-saved-direct-key | 23 / 22 | 23 / 16 | 23 / 15 | +| distinct-saved-direct-key | 29 / 30 | 29 / 36 | 29 / 37 | +| returned-value-equals-saved-after-direct-change | 2 / 2 | 2 / 4 | 2 / 3 | + +## RED, GREEN, and mutant + +The audited operator is `src/query.ts#applySuccessfulResult:field-drop:write +( { type : update , value : newItem } )`, M1334 at byte offset `97728`, line +`2756`, on source SHA256 +`1a794774e60d3c1dbacd515d0350b048557714adf80d6e3b9d42e7250841754b`. +The immutable Vite transform removes exactly ``type: `update`, `` at that +verified span. The comma/space removal preserves valid syntax; the semantic +edit is the audit's exact field deletion. Transformed SHA256 is +`8a1ffc1dbc0f37839b81a9faab2b21e0f253bc8accbc649b92117de5934216e0`. +The browser transform logs that the actual page imported the changed source. +No live production file was edited. + +| Execution | Result | +| --- | --- | +| Clean controlled + native Node SQLite, complete direct suite | 244 passed; both fixed/fresh campaigns and all calibrations | +| Clean actual Chromium + worker SQLite/OPFS, complete direct suite | 122 passed; fixed/fresh campaigns and all calibrations | +| Exact M1334 replay, controlled + native Node SQLite | 2 assertion failures at `sync-write` / `declared sync-write operation` | +| Same clean replay, controlled + native Node SQLite | 2 passed | +| Exact M1334 replay, actual Chromium/OPFS | 1 assertion failure at the same checkpoint and law | +| Same clean replay, actual Chromium/OPFS | 1 passed | + +All replays use campaign seed `563`, shrink path `15:0:0:0:0:0`: + +```json +{"shape":"raw","retained":true,"write":"before-fetch","operation":"insert","includesDirect":true,"keepsSaved":false,"directValue":0,"sourceValue":1,"savedValue":0,"overlapsSaved":false} +``` + +The intended assertion reports `observed undefined`. These are comparison +failures, not setup errors, timeouts, or incidental crashes. The calibration +`omits the declared sync-write operation` also corrupts a real captured +observation and proves the shared comparison rejects it. Scoped strict +TypeScript and ESLint both passed. The final +`pnpm --config.verifyDepsBeforeRun=false lint:changed` also passed. + +An earlier property-only selector lacked the required seed/path and failed +configuration. An initial browser attempt failed to bind its local server +inside the sandbox. Neither is product evidence. The final browser runs used +the ordinary authorized local-server permission and completed. + +## Rubric outcomes and limits + +| Requirement | Evidence | +| --- | --- | +| ORC-001 | Required public sync input discriminant; authority and partial scope above. | +| ORC-002 | API membership predicate independent of Query's diff/classifiers; existing merge model unchanged. | +| ORC-003 | Law, boundary recorder, comparison and reach prose live beside their executable definitions. | +| ORC-004 | Existing raw/derived, retention, order, inclusion, overlap and numeric grammar retained. The replay reconstructs M1334. Removing the changed-value/retained construction loses guaranteed result-time row-write reach; equal/unequal values and both orders remain neighbors. Overlapping insert histories remain rejected as illegal. | +| ORC-005 | Raw operation captured before unchanged forwarding; each host reaches the actual result-time write. No missing-input hypothesis is needed: the producer supplies the operation at this boundary. | +| ORC-006 | Exact field-drop rejected on three hosts, plus wrong-observation calibration; outcome classifications above. | +| ORC-007 | Same property, grammar, run budget, recorder and comparison run fixed/fresh on each host; exact seed/path replay tested RED and GREEN. | +| ORC-008 | No stateful reference-model change; not applicable. | +| ORC-009 | `sync-write` is one public change-message invocation. `fetched-row-write` combines fetch return with a later row-bearing change message; the mapping is explicit beside the recorder. | +| ORC-010 | Ordered-prefix comparison retains the earlier protocol violation; existing cleanup and browser diagnostic handling retained and calibrated. | +| ORC-011 | No shared semantic-fault hypothesis requiring a second formulation was identified; not applicable. | +| ORC-012 | This versioned record and exact fingerprints bound the claim. Other audit survivors remain open. | +| ORC-013 | Missing versus declared operation in the same history distinguishes the API requirement; the whole insert/update/delete union remains allowed. | +| ORC-014 | Controlled storage, native SQLite, and real Chromium/OPFS share the same histories and judgment. Each runs the physical-fetch/write premise. | +| ORC-015 | All fourteen declared states reached in all six campaigns; constructed and generated missing-state controls passed. | +| ORC-016 | Fixed histories and calibrations use the shared driver/model/check. No ordinary example assertions were added to the oracle. | + +This closes the missing-operation comparison gap within the existing first-eager +direct-write grammar and three receiving paths. It does not validate every sync +message field, transaction pairing, write count/order, arbitrary mutations, +failed I/O, peers, or concurrent writers. These added observations do not retire +any existing row law or grant an allowance for another surviving mutant. + +The independent final review accepted the authority, raw boundary observation, +unchanged row model, generic reach, and three-host evidence. Its one earlier +concern was resolved by removing the operation-specific reach condition, so a +legal delete/insert strategy does not fail the declared-state obligation. diff --git a/docs/contributing/oracle-reviews/2026-11-10-startup-property-gap-retention.md b/docs/contributing/oracle-reviews/2026-11-10-startup-property-gap-retention.md new file mode 100644 index 0000000000..026a578e18 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-startup-property-gap-retention.md @@ -0,0 +1,303 @@ +# Persisted startup: mutation retention after property conversion + +This record supplements [the property conversion evidence](2026-11-10-startup-property-conversion.md). +It checks whether converting the existing startup campaigns to `oracleProperty` +retained their sensitivity to the same selected mutants. It does not change the +accepted law, production implementation, comparison checkpoints, or run budgets. +It is a partial mutation selection, not an exhaustive mutation campaign or a PR +readiness verdict. + +## Frozen inputs and comparison + +The input manifest was captured at +`29de8a92e3aae74dc4c1c2c2d2d47e6edd44dc86`. Later commits changed HEAD while the +171 recorded source, test, configuration, and installed CLI files remained +byte-identical. The manifest and every comparison are under +`.review-2367/gap-property-after/`. Mutants are compared by their stable `key`, +not their numbered IDs or mutable HEAD. + +The originally resolved diff base is +`caa936d5af5332a6eca64335d291ba8e42c8cf37`. During the running PowerSync +retries, a parent-agent fetch also moved `origin/main` to +`858c8f0bf747e80f07d2fd6b10d8050bd0c84f8b`. The CLI had already selected its +mutants, and the recorded checkout inputs and four audited production files +remained unchanged. This record retains the original diff-base scope. The +latest-main merge and its verification are separate evidence. + +The production source hashes are: + +| Adapter | SHA-256 | +| --- | --- | +| Query | `6bbf1542e6daa38f33315445c5bb0ae3fbbf90d907cd388cb5d13f41c3b37cc5` | +| RxDB | `2dca19cff8bb602b2aa59f46c7635035e49d0f1cda0a2222e1917f6d2eb8aa62` | +| PowerSync | `d945eb99bff416fc9c7ba041a802fa842072e5cf03a71d02dd2ed214610df408` | +| TrailBase | `a0608813edaa2bc966ca6fbdbeb747b9fc6fbd8dd08a9c6edabbfe75288d2cd7` | + +Every command retains campaign seed `2367` and the previous selection. Exact +arguments are in the adapter `*-command.json` files. `compare.py` checks the +49 prior operator kills and the complete frozen manifest. The six additional +Query receiving-path kills are reported separately; they cannot compensate +for a lost prior kill. A timeout, native abort, setup error, or instrument +refusal is not a kill. + +## Completed results + +| Adapter | Report | Prior operator kills retained | Other evidence | +| --- | --- | --- | --- | +| Query | `97f527f64b9c` | 30/30 | Six additional operator kills; two authored controls killed | +| RxDB | `f33d959d659e` | 6/6 | Adapted semantic control killed in a separate report | +| TrailBase | `5d45c4a4288b` | 7/7 | Original semantic control killed | +| PowerSync | `fb2d82759888` | 6/6 | Adapted semantic control killed in `c8444431821b`; one calibration nondeterminism remains open | + +The final stable-key comparison retains all 49 prior operator kills, plus the +six additional Query operator kills. The 171 frozen files remained unchanged +through the final process exits. All audit processes have ended and the live +input freeze is released. Query, RxDB, and TrailBase have zero nondeterministic +exclusions. Both PowerSync reports retain the one explicitly open calibration +distinction diagnosed below. Every report has zero other excluded tests. +The Query report has 38 kills, four survivors, and two timeouts. +Its additional receiving-path kills are `M558`, `M559`, `M1376`, `M1377`, +`M1481`, and `M1793`. The remaining Query operator survivors are `M563`, +`M1473`, and `M1794`; `M1468` and `M1469` timed out. Its authored no-op control +survived. TrailBase `M151` survives, as before. These outcomes remain gaps +unless killed or covered by an explicit maintainer allowance for their anchored +gaps; a claim of equivalence or no observed difference does not close them. + +The old broad RxDB and PowerSync authored patches no longer apply to the +current source. Their adapted semantic controls are separate inputs and +reports. `does-not-apply` for the old patch is not a kill. + +## CLI and runtime failures + +The installed CLI/framework is pinned to +`e1fb5048c6d33e958643360d87e168c302e03bd1`. Its installed bytes were not edited. +The first official Query baseline could not load the native SQLite binding. +The parent agent rebuilt the installed `better-sqlite3` 12.11.1 addon under +Node 24.19.0, ABI 137. This installation repair changed no committed package or +lockfile. `native-repair.json` records the native binary hash. + +The official Query run then aborted with: + +```text +query.ts:M1376: tests/retained-scoped-hydration-oracle.test.ts has no unit and no earlier kill +``` + +The earlier error reported to the maintainer was the same missing-unit check +at `check.mjs:1550`, with a different active mutant and receiver: + +```text +Error: query.ts:M1468: tests/ssr-dehydration-oracle.test.ts has no unit and no earlier kill +``` + +The original log is `.review-2367/gap-current/original-five.log`; the diagnostic +and accounting record are `diagnostic-five.log` and +`expected-failure-accounting-bug.json` in that directory. A preceding +`test.fails` task had raw state `fail` in its `onTestFinished` callback, but +Vitest subsequently inverted it to `passed`. The capture hook incorrectly +marked that transient state as an early kill and skipped the later copies. +The runner then found no genuine earlier kill to justify their missing units. +The repair excludes `context.task.fails` from the capture hook's early-stop +condition; it does not accept the missing receiver as a product verdict. + +The isolated guard copy at +`/private/tmp/tanstack-db-2394-gap-cli-e1fb-guard` excludes expected-failure +tests from the capture hook's early-stop condition and adds diagnostics to the +existing missing-unit exception. The successful Query report uses that copy. +Its clean captures ran 1,278 tests in 125.5s and 121.5s, with zero +nondeterminism. The exact patch and original failure are preserved in +`expected-failure-guard-e1fb.patch` and `query-official-abort/`. This successful +rerun does not alone establish the cause of every earlier missing-unit failure. + +Official PowerSync runs reported 62 nondeterministic tests, but a baseline +worker had aborted and omitted tests. Those reports are invalid evidence. +The concrete classifier defect is that `check.mjs` checks retained-file +failures only on the first clean pass, while `baselineOf` can classify absent +second-pass rows as nondeterminism. An independent repeated capture of one +flagged fixed history produced identical observations. The preserved evidence +does not attribute every one of the 62 exclusions to a single cause. + +The isolated integrity copy at +`/private/tmp/tanstack-db-2394-gap-cli-e1fb-integrity` retains both clean +captures and refuses failed, aborted, missing, extra, or duplicate capture +rows before the existing observation/hash comparison. It does not relax that +comparison. Eight calibration cases show that it accepts equal complete +captures, preserves genuine differences as nondeterminism, and refuses each +of the tested incomplete or aborted capture forms. Its patch, hashes, and +calibrations are in `baseline-integrity-e1fb.patch`, +`baseline-integrity-cli-manifest.json`, and +`baseline-integrity-calibration.json`. + +This integrity guard rejected an actual explicit-forks PowerSync run under +Node 24.19.0. The first clean pass completed all 178 tests. In the second pass, +`persisted-capability-conformance-oracle.test.ts` had zero tests and zero rows +after `[vitest-pool]: Worker forks emitted error`; its first pass had 46 tests +and 47 capture rows, including the background row. The remaining second-pass +owners completed. The native stack reports +`RemoveEnvironmentCleanupHook: Assertion failed: (env) != nullptr` from +`better-sqlite3`'s `Database::~Database`. The log, command, and both captures +are preserved under `powersync-node24.19-integrity-refused*`. Explicit forks +and the addon rebuild therefore did not establish a repair of this native +failure. + +The separate adapted-control run under Node 24.19.0 was refused for the same +second-pass capability-worker failure. Its captures and diagnostics are +preserved under `powersync-semantic-control-node24.19-integrity-refused*`. + +The retries use the available bundled Node 24.22.1 runtime, also ABI 137, with +explicit forks. The exact executable and PATH are recorded in +`powersync-runtime.json`. Vitest's fork implementation calls +`child_process.fork` without an `execPath` override, so its children inherit +the selected runtime; PATH also starts with that runtime's directory. This +runtime variation is distinct from source equivalence, and a successful retry +would not prove which native lifetime defect caused the older runtime's abort. + +Both new-runtime retries completed all 178 tests in both clean passes without +native or setup failures. The main selection took 147.5s and 167.8s; the +adapted-control selection took 167.2s and 239.6s. Both found the same one +observed-hash difference in `calibration: raw judgment rejects wrong +before-source rows`. Eight isolated captures of that exact calibration each +executed one test and produced digest +`7aa71e006ccf3bf32e628aaebaae85608009a2aa`. Changing only its captured raw +`mutationReleasedBeforeFulfillment` from `true` to `false` exactly reproduces +the first clean pass's digest +`7c71b014dec592606b97121dad3cecaab5bb258a`. + +The history has no mutation (`fresh`, previous 0, next 0, mutation false). +The production driver initializes that test-local flag before source release +and sets it afterward; fulfillment can occur on either side. The raw checker +consults the flag only when `history.mutation` is true. This diagnostic +distinction has no law judgment for the captured history. It still needs an +explicit projection repair that preserves every mutation-true fact; this +record does not waive the CLI's resulting calibration exclusion. +`powersync-calibration-hash-diagnosis.json` preserves the exact reconstruction. + +## Remaining evidence + +The no-mutation raw projection needs its reviewed repair, calibrations, and +clean captures with no calibration exclusion. The affected selected mutants +then need a retention run against the integrated inputs. The existing +survivors and timeouts remain gaps unless killed or covered by an anchored +maintainer allowance. Native aborts and CLI refusals supply no product +correctness verdict, and none count toward the 55 retained/additional operator +kills. This evidence predates the latest-main merge and pending test +integrations; it does not claim those later inputs have been audited. + +## Test-title identity failure in the immutable conversion audit + +The subsequent conversion audit uses detached snapshot +`c4c00b20b6c91549a2b9fe502096d0841520f3fe` at +`/private/tmp/tanstack-db-2394-conversion-audit-4b71d9d`. It includes the reviewed +no-mutation projection repair and the primary Query and TrailBase property +conversions, with the original Query production source. Later Query receiving +paths and its value-fallback repair are a separate delta experiment. + +The strict baseline-integrity guard refused its Query run before mutation: + +```text +Error: first unmutated pass has incomplete captures for .../tests/documented-mutations-oracle.test.ts: {"missing":[],"extra":[],"duplicates":47} +``` + +Both clean passes executed 134 documented-history tests. Vitest 5.1.0 defaults +`test.taskTitleValueFormatTruncate` to 40 characters for values interpolated by +`test.each` and `test.for`. Its `formatTitle` implementation truncates string +attributes at that setting. Distinct long `$id` values therefore collapsed +into 87 capture keys. This is a test-identity failure in the gap instrument; +it supplies no product-correctness verdict. + +A new isolated copy at +`/private/tmp/tanstack-db-2394-gap-cli-e1fb-titles` sets +`test.taskTitleValueFormatTruncate: 0` in the generated wrapper config. The +Vitest implementation returns the original string when that setting is zero. +The installed CLI and the running PowerSync copy remain untouched. The +original refusal is preserved under `.retention-audit/query-title-refusal*`. + +The actual documented-history receiver then passed all 134 tests and recorded +134 distinct full keys. Equal complete captures pass the integrity guard; +adding a duplicate of an actual full-title capture still fails with +`duplicates:1`. The guard remains strict. The first calibration attempt had +an incorrect process working directory and failed to read documentation; that +setup error is preserved and supplies no successful calibration evidence. +The corrected result, source setting, full identities, and deliberate +duplicate rejection are in +`.retention-audit/title-identity-calibration/result.json`. The new CLI's hashes +and exact Query command are captured separately in `query-title-instruments.json` +and `query-command.json`. + +## Completed immutable conversion retention + +The detached conversion audit at `c4c00b20b6c91549a2b9fe502096d0841520f3fe` +completed with all **55 required operator kills retained by stable key**: the +49 original kills and the six additional Query kills. The seed is 2367. +Every report has zero nondeterministic and zero excluded tests. This result +supersedes the earlier incomplete no-mutation projection comparison; the +historical refusals and their diagnoses above remain part of the record. + +| Adapter | Required operator kills | Retained | Run | +| --- | ---: | ---: | --- | +| Query | 36 | 36 | `c6dab6374238` | +| RxDB | 6 | 6 | `d51a3bd1d663` | +| PowerSync | 6 | 6 | `dfe80e3a09f8` | +| TrailBase | 7 | 7 | `311e08b08d09` | + +The two required Query authored controls and the TrailBase authored control +also remain killed. The original RxDB and PowerSync authored patches no longer +apply after the validated persistence capability became a named local; that is +not a kill. Their separately adapted semantic controls both fail at the +persisted-startup comparison, in runs `2df9eb570204` and `79ea5f23bdad`. + +Both Query clean passes completed all 1,444 tests; both TrailBase passes +completed all 410 tests. PowerSync completed all 178 tests twice. Stable-key +comparison and the complete exact commands are retained in the snapshot's +`.retention-audit/retention-comparison.json` and `*-command.json` files. +Independent verification found no changed tracked source, control, runtime, +CLI file, any of the 1,092 built workspace outputs, or the three frozen +workspace bundles, and no import from the live checkout's source. + +Retention does not mean that every mutation is rejected. This original-source +snapshot still reports Query `M563`, `M1473`, `M1794`, and the authored +parenthesized-condition control as survivors, and TrailBase `M151` as a +survivor. The later Query receiving witnesses and repaired Query source are a +separate changed-callback experiment. Primary PowerSync/RxDB eager-source +property conversions are likewise an additive conversion-only audit at +`777935fd68daa497fdd3c9db533845b710be68a1`; they are outside this completed +55-kill experiment. These results do not claim a complete source mutation +campaign or closure of the remaining survivor classifications. + +## Completed primary PowerSync and RxDB conversion retention + +The subsequent seven-file primary eager-source conversion at +`777935fd68daa497fdd3c9db533845b710be68a1` also completed its retention audit. +PowerSync and RxDB each retain all six existing operator kills by stable key, +with zero nondeterministic and zero excluded tests. These are repeat checks of +12 keys already present in the 55-key campaign, not 12 additional distinct kills. +The application source is unchanged from c4 in this conversion-only snapshot. + +| Adapter | Required operator kills | Retained | Run | +| --- | ---: | ---: | --- | +| PowerSync | 6 | 6 | `96e1e23c1c70` | +| RxDB | 6 | 6 | `5f5866b66af7` | + +PowerSync's two clean captures pass all 265 tests, including all 89 tests in +the converted primary owner. RxDB's captures pass all 305 tests, including all +129 tests in its converted primary owner. Both use Node 24.22.1, forks and seed +2367. The CLI's printed clean duration is the maximum single-unit duration, +not total pass wall time; per-unit durations are retained in the evidence. +The PowerSync mutation run uses one context; benchmark measurements remained +held while mutation work ran. The final source, build, control and instrument +hash checks pass, and there are no live-checkout source imports. + +RxDB's separately adapted original-defect control is killed again in run +`d48451ac1f6a`. The duplicate PowerSync adapted-control run is deliberately +omitted: its earlier run `79ea5f23bdad` killed it through the persisted-startup +receiver, and all 33 captured local source/build inputs of that receiver are +byte-identical in the seven-file conversion snapshot. The control bytes and +runtime are identical too. None of the changed conversion paths modifies that +receiving instrument. Repeating that control would ask no distinct unproven +question. The exact before/after input hashes, original control evidence, +12-key comparison, commands, runtime and clean captures are preserved in the +[primary conversion evidence](2026-11-10-primary-eager-conversion-retention.json). + +This closes conversion retention for those primary owners. It does not claim +closure of later startup-work predicates, the final live production changes, +the unseeded fresh PowerSync campaign, or browser receiving paths added by +other workers. Those obligations retain their separate oracle and host evidence. diff --git a/docs/contributing/oracle-reviews/2026-11-10-trailbase-browser-property-integration.md b/docs/contributing/oracle-reviews/2026-11-10-trailbase-browser-property-integration.md new file mode 100644 index 0000000000..e2f6bc9b72 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-trailbase-browser-property-integration.md @@ -0,0 +1,288 @@ +# TrailBase browser oracle-property integration + +Reviewed base: `0e86c1fda`. This is the author's integration record. TrailBase +production remains unchanged; the new files convert and receive existing laws. +An immutable audit also captured snapshot `c4c00b2`. Subsequent cleanup in +this integration changed import ordering and erased TypeScript fixture annotations only. +No production guard, row law, comparison checkpoint, or run budget changed. + +## Law, ownership, and receiving paths + +Cleanup retires a sync run's authority. Late old list or persisted-hydration +work cannot change a cleaned-up Collection or a replacement's public/durable +rows, reader, readiness, or errors. A complete eager result replaces saved rows. +Authorities remain TrailBase's [lifecycle contract](../../../packages/trailbase-db-collection/tests/ORACLE.md), +the glossary's cleanup/sync-run terms, the shared complete eager-result law, +and the SQLite guide's "Render after SQLite restore" readiness contract. +The shared eager-source judgment separately owns field fidelity, publication, +initial readiness, health, restart and resource-release laws. + +The [hydration suite](../../../packages/trailbase-db-collection/tests/lifecycle-hydration-oracle-suite.ts) +retains the independent row-set model and shared three-cut comparison. Controlled +storage, real Node SQLite, and real Chromium WASQLite/OPFS call the same +production driver. The [eager suite](../../../packages/trailbase-db-collection/tests/eager-live-source-oracle-suite.ts) +uses the original shared model/check at every original checkpoint; its plain +RecordApi supplies the same native stream in Node and Chromium. Expected rows +never come from production commit counts or classifiers. + +Nine hydration builders return fresh randomized histories and retain their +required states in a WeakMap. Five eager builders each require all 23 states, +preserving minimum-five reach. Both seeded constructions and objects produced +by the generated arbitrary are checked. The primary lifecycle/page conversion +retains 50/40 runs and seeds 714203/205917. Its boundary grammar is enumerated: +all 51 original histories (16 early demand, 28 one-page races, 4 held pages, 2 stream +endings, 1 fresh run); it has no random campaign or shrink path. + +## Normal command results + +From `packages/browser-db-sqlite-persistence`: + +```sh +pnpm --config.verifyDepsBeforeRun=false test:oracles:adapters-browser \ + tests/trailbase-hydration-oracle.test.ts \ + tests/trailbase-eager-live-source-oracle.test.ts \ + tests/trailbase-browser-fidelity.test.ts --maxWorkers=1 +``` + +**159/159 checks GREEN** across 3 files in 358.01 seconds: 119 eager, 37 hydration, and 3 recorder +controls. Eager runs 600 fixed seed 2118 plus 600 fresh; hydration runs 50 fixed +seed 728159 plus 50 fresh, its 24 fixed histories, calibrations and determinism. +Counts below include the framework's seed-derived constructions as well as +its generated runs; literal fixed histories do not discharge required reach. + +From `packages/trailbase-db-collection`: + +```sh +pnpm --config.verifyDepsBeforeRun=false test:oracles \ + --coverage.enabled=false --maxWorkers=1 +``` + +**410/410 checks GREEN**, 3 files, 112.12 seconds, zero type errors. This includes the full +primary lifecycle/page/finite-boundary suite, both persisted-hydration hosts, +the 600-run eager campaigns, and shared persisted-startup receiving paths. +The first integration attempt failed setup on bare scratch import aliases in +the primary lifecycle file; correcting them to relative imports repaired setup. +That failed attempt is not credited as product evidence. + +### Persisted hydration reach + +| Declared state | Controlled fixed | Controlled fresh | Node SQLite fixed | Node SQLite fresh | Chromium fixed | Chromium fresh | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| empty-replacement | 14 | 16 | 14 | 8 | 14 | 7 | +| hydration-held-cleanup | 34 | 31 | 34 | 27 | 34 | 30 | +| late-list-rejection | 25 | 28 | 25 | 32 | 25 | 29 | +| new-row-outside-old-list | 18 | 20 | 18 | 20 | 18 | 18 | +| nonempty-replacement | 45 | 43 | 45 | 51 | 45 | 52 | +| old-row-omitted | 23 | 15 | 23 | 14 | 23 | 17 | +| resume-cleaned-up | 23 | 13 | 23 | 23 | 23 | 19 | +| resume-replacement-loading | 16 | 25 | 16 | 21 | 16 | 19 | +| resume-replacement-ready | 20 | 21 | 20 | 15 | 20 | 21 | + +### Shared eager-source reach + +| Declared state | Node fixed | Node fresh | Chromium fixed | Chromium fresh | +| --- | ---: | ---: | ---: | ---: | +| change-batch | 412 | 433 | 412 | 415 | +| failure-after-ready | 377 | 405 | 377 | 372 | +| failure-before-ready | 380 | 393 | 380 | 394 | +| field-removed | 301 | 322 | 301 | 308 | +| first-ready | 456 | 488 | 456 | 449 | +| malformed-before-ready | 350 | 360 | 350 | 364 | +| multi-change-batch | 356 | 375 | 356 | 358 | +| restart | 413 | 424 | 413 | 400 | +| restart-after-failure | 339 | 354 | 339 | 336 | +| value:array:change | 320 | 339 | 320 | 326 | +| value:array:load | 337 | 376 | 337 | 338 | +| value:boolean:change | 323 | 337 | 323 | 324 | +| value:boolean:load | 336 | 363 | 336 | 343 | +| value:float:change | 314 | 336 | 314 | 316 | +| value:float:load | 325 | 347 | 325 | 344 | +| value:integer:change | 321 | 338 | 321 | 323 | +| value:integer:load | 340 | 364 | 340 | 349 | +| value:null:change | 316 | 347 | 316 | 328 | +| value:null:load | 338 | 372 | 338 | 338 | +| value:object:change | 313 | 337 | 313 | 319 | +| value:object:load | 339 | 370 | 339 | 335 | +| value:string:change | 319 | 342 | 319 | 321 | +| value:string:load | 344 | 365 | 344 | 338 | + +### Primary lifecycle and page reach + +| Declared state | Fixed | Fresh | +| --- | ---: | ---: | +| cleanup-during-list | 30 | 28 | +| cleanup-during-subscribe | 32 | 28 | +| list-rejected | 14 | 20 | +| metadata-absent | 42 | 46 | +| metadata-present | 21 | 17 | +| replacement-before-retired-delivery | 13 | 11 | +| stream-buffered-close | 9 | 6 | +| stream-cleanup | 8 | 12 | +| stream-close | 17 | 7 | +| stream-immediate-cleanup | 18 | 9 | +| stream-parse-error | 5 | 6 | +| stream-read-error | 8 | 11 | +| subscribe-rejected | 15 | 13 | + +| Declared state | Fixed | Fresh | +| --- | ---: | ---: | +| cursor-paging | 25 | 31 | +| delete-first-page-in-flight | 6 | 6 | +| event-in-transit-across-page | 10 | 12 | +| insert-new-key | 6 | 6 | +| offset-paging | 22 | 16 | +| overlapping-loads | 17 | 26 | +| update-between-pages | 10 | 9 | + +## Browser failure fidelity + +The generic [Chromium host](../../../packages/browser-db-sqlite-persistence/tests/adapter-chromium-host.ts) +starts Vite on a random local port and launches real Chromium (local Chrome, +CI's installed Chromium, or the existing PLAYWRIGHT_CHANNEL override). Its +page uses actual WASQLite workers/OPFS and closes/removes artifacts per history. +It supplies cross-origin-isolation headers and a single Query SDK alias for the +shared Query receivers. No browser premise is simulated. + +The receiver preserves the full raw product result. Shared judgments report a +product mismatch under its original law/checkpoint and retain browser errors +as secondary diagnostics. Only an otherwise clean product result emits +`detached-browser-error` at checkpoint `browser`. An append-only error recorder +with a consumed cursor checks each diagnostic once, including errors arriving +between histories. Attribution is by arrival, not a hidden source sync run. +Errors during host startup reject acquisition and preserve their cause through +cleanup. + +The ordinary [recorder controls](../../../packages/browser-db-sqlite-persistence/tests/trailbase-browser-fidelity.test.ts) +use the same shared checks. For both receiving paths they combine a product +mismatch with a real page error, check a clean result plus a page error, deliver +another error between histories, then require the following clean history to +pass. A real bootstrap page error separately rejects host acquisition. These +3 controls pass in the 159-check run. After import/type-only cleanup, the normal +command restricted to this control file passes 3/3 again in 4.71 seconds. + +## Generated-only reach RED and replay + +A scratch recorder transform suppresses the required reach label only for a +constructed output unequal to every seed-derived construction at seed 728159. +Seeded constructions are explicitly exempt. Production rows, the raw final +observation, the independent model and the comparison stay unchanged. Thus the +failure exercises the local WeakMap obligation on the generated branch rather +than the framework's seeded-construction map. + +The fixed campaign shrinks to seed 728159, path `9:0:0:0`, with an empty +saved/old/replacement set, completed old list, default rendering, and resume at +cleaned-up. It fails at `reach`, law `constructed builder reach`, difference +`generated construction omitted hydration-held-cleanup`. + +The durable named owner's guarded replay also reproduces that exact RED using +an immutable external Vitest transform. The replay witness records one +execution with `failed:true`. The identical clean guarded replay passes with +one execution and `failed:false`. Commands from the repository root: + +```sh +TANSTACK_DB_ORACLE_SEED=728159 \ +TANSTACK_DB_ORACLE_PATH=9:0:0:0 \ +TANSTACK_DB_ORACLE_PROPERTY=trailbase.lifecycle.persisted-hydration.controlled \ +pnpm --config.verifyDepsBeforeRun=false exec node --import tsx \ + packages/db/tests/oracle-replay.ts \ + --config /private/tmp/pr2394-trailbase-integration-evidence/guarded-missing-state.config.ts \ + packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts +``` + +Remove only the `--config` argument for the clean guarded replay. This is a +calibration of generated reach enforcement; it is not a kill of M151. The +permanent suite also rejects all 9 individual hydration state omissions and lost +replacement rows. The eager conversion separately records its generated-only +RED/clean replay and 115 builder/state omission controls. + +The durable Chromium owner also passes the same seed/path through guarded +replay: one completed execution, `numRuns:1`, `failed:false`, 4.30 seconds. +From `packages/browser-db-sqlite-persistence`: + +```sh +TANSTACK_DB_ORACLE_SEED=728159 \ +TANSTACK_DB_ORACLE_PATH=9:0:0:0 \ +TANSTACK_DB_ORACLE_PROPERTY=trailbase.lifecycle.persisted-hydration.chromium-opfs \ +pnpm --config.verifyDepsBeforeRun=false exec node --import tsx \ + ../db/tests/oracle-replay.ts --config vitest.adapter-oracles.config.ts \ + tests/trailbase-hydration-oracle.test.ts +``` + +The sandboxed attempt could not bind the temporary localhost server (`EPERM`) +and recorded no execution. The permitted unsandboxed run above is the credited +result. It verifies browser replay routing; the reach-mutant RED above uses the +controlled storage owner. + +## Integration checks and retained witnesses + +Browser package `tsc --noEmit` passes after the final cleanup. Scoped ESLint +passes for all owned TS/config files. `vitest list --config vite.config.ts +--filesOnly` excludes all 3 Chromium files from both default runtime and Vitest +typecheck discovery while retaining ordinary tests. Package TypeScript still +includes these files. The dedicated config and browser CI step execute them +with an installed browser; both browser owners have compatible replay config +mappings. These are receiving-path routing changes, not disabled comparisons. + +The final branch-wide `pnpm --config.verifyDepsBeforeRun=false lint:changed` +run exits 1. It reports stale `legacyCampaigns` entries for the converted Query +ownership, TrailBase lifecycle, and TrailBase eager files. It also reports the +TrailBase eager property budget as removed (previously 600): the scanner does +not resolve its new registration helper. The helper still registers 600 fixed +and 600 fresh histories, as the normal Node and Chromium runs above demonstrate. +No run budget was reduced. Central legacy-entry cleanup and this scanner report +remain integration work; scoped ESLint passing does not imply branch-wide lint +passes. The exact output is in `lint-changed-final.log`. + +All old Playwright spec/page/HTML files remain. The new hydration receiver +supersedes only the branch of 24 TrailBase histories in the broader eager-gap portfolio of 226 histories; +it does not replace its Query precedence/scoped/cached-claim or RxDB/PowerSync +reload receivers. The persisted-startup portfolio, including 103 Query histories, +other adapters, fallback/capabilities and repeated same-origin startup, also +remains. No broad Playwright file can retire on this narrower evidence. + +## Remaining M151 gap and work distinction + +The post-`hydrateBaseline()` cancellation return in `trailBaseCollectionOptions` +remains intact. Deleting it produced no public/durable mismatch in the bounded +persisted composition, because retired callback authority is fenced downstream. +That is not dead execution: deletion continues through accepted synced entries. +For N applied rows and P projected keys the walk costs O(N+P); G omitted keys +allocate an O(G) array and cause one old begin, G write attempts and a commit. +The guard avoids that CPU and temporary memory work when an old continuation resumes +against a large replacement. + +Recommendation: retain this useful cancellation optimization. No promise that cleanup stops all internal work was invented to force a kill. Under current policy the anchored +M151 gap remains **open pending an oracle kill or explicit maintainer allowance**. +The bounded public equivalence result and this report do not close it. + +## Limits and artifacts + +RecordApi behavior and decoded events remain controlled; no deployed TrailBase +service or SDK fetch/SSE wire claim is made. Hydration uses three string keys +and numeric values, one retired/replacement run, three resume cuts, and default +versus network-first rendering before its 60-second deadline. Shared startup +coverage separately owns network fallback. No arbitrary concurrent mutation, +I/O failure, unbounded row count or every intermediate publication is proved. + +Scratch logs, immutable controls, replay delta and preserved conversion drafts +moved from the checkout to `/private/tmp/pr2394-trailbase-integration-evidence`. +Relevant logs: `durable-browser.log`, `durable-node-oracles-final.log`, +`durable-fidelity-final.log`, `durable-types-final.log`, `durable-lint-final.log`, +`default-discovery-final.log`, `durable-guarded-generated-red.log`, and +`durable-guarded-generated-clean.log`, `durable-guarded-chromium-clean.log`, plus +`lint-changed-final.log`. They are local supporting artifacts; +this versioned record retains the verdict-critical results and limits. + +## Integration amendment: tracked budgets and separate browser job + +After the normal campaigns above, the registration helper now accepts +`{ property, defaultRuns: 600 }` from each receiving wrapper. The helper uses +that count for the same property and both campaigns. This makes the existing +600-run property budget visible to the static scanner. No history, model, +comparison, calibration, identifier, or run count changed. + +`node scripts/check-oracle-run-counts.mjs` and `pnpm lint:changed` now pass +without the earlier removed-budget report. The new Chromium receivers run in +a separate `adapter-browser` E2E matrix group with its own Chromium installation. +The existing browser group keeps its 35-minute timeout and existing campaigns. +The aggregate E2E check requires the new group to succeed too. diff --git a/docs/contributing/oracle-reviews/2026-11-10-trailbase-lifecycle-framework-conversion.md b/docs/contributing/oracle-reviews/2026-11-10-trailbase-lifecycle-framework-conversion.md new file mode 100644 index 0000000000..cead6fa6d6 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-10-trailbase-lifecycle-framework-conversion.md @@ -0,0 +1,164 @@ +# TrailBase lifecycle framework conversion + +Source checkpoint: `76e3a7fbeaf5bc3127a06191a15bd1229412a7b3`, with the +uncommitted oracle-only conversion. No production file changes belong to this +conversion. The same-seed stable-key mutation retention report is a separate +required gate; the calibration results below do not replace it. + +## Law, model and history preservation + +The governing contract is `packages/trailbase-db-collection/tests/ORACLE.md`, +with Collection readiness, cleanup, sync-run retirement and health terminology +from the glossary. The conversion preserves all 82 original assertion sites. +Eighty retain their exact relation. The reports/rejections sites preserve their +empty-versus-events judgment while capturing opaque reasons as data. + +The authored RecordApi/service and independent Maps compute row truth from +commands. Their reference facts and the public production observations occupy +separate trace records. The checker compares them at every original cut. Error +and resource identity are relational observations encoded with stable tokens. +No driver runs Vitest assertions or reports a pass/fail summary. + +The lifecycle grammar retains one to three sync runs, eager/on-demand mode, +metadata absence/presence, zero to eight changes, five stream endings and both +cleanup timings. It now also generates the previously fixed-only optional +rejecting cancellation. The page grammar retains 258 source rows, cursor/offset, +one/two loads, at most ten steps and four changes. Offset inserts/deletes remain +excluded because service pagination shifts position. Its inverse rejects those +invalid forms and more than four changes. + +All 36 lifecycle fixed histories, five page fixed histories and 51 finite +histories remain. Fixed/fresh budgets and seeds remain 50/714203 and 40/205917. +The lifecycle and page grammars add thirteen and seven randomized constructed +builders. Every builder returns a fresh object and records requirements in a +WeakMap; checks enforce those requirements for generated outputs too. + +## Executed evidence + +Scratch files and logs are under `/private/tmp/pr2394-trailbase-lifecycle`. +`clean3.log`: 128/128 checks GREEN, including per-builder missing-state controls, +provider-owned row corruption, wrong status, error identity, reader ownership, +timer release, omitted rows and early fetch controls. `clean4.log`: 130/130 +GREEN after adding DOMException and opaque class diagnostic controls. Symbol +capture and the final integrated receiving run are recorded in the final +appendix once executed. + +`generated-controls.log` runs all twenty builders on fresh generated histories. +Each output differs in data from every named fixed-seed construction, is accepted +by its arbitrary, passes the unchanged production recorder/checker, and is rejected +under `constructed builder reach` when its required reach event is removed. +The two harness checks pass; all twenty per-builder results are printed. + +The driver and reference model are deterministic over fixed/constructed inputs +and three generated histories, as enforced by the framework. TypeScript and +normal/oracle ESLint passed on scratch before the final diagnostic additions. + +## Rubric accounting + +- ORC-001/002: Existing authority and independent service/Map laws are retained. + The trace transports independent fixture/model facts, never production-derived + row expectations. The fixture boundary remains the trusted base. +- ORC-003/009: Opening law, local model/driver prose and named cuts are retained. + Driver names now say `observe`; only the refinement functions judge results. + Opaque identity tokens are the declared abstraction for resources and errors. +- ORC-004/007: Both budgets/seeds and all fixed inputs remain. Missing metadata, + rejecting cancellation and allocated page commands now reconstruct. Numeric + ranges and offset exclusions remain; the mapper has a checked inverse. Replay + selectors change to each property followed by `.*random seed`. +- ORC-005/006: The same real adapter, native streams, Collection and live-query + paths run. Comparisons remain at all original cuts. Wrong-result and provider + corruption controls yield named law violations rather than setup failures. +- ORC-008: No model state is removed, merged or split. Service maps, in-transit + events and cleanup retirement retain their original distinguishing histories. +- ORC-010: Drivers retain cleanup/failure aggregation. Prefix checks compare + existing cuts before completeness, so a later driver error cannot replace an + earlier recorded violation. Opaque diagnostics cannot fail data capture for a + DOMException, class instance or symbol before the forbidden-event comparison. +- ORC-011: No new shared semantic fault hypothesis is addressed by this pure + conversion. Independent second formulation is not triggered. +- ORC-012: This record identifies the reviewed source checkpoint and bounded + evidence; it does not claim new product bug-class closure. +- ORC-013: Existing boundary distinctions and fixed histories remain. This + conversion adds no broader reusable-law claim. +- ORC-014: The legacy scope stays at controlled RecordApi/native streams. The + persisted hydration receiver and real Chromium witness use the shared primary + hydration model and are integrated by their separate conversion. +- ORC-015: Required states have randomized builders, fresh identities, WeakMap + enforcement, per-builder omission controls and distinct generated-only checks. +- ORC-016: Fixed regressions reuse the same model, driver and checker. The finite + property enumerates its whole legal grammar; ordinary assertions do not remain + inside oracle drivers. + +## Limits + +No deployed TrailBase server, SDK wire decoding, service reconnect or arbitrary +scheduler claim is added. The conversion does not fix production. Previous +mutant kills must survive the parent same-seed retention gate before readiness. + +## Final scratch appendix + +`clean-final.log`: **131/131 GREEN** in 26.35 seconds. This includes all three +arbitrary-reason capture controls: DOMException, opaque class instance and +symbol. `types5.log` and `lint5.log` are empty successful strict TypeScript and +normal/oracle ESLint outputs. The active source and observation normalization +were held fixed for this run. + +Normal enforced minimum is one randomized construction per state and per +campaign. Every builder also runs in the ordinary generated population with its +WeakMap obligation. Final measured counts: + +| Lifecycle state | Fixed | Random | +| --- | ---: | ---: | +| cleanup-during-list | 30 | 31 | +| cleanup-during-subscribe | 32 | 28 | +| list-rejected | 14 | 11 | +| metadata-absent | 42 | 37 | +| metadata-present | 21 | 26 | +| replacement-before-retired-delivery | 13 | 9 | +| stream-buffered-close | 9 | 2 | +| stream-cleanup | 8 | 11 | +| stream-close | 17 | 12 | +| stream-immediate-cleanup | 18 | 11 | +| stream-parse-error | 5 | 13 | +| stream-read-error | 8 | 7 | +| subscribe-rejected | 15 | 15 | + +| Page state | Fixed | Random | +| --- | ---: | ---: | +| cursor-paging | 25 | 23 | +| offset-paging | 22 | 24 | +| overlapping-loads | 17 | 21 | +| event-in-transit-across-page | 10 | 11 | +| update-between-pages | 10 | 6 | +| delete-first-page-in-flight | 6 | 8 | +| insert-new-key | 6 | 10 | + +The scratch-only reach mutant filters one `reach` observation from the +production driver only when a construction's encoded history differs from +every named seed-derived construction. It retains required-state metadata, +reference facts, production observations and the original checker. This tests +generated outputs separately from the framework's named construction map. + +- Lifecycle: reseeded campaign 916721 fails under `constructed builder reach` + at `complete`, omitting metadata-absent. Path **37:0** shrinks to eager mode, + metadata absent, one cancel-load resolving late. The direct mutant replay + fails after one test at the same law/cut with no further shrinking. The same + direct replay without suppression is GREEN. +- Page/event: reseeded campaign 916721 fails under that same law/cut, omitting + insert-new-key. Path **32:0:0** shrinks to cursor mode, one load, one immediate + set of key300. The direct mutant replay fails after one test at the same + law/cut with no further shrinking. The same clean direct replay is GREEN. + +Commands use `node /node_modules/vitest/vitest.mjs run`, the isolated +`reach-mutant.config.mjs` or clean `vite.config.mjs`, `--maxWorkers 1`, and +`-t 'trailbase.lifecycle.*random seed'` or +`-t 'trailbase.page-event.*random seed'`. Campaigns set +`TANSTACK_DB_ORACLE_SEED=916721`. Direct replays additionally set +`TANSTACK_DB_ORACLE_PROPERTY` and `TANSTACK_DB_ORACLE_PATH` to the values above. +Only mutant runs set `TRAILBASE_REACH_STATE=metadata-absent` or +`TRAILBASE_REACH_STATE=insert-new-key`. + +Logs: `lifecycle-generated-red3.log`, `page-generated-red2.log`, +`lifecycle-generated-replay-red.log`, `page-generated-replay-red.log`, and the +corresponding `*-generated-replay-green.log`. Earlier parse/configuration errors +are retained separately; none is counted as an oracle RED or mutant kill. diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.html b/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.html new file mode 100644 index 0000000000..c8898b9eb8 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.html @@ -0,0 +1,13 @@ + + + + + Persisted baseline lifecycle receiver + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.ts new file mode 100644 index 0000000000..2b35fe23ab --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.ts @@ -0,0 +1,38 @@ +/** Actual worker/OPFS storage receives the same lifecycle histories/check. */ +import { + createBrowserWASQLitePersistence, + openBrowserWASQLiteOPFSDatabase, +} from '../src' +import { observeBaselineLifecycle } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-driver' +import { rxdbBaselineLifecycleAdapter } from '../../rxdb-db-collection/tests/persisted-baseline-lifecycle-driver' +import { powerSyncBrowserBaselineLifecycleAdapter } from '../../powersync-db-collection/tests/persisted-baseline-lifecycle-browser' +import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { removeStartupOPFSArtifacts } from './persisted-startup-opfs-cleanup' +import type { BaselineLifecycleHistory } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle' + +async function runBaselineLifecycle( + adapter: `rxdb` | `powersync`, + history: BaselineLifecycleHistory, + id: string, + candidate: boolean, +) { + const databaseName = `ps-baseline-lifecycle.sqlite` + await removeStartupOPFSArtifacts(databaseName) + const database = await openBrowserWASQLiteOPFSDatabase({ databaseName }) + return withOracleCleanup( + () => + observeBaselineLifecycle( + adapter === `rxdb` + ? rxdbBaselineLifecycleAdapter + : powerSyncBrowserBaselineLifecycleAdapter, + createBrowserWASQLitePersistence({ database }), + inProcessSeams.holdFirstRowSnapshot, + history, + id, + candidate, + ), + [() => database.close?.(), () => removeStartupOPFSArtifacts(databaseName)], + ) +} +Object.assign(window, { runBaselineLifecycle }) diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts index bd77f229cb..b7c69a96b1 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts @@ -1,7 +1,11 @@ import { expect, test } from '@playwright/test' import { declareOracle } from '../../db/tests/oracle-declaration' import { queryStartupHistories } from '../../query-db-collection/tests/persisted-startup-driver' -import { persistedStartupHistories } from '../../db-adapter-conformance/tests/persisted-startup-oracle' +import { + persistedStartupBatchHistories, + persistedStartupHistories, + persistedStartupSingleCommitHistories, +} from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { capabilityHistories } from '../../db-adapter-conformance/tests/persisted-capability-oracle' import type { StartupConformanceResult } from './persisted-startup-conformance.opfs' @@ -12,16 +16,16 @@ function checkStartupConformance(result: StartupConformanceResult) { expect( result.results.filter((item) => item.adapter === `query`), ).toHaveLength(queryStartupHistories.length) - for (const adapter of [ - `convex`, - `electric`, - `trailbase`, - `rxdb`, - `powersync`, - ]) + for (const adapter of [`convex`, `electric`, `trailbase`]) expect( result.results.filter((item) => item.adapter === adapter), ).toHaveLength(persistedStartupHistories.length) + expect( + result.results.filter((item) => item.adapter === `powersync`), + ).toHaveLength(persistedStartupBatchHistories.length) + expect(result.results.filter((item) => item.adapter === `rxdb`)).toHaveLength( + persistedStartupSingleCommitHistories.length, + ) for (const name of [`rxdb`, `powersync`]) expect( result.capabilities.filter((item) => item.adapter === name), diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts index 45c7182bab..786c72042f 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts @@ -1,9 +1,11 @@ // Chromium OPFS receives the same shared startup comparison as both Node -// hosts. The real QueryClient and Convex SDK supply their adapter boundaries; -// this page supplies only actual worker/OPFS storage and result reporting. +// hosts. Each adapter supplies its real SDK or controlled provider boundary; +// this page supplies actual worker/OPFS storage and result reporting. import { PersistedStartupViolation, + persistedStartupBatchHistories, persistedStartupHistories, + persistedStartupSingleCommitHistories, persistedStartupViolations, startupReachedStates, startupRequiredStates, @@ -71,7 +73,12 @@ async function run() { // This page has no Node oracle runner. Each completed history calls its // startup observer once; fail locally if any required receiving state is absent. const reach: Record = Object.fromEntries( - startupRequiredStates(adapter.scope).map((state) => [state, 0]), + startupRequiredStates( + adapter.scope, + histories.some((h) => h.batchSize !== undefined), + histories.some((h) => h.singleSourceCommit), + histories.some((h) => h.sourceKeys), + ).map((state) => [state, 0]), ) for (const history of histories) { const index = sequence++ @@ -128,11 +135,15 @@ async function run() { trailBaseStartupAdapter, persistedStartupHistories, ) - await receive(`rxdb`, rxdbStartupAdapter, persistedStartupHistories) + await receive( + `rxdb`, + rxdbStartupAdapter, + persistedStartupSingleCommitHistories, + ) await receive( `powersync`, powerSyncBrowserStartupAdapter, - persistedStartupHistories, + persistedStartupBatchHistories, ) for (const [name, adapter] of [ [`rxdb`, rxdbStartupAdapter], diff --git a/packages/browser-db-sqlite-persistence/e2e/query-immediate-cache-oracle.browser.test.ts b/packages/browser-db-sqlite-persistence/e2e/query-immediate-cache-oracle.browser.test.ts new file mode 100644 index 0000000000..5509d8b82f --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-immediate-cache-oracle.browser.test.ts @@ -0,0 +1,47 @@ +/** Real Chromium receives the same synchronous eager materialization judgment. */ +import { afterAll, beforeAll } from 'vitest' +import { oracleProperty } from '../../db/tests/oracle-property' +import { immediateEagerMaterializationOptions } from '../../query-db-collection/tests/immediate-eager-materialization-oracle' +import { + captureBrowserHistory, + startAdapterChromium, +} from '../tests/adapter-chromium-host' +import { deliverQueryHistory } from './query-ownership-browser-support' +import type { QueryReceivingResult } from './query-ownership-browser-support' +import type { ImmediateMaterializationHistory } from '../../query-db-collection/tests/immediate-eager-materialization-oracle' + +let browser: Awaited> | undefined +beforeAll(async () => { + browser = await startAdapterChromium({ + entry: + '/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.html', + ready: 'runQueryImmediateEagerMaterialization', + }) +}, 120000) +afterAll(async () => { + await browser?.close() +}, 120000) +const options = immediateEagerMaterializationOptions(async (h, id, observe) => { + const activeBrowser = browser + if (!activeBrowser) throw new Error('Chromium receiver not initialized') + const result = await captureBrowserHistory(activeBrowser, () => + activeBrowser.page.evaluate( + async ({ history, collectionId }) => + ( + window as unknown as { + runQueryImmediateEagerMaterialization: ( + history: ImmediateMaterializationHistory, + id: string, + ) => Promise + } + ).runQueryImmediateEagerMaterialization(history, collectionId), + { history: h, collectionId: id }, + ), + ) + deliverQueryHistory(result, observe) +}, 'chromium') +oracleProperty({ + ...options, + id: 'query.immediate-eager-materialization.chromium-opfs', + check: (h, trace, expected) => options.check(h, trace, expected), +}) diff --git a/packages/browser-db-sqlite-persistence/e2e/query-ownership-browser-support.ts b/packages/browser-db-sqlite-persistence/e2e/query-ownership-browser-support.ts new file mode 100644 index 0000000000..15f8348e2f --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-ownership-browser-support.ts @@ -0,0 +1,59 @@ +/** Browser transport preserves product observations before cleanup diagnostics. + * Error object identity does not cross the page boundary. Name, message, stack + * and separate aggregate/cause diagnostics do; no error-identity law is claimed. + */ +import type { Observation } from '../../db/tests/oracle-property' + +export type QueryReceivingResult = { + trace: Array + failure?: { + name: string + message: string + stack?: string + cause?: string + secondary: ReadonlyArray + } +} +export async function recordQueryHistory( + run: (observe: (checkpoint: string, value: unknown) => void) => Promise, +): Promise { + const trace: Array = [] + try { + await run((checkpoint, value) => trace.push({ checkpoint, value })) + return { trace } + } catch (error) { + return { + trace, + failure: { + name: error instanceof Error ? error.name : 'NonError', + message: error instanceof Error ? error.message : String(error), + ...(error instanceof Error && error.stack + ? { stack: error.stack } + : {}), + ...(error instanceof Error && error.cause !== undefined + ? { cause: String(error.cause) } + : {}), + secondary: + error instanceof AggregateError + ? Array.from(error.errors, String) + : [], + }, + } + } +} +export function deliverQueryHistory( + result: QueryReceivingResult & { browserErrors: ReadonlyArray }, + observe: (checkpoint: string, value: unknown) => void, +): void { + // Product facts arrive first. The shared check chooses their mismatch before + // detached browser errors or a later cleanup error can replace it. + for (const o of result.trace) observe(o.checkpoint, o.value) + observe('browserErrors', result.browserErrors) + if (result.failure) { + const error = new Error(result.failure.message, { + cause: { ...result.failure, browserErrors: result.browserErrors }, + }) + error.name = result.failure.name + throw error + } +} diff --git a/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.html b/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.html new file mode 100644 index 0000000000..d43ee289d7 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts new file mode 100644 index 0000000000..91b8da5dd8 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts @@ -0,0 +1,52 @@ +import { + createBrowserWASQLitePersistence, + openBrowserWASQLiteOPFSDatabase, +} from '../src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { observePersistedDirectOwnership } from '../../query-db-collection/tests/persisted-direct-ownership-driver' +import { observeImmediateEagerMaterialization } from '../../query-db-collection/tests/immediate-eager-materialization-driver' +import { + removeStartupOPFSArtifacts, + startupOPFSArtifacts, +} from './persisted-startup-opfs-cleanup' +import { recordQueryHistory } from './query-ownership-browser-support' +import type { DirectOwnershipHistory } from '../../query-db-collection/tests/persisted-direct-ownership-oracle' +import type { ImmediateMaterializationHistory } from '../../query-db-collection/tests/immediate-eager-materialization-oracle' + +async function runQueryPersistedDirectOwnership( + h: DirectOwnershipHistory, + id: string, +) { + return recordQueryHistory(async (observe) => { + const name = 'ps-query-direct-ownership.sqlite' + await removeStartupOPFSArtifacts(name) + const database = await openBrowserWASQLiteOPFSDatabase({ + databaseName: name, + }) + await withOracleCleanup(async () => { + await observePersistedDirectOwnership( + createBrowserWASQLitePersistence({ database }), + h, + id, + observe, + ) + }, [() => database.close?.(), () => removeStartupOPFSArtifacts(name)]) + const artifacts = await startupOPFSArtifacts() + if (artifacts.length) + throw new Error( + 'Query ownership OPFS artifacts remain: ' + artifacts.join(', '), + ) + }) +} +async function runQueryImmediateEagerMaterialization( + h: ImmediateMaterializationHistory, + id: string, +) { + return recordQueryHistory(async (observe) => { + await observeImmediateEagerMaterialization(h, id, observe) + }) +} +Object.assign(window, { + runQueryPersistedDirectOwnership, + runQueryImmediateEagerMaterialization, +}) diff --git a/packages/browser-db-sqlite-persistence/e2e/query-persisted-direct-ownership-oracle.browser.test.ts b/packages/browser-db-sqlite-persistence/e2e/query-persisted-direct-ownership-oracle.browser.test.ts new file mode 100644 index 0000000000..120ed99000 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-persisted-direct-ownership-oracle.browser.test.ts @@ -0,0 +1,47 @@ +/** Real worker SQLite/OPFS receives the primary direct-row ownership judgment. */ +import { afterAll, beforeAll } from 'vitest' +import { oracleProperty } from '../../db/tests/oracle-property' +import { persistedDirectOwnershipOptions } from '../../query-db-collection/tests/persisted-direct-ownership-oracle' +import { + captureBrowserHistory, + startAdapterChromium, +} from '../tests/adapter-chromium-host' +import { deliverQueryHistory } from './query-ownership-browser-support' +import type { QueryReceivingResult } from './query-ownership-browser-support' +import type { DirectOwnershipHistory } from '../../query-db-collection/tests/persisted-direct-ownership-oracle' + +let browser: Awaited> | undefined +beforeAll(async () => { + browser = await startAdapterChromium({ + entry: + '/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.html', + ready: 'runQueryPersistedDirectOwnership', + }) +}, 120000) +afterAll(async () => { + await browser?.close() +}, 120000) +const options = persistedDirectOwnershipOptions(async (h, id, observe) => { + const activeBrowser = browser + if (!activeBrowser) throw new Error('Chromium receiver not initialized') + const result = await captureBrowserHistory(activeBrowser, () => + activeBrowser.page.evaluate( + async ({ history, collectionId }) => + ( + window as unknown as { + runQueryPersistedDirectOwnership: ( + history: DirectOwnershipHistory, + id: string, + ) => Promise + } + ).runQueryPersistedDirectOwnership(history, collectionId), + { history: h, collectionId: id }, + ), + ) + deliverQueryHistory(result, observe) +}, 'chromium-opfs') +oracleProperty({ + ...options, + id: 'query.persisted-direct-ownership.chromium-opfs', + check: (h, trace, expected) => options.check(h, trace, expected), +}) diff --git a/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-bootstrap-error.html b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-bootstrap-error.html new file mode 100644 index 0000000000..3a7081f611 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-bootstrap-error.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-cleanup.ts b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-cleanup.ts new file mode 100644 index 0000000000..340c273f7c --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-cleanup.ts @@ -0,0 +1,29 @@ +// Worker termination can release its OPFS handles in a later native task. +// Retry only the exact transient busy condition after awaited database close. +import { startupTurn } from '../../db-adapter-conformance/tests/persisted-startup-driver' + +export async function removeClosedArtifacts( + remove: () => Promise, + observe: (cut: string, value: unknown) => void, + attempts = 100, +) { + for (let attempt = 1; ; attempt++) { + try { + await remove() + return + } catch (error) { + if (!( + error instanceof DOMException && + error.name === 'NoModificationAllowedError' + )) + throw error + observe('opfsCleanupRetry', { + attempt, + name: error.name, + message: error.message, + }) + if (attempt >= attempts) throw error + await startupTurn() + } + } +} diff --git a/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-oracle.browser.test.ts b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-oracle.browser.test.ts new file mode 100644 index 0000000000..3a38e92d7f --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-oracle.browser.test.ts @@ -0,0 +1,117 @@ +import { afterAll, beforeAll, describe } from 'vitest' +import { + captureBrowserHistory, + startAdapterChromium, +} from '../tests/adapter-chromium-host' +import { + arbitrary, + constructed, + cutoffViolations, + expected, + obligations, + states, +} from '../../query-db-collection/tests/persisted-rejected-write-cutoff-oracle' +import { oracleProperty, pass, violation } from '../../db/tests/oracle-property' +import { deliverQueryHistory } from './query-ownership-browser-support' +import type { QueryReceivingResult } from './query-ownership-browser-support' +import type { + History, + Trace, +} from '../../query-db-collection/tests/persisted-rejected-write-cutoff-oracle' + +describe('chromium-opfs', () => { + let sequence = 0 + let browser: Awaited> | undefined + beforeAll(async () => { + browser = await startAdapterChromium({ + entry: + '/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.html', + ready: 'runCutoffCandidate', + }) + }, 120000) + afterAll(async () => { + await browser?.close() + }, 120000) + oracleProperty({ + id: 'query.persisted-rejected-write-cutoff.chromium-opfs', + law: { + statement: + 'A rejected direct write and stale observer callback leave a fetched cache authoritative for an Infinity-stale peer acquisition.', + authority: + 'Query guide Understanding the Data Stores item 1, Direct Writes and Query Sync; Query Core staleTime and refetchOnMount; primary ownership direct-write-order judgment.', + owner: 'Query ownership lifecycle receiving extension', + limits: + 'Empty eager persisted startup, one successful fetched row, rejected missing-key update/delete, raw or direct wrapped array; no accepted mutation, invalidation, failed I/O or concurrent writer.', + }, + histories: { arbitrary, runs: 20, fixedSeeds: [1794], states, constructed }, + model: expected, + driver: async (h, observe) => { + if (!browser) throw new Error('Chromium receiver did not start') + const host = browser + const captured = await captureBrowserHistory(host, async () => + host.page.evaluate( + async ({ history, id }) => + ( + window as unknown as { + runCutoffCandidate: ( + h: History, + id: string, + ) => Promise + } + ).runCutoffCandidate(history, id), + { history: h, id: 'cutoff-opfs-' + sequence++ }, + ), + ) + observe('browserReceiver', true) + deliverQueryHistory(captured, observe) + }, + check: (h, trace) => { + const bad = cutoffViolations(h, trace)[0] + return bad + ? violation( + bad.checkpoint, + bad.difference, + bad.law ?? 'fetched cache authority', + ) + : pass + }, + calibrations: [ + { + name: 'keeps an earlier count mismatch on a truncated trace', + law: 'fetched cache authority', + corrupt: (trace: ReadonlyArray) => { + const at = trace.findIndex((o) => o.checkpoint === 'firstFetches') + return at < 0 + ? undefined + : trace + .slice(0, at + 1) + .map((o) => + o.checkpoint === 'firstFetches' ? { ...o, value: 2 } : o, + ) + }, + }, + ...states.map((state) => ({ + name: 'omits constructed state ' + state, + law: 'fetched cache authority', + corrupt: (trace: ReadonlyArray, h: History) => + obligations.get(h)?.includes(state) + ? trace.filter((o) => o.checkpoint !== 'reach' || o.value !== state) + : undefined, + })), + { + name: 'adds a request for a fetched cache', + law: 'fetched cache authority', + corrupt: (trace: ReadonlyArray) => + trace.map((o) => + o.checkpoint === 'peerFetches' ? { ...o, value: 2 } : o, + ), + }, + { + name: 'changes peer value without a fetch', + law: 'fetched cache authority', + corrupt: (trace: ReadonlyArray) => + trace.map((o) => (o.checkpoint === 'peer' ? { ...o, value: [] } : o)), + }, + ], + }) +}) diff --git a/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.html b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.html new file mode 100644 index 0000000000..99479ca6e4 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.ts new file mode 100644 index 0000000000..e829953735 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.ts @@ -0,0 +1,84 @@ +import { + createBrowserWASQLitePersistence, + openBrowserWASQLiteOPFSDatabase, +} from '../src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { observeCutoff } from '../../query-db-collection/tests/persisted-rejected-write-cutoff-oracle' +import { + removeStartupOPFSArtifacts, + startupOPFSArtifacts, +} from './persisted-startup-opfs-cleanup' +import { recordQueryHistory } from './query-ownership-browser-support' +import { removeClosedArtifacts } from './query-rejected-write-cutoff-cleanup' +import type { History } from '../../query-db-collection/tests/persisted-rejected-write-cutoff-oracle' + +export type ReceiverFault = + | 'cleanup' + | 'mismatch-cleanup' + | 'mismatch-only' + | 'prefix-cleanup' + | 'transient-busy' + | 'permanent-busy' + | 'mismatch-permanent-busy' + +async function runCutoffCandidate( + h: History, + id: string, + fault?: ReceiverFault, +) { + return recordQueryHistory(async (observe) => { + const name = 'ps-query-cutoff-candidate.sqlite' + await removeStartupOPFSArtifacts(name) + const database = await openBrowserWASQLiteOPFSDatabase({ + databaseName: name, + }) + await withOracleCleanup(async () => { + await observeCutoff( + createBrowserWASQLitePersistence({ database }), + h, + id, + (cut, value) => { + if (fault === 'prefix-cleanup' && cut === 'stalePremise') + throw new Error('cutoff interrupted result') + observe( + cut, + fault?.startsWith('mismatch') && cut === 'peerFetches' + ? 2 + : fault === 'prefix-cleanup' && cut === 'firstFetches' + ? 2 + : value, + ) + }, + ) + }, [ + () => database.close?.(), + async () => { + let injected = 0 + await removeClosedArtifacts( + async () => { + if ( + fault?.includes('permanent-busy') || + (fault === 'transient-busy' && injected++ === 0) + ) + throw new DOMException( + 'cutoff injected busy', + 'NoModificationAllowedError', + ) + await removeStartupOPFSArtifacts(name) + }, + observe, + fault?.includes('permanent-busy') ? 3 : 100, + ) + }, + () => { + if (fault?.includes('cleanup')) throw new Error('cutoff cleanup fault') + }, + ]) + const artifacts = await startupOPFSArtifacts() + if (artifacts.length) + throw new Error( + 'Candidate OPFS artifacts remain: ' + artifacts.join(', '), + ) + }) +} +Object.assign(window, { runCutoffCandidate }) diff --git a/packages/browser-db-sqlite-persistence/e2e/query-scope-omission-oracle.browser.test.ts b/packages/browser-db-sqlite-persistence/e2e/query-scope-omission-oracle.browser.test.ts new file mode 100644 index 0000000000..0a65225dc8 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-scope-omission-oracle.browser.test.ts @@ -0,0 +1,60 @@ +import { afterAll, beforeAll, it } from 'vitest' +import { oracleReplayRequested } from '../../db/tests/oracle-config' +import { oracleProperty } from '../../db/tests/oracle-property' +import { + captureBrowserHistory, + startAdapterChromium, +} from '../tests/adapter-chromium-host' +import { + calibrateGeneratedConstructions, + options, +} from '../../query-db-collection/tests/scope-omission-reacquisition-oracle' +import { deliverQueryHistory } from './query-ownership-browser-support' +import type { History } from '../../query-db-collection/tests/scope-omission-reacquisition-oracle' +import type { QueryReceivingResult } from './query-ownership-browser-support' + +let browser: Awaited> | undefined +beforeAll(async () => { + browser = await startAdapterChromium({ + entry: + '/packages/browser-db-sqlite-persistence/e2e/query-scope-omission.opfs.html', + ready: 'runScopeOmission', + }) +}, 120000) +afterAll(async () => { + await browser?.close() +}, 120000) +const receive: Parameters[0] = async (h, id, observe) => { + if (!browser) throw new Error('Missing Chromium receiver') + const host = browser + const result = await captureBrowserHistory(host, () => + host.page.evaluate( + ({ history, collectionId }) => + ( + window as unknown as { + runScopeOmission: ( + h: History, + id: string, + ) => Promise + } + ).runScopeOmission(history, collectionId), + { history: h, collectionId: id }, + ), + ) + observe('browserReceiver', true) + deliverQueryHistory(result, observe) +} +const property = options(receive, 'chromium-opfs') +oracleProperty({ + ...property, + check: (h, trace, expected) => property.check(h, trace, expected), +}) +let controlSequence = 0 +it.skipIf(oracleReplayRequested())( + 'rejects missing states from generated constructions', + () => + calibrateGeneratedConstructions((h, observe) => + receive(h, 'generated-control-browser-' + controlSequence++, observe), + ), + 120000, +) diff --git a/packages/browser-db-sqlite-persistence/e2e/query-scope-omission.opfs.html b/packages/browser-db-sqlite-persistence/e2e/query-scope-omission.opfs.html new file mode 100644 index 0000000000..495b0c2d8f --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-scope-omission.opfs.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/query-scope-omission.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/query-scope-omission.opfs.ts new file mode 100644 index 0000000000..14c2336b79 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/query-scope-omission.opfs.ts @@ -0,0 +1,44 @@ +import { + createBrowserWASQLitePersistence, + openBrowserWASQLiteOPFSDatabase, +} from '../src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { observeScope } from '../../query-db-collection/tests/scope-omission-reacquisition-oracle' +import { recordQueryHistory } from './query-ownership-browser-support' +import { + removeStartupOPFSArtifacts, + startupOPFSArtifacts, +} from './persisted-startup-opfs-cleanup' +import { removeClosedArtifacts } from './query-rejected-write-cutoff-cleanup' +import type { History } from '../../query-db-collection/tests/scope-omission-reacquisition-oracle' + +async function runScopeOmission(h: History, id: string) { + return recordQueryHistory(async (observe) => { + const name = 'ps-query-scope-omission.sqlite' + await removeStartupOPFSArtifacts(name) + const database = await openBrowserWASQLiteOPFSDatabase({ + databaseName: name, + }) + await withOracleCleanup( + () => + observeScope( + createBrowserWASQLitePersistence({ database }), + h, + id, + observe, + ), + [ + () => database.close?.(), + () => + removeClosedArtifacts( + () => removeStartupOPFSArtifacts(name), + observe, + ), + ], + ) + const artifacts = await startupOPFSArtifacts() + if (artifacts.length) + throw new Error('Scope OPFS artifacts remain: ' + artifacts.join(',')) + }) +} +Object.assign(window, { runScopeOmission }) diff --git a/packages/browser-db-sqlite-persistence/e2e/trailbase-bootstrap-error.html b/packages/browser-db-sqlite-persistence/e2e/trailbase-bootstrap-error.html new file mode 100644 index 0000000000..b7052fc5ca --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/trailbase-bootstrap-error.html @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/trailbase-eager.ts b/packages/browser-db-sqlite-persistence/e2e/trailbase-eager.ts new file mode 100644 index 0000000000..1d22dc0af6 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/trailbase-eager.ts @@ -0,0 +1,14 @@ +/** A real native-stream browser receiving path for the shared eager laws. */ +import { observeTrailBaseEagerHistory } from '../../trailbase-db-collection/tests/eager-live-source-framework-oracle' +import { trailBaseConformanceDriver } from '../../trailbase-db-collection/tests/conformance-driver' +import type { EagerLiveSourceHistory } from '../../db-adapter-conformance/src' + +export const runTrailBaseEager = ( + history: EagerLiveSourceHistory, + corruptValues = false, +) => + observeTrailBaseEagerHistory( + trailBaseConformanceDriver(), + history, + corruptValues, + ) diff --git a/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.html b/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.html new file mode 100644 index 0000000000..a108e8bdfa --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.ts b/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.ts new file mode 100644 index 0000000000..b4d15e8bc6 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.ts @@ -0,0 +1,37 @@ +/** Real OPFS receiver; the Vitest registration owns the shared judgment. */ +import { + createBrowserWASQLitePersistence, + openBrowserWASQLiteOPFSDatabase, +} from '../src' +import { observeHydrationLifecycle } from '../../trailbase-db-collection/tests/lifecycle-hydration-driver' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { + removeStartupOPFSArtifacts, + startupOPFSArtifacts, +} from './persisted-startup-opfs-cleanup' +import { runTrailBaseEager } from './trailbase-eager' +import type { HydrationLifecycleHistory } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' + +export async function runTrailBaseHydration( + history: HydrationLifecycleHistory, + id: string, +) { + const databaseName = `ps-trailbase-converted.sqlite` + await removeStartupOPFSArtifacts(databaseName) + const database = await openBrowserWASQLiteOPFSDatabase({ databaseName }) + const observation = await withOracleCleanup( + () => + observeHydrationLifecycle( + createBrowserWASQLitePersistence({ database }), + history, + id, + ), + [() => database.close?.(), () => removeStartupOPFSArtifacts(databaseName)], + ) + const artifacts = await startupOPFSArtifacts() + if (artifacts.length) + throw new Error(`OPFS artifacts remain: ${artifacts.join(`, `)}`) + return observation +} + +Object.assign(window, { runTrailBaseHydration, runTrailBaseEager }) diff --git a/packages/browser-db-sqlite-persistence/package.json b/packages/browser-db-sqlite-persistence/package.json index 5b7c1f3db4..b8e6bce2ef 100644 --- a/packages/browser-db-sqlite-persistence/package.json +++ b/packages/browser-db-sqlite-persistence/package.json @@ -24,6 +24,7 @@ "lint": "eslint . --fix", "test": "vitest --run", "test:oracles": "vitest --run --coverage.enabled=false tests/shared-driver-fairness-oracle.test.ts tests/opfs-page-lifecycle-oracle.test.ts tests/opfs-open-outcome-oracle.test.ts tests/opfs-worker-diagnostics-oracle.test.ts tests/opfs-request-order-oracle.test.ts tests/per-collection-coordinator-oracle.test.ts tests/persisted-options.test.ts", + "test:oracles:adapters-browser": "vitest --run --config vitest.adapter-oracles.config.ts", "test:opfs-fairness": "playwright test --config playwright.opfs.config.ts", "test:opfs-electric": "playwright test --config playwright.opfs-electric.config.ts", "test:e2e": "pnpm --filter @tanstack/db-ivm build && pnpm --filter @tanstack/db build && pnpm --filter @tanstack/db-sqlite-persistence-core build && pnpm --filter @tanstack/browser-db-sqlite-persistence build && vitest --config vitest.e2e.config.ts --run" diff --git a/packages/browser-db-sqlite-persistence/tests/adapter-chromium-host.ts b/packages/browser-db-sqlite-persistence/tests/adapter-chromium-host.ts new file mode 100644 index 0000000000..9c8445f6b1 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/tests/adapter-chromium-host.ts @@ -0,0 +1,131 @@ +/** Vitest controls a real Chromium page; no browser premise is simulated. */ +import { resolve } from 'node:path' +import { createServer } from 'vite' +import { chromium } from '@playwright/test' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import type { PluginOption } from 'vite' + +export async function startAdapterChromium({ + entry = `/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.html`, + ready = `runTrailBaseHydration`, + allow = [] as Array, + plugins = [] as Array, +} = {}) { + const root = resolve(import.meta.dirname, `../../..`) + const server = await createServer({ + configFile: resolve( + root, + `packages/browser-db-sqlite-persistence/vite.opfs.config.ts`, + ), + root, + plugins: [ + ...plugins, + { + name: `receiving-module-isolation`, + configResolved(config) { + // Vite's file-config merge drops inline nulls. Set this after that + // merge so worktree edits cannot reload an active receiving page. + config.server.watch = null + }, + }, + ], + // Discover only this receiving page's dependencies, not unrelated HTML + // projects elsewhere in the repository. + optimizeDeps: { + entries: [ + entry.startsWith(`/@fs/`) ? entry.slice(4) : resolve(root, `.${entry}`), + ], + }, + resolve: { + alias: { + '@tanstack/query-core': resolve( + root, + `packages/query-db-collection/node_modules/@tanstack/query-core`, + ), + }, + }, + server: { + host: `127.0.0.1`, + port: 0, + hmr: false, + fs: { allow: [root, ...allow] }, + headers: { + 'Cross-Origin-Opener-Policy': `same-origin`, + 'Cross-Origin-Embedder-Policy': `require-corp`, + }, + }, + }) + let browser: Awaited> | undefined + try { + await server.listen() + const channel = + process.env.PLAYWRIGHT_CHANNEL ?? (process.env.CI ? undefined : `chrome`) + browser = await chromium.launch({ + ...(channel ? { channel } : {}), + headless: true, + }) + const context = await browser.newContext() + const page = await context.newPage() + const errors: Array = [] + page.on(`pageerror`, (error) => errors.push(error.message)) + const address = server.httpServer!.address() + if (!address || typeof address === `string`) throw new Error(`No Vite port`) + await page.goto(`http://127.0.0.1:${address.port}${entry}`) + await page.waitForFunction((name) => name in window, ready) + if (errors.length) { + const failures = errors.map((message) => new Error(message)) + throw new AggregateError( + failures, + `Browser startup failed: ${errors.join(`; `)}`, + { + cause: failures[0], + }, + ) + } + return { + page, + errors, + watchedPaths: () => server.watcher.getWatched(), + async close() { + await withOracleCleanup(() => browser!.close(), [() => server.close()]) + }, + } + } catch (error) { + return withOracleCleanup(() => { + throw error + }, [() => browser?.close(), () => server.close()]) + } +} + +// Errors are append-only. A cursor consumes them once; diagnostics that arrive +// between histories are checked by the next one, with arrival-only attribution. +const consumedBrowserErrors = new WeakMap() + +/** + * Record one history's browser diagnostics without replacing its product + * result. The shared comparison judges that result first. An exception before + * a result remains primary and carries browser diagnostics as secondary. + */ +export async function captureBrowserHistory( + host: { errors: ReadonlyArray }, + run: () => Promise, +): Promise }> { + const offset = consumedBrowserErrors.get(host) ?? 0 + const takeErrors = () => { + const errors = host.errors.slice(offset) + consumedBrowserErrors.set(host, host.errors.length) + return errors + } + try { + const result = await run() + return { ...result, browserErrors: takeErrors() } + } catch (error) { + const browserErrors = takeErrors() + if (!browserErrors.length) throw error + throw new AggregateError( + [error, ...browserErrors.map((message) => new Error(message))], + `History failed; secondary browser errors: ${browserErrors.join(`; `)}`, + { cause: error }, + ) + } +} diff --git a/packages/browser-db-sqlite-persistence/tests/persisted-baseline-lifecycle-oracle.test.ts b/packages/browser-db-sqlite-persistence/tests/persisted-baseline-lifecycle-oracle.test.ts new file mode 100644 index 0000000000..cc7a4f79e9 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/tests/persisted-baseline-lifecycle-oracle.test.ts @@ -0,0 +1,66 @@ +// @vitest-environment node +import { resolve } from 'node:path' +import { afterAll, beforeAll } from 'vitest' +import { baselineCandidatePlugins } from '../../db-adapter-conformance/tests/persisted-baseline-candidate-plugins' +import { registerBaselineLifecycle } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle-suite' +import { + captureBrowserHistory, + startAdapterChromium, +} from './adapter-chromium-host' +import type { BaselineLifecycleObservation } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle' + +let host: Awaited> | undefined +beforeAll(async () => { + host = await startAdapterChromium({ + entry: `/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.html`, + ready: `runBaselineLifecycle`, + plugins: baselineCandidatePlugins(resolve(import.meta.dirname, `../../..`)), + }) +}, 120_000) +afterAll(async () => { + await host?.close() +}) +for (const { adapter, config } of [ + { + adapter: `rxdb`, + config: { + property: `adapter-conformance.persisted-baseline-lifecycle.rxdb.chromium-opfs`, + defaultRuns: 24, + streamFailure: true, + }, + }, + { + adapter: `powersync`, + config: { + property: `adapter-conformance.persisted-baseline-lifecycle.powersync.chromium-opfs`, + defaultRuns: 24, + streamFailure: false, + }, + }, +] as const) { + registerBaselineLifecycle(config, (history, id) => + captureBrowserHistory(host!, () => + host!.page.evaluate( + async ({ adapter: name, history: h, id: key, candidate }) => { + const receive = ( + window as unknown as { + runBaselineLifecycle: ( + adapter: typeof name, + history: typeof h, + id: string, + candidate: boolean, + ) => Promise + } + ).runBaselineLifecycle + return receive(name, h, key, candidate) + }, + { + adapter, + history, + id, + candidate: process.env.TANSTACK_DB_BASELINE_EXPECT_HYDRATE !== `0`, + }, + ), + ), + ) +} diff --git a/packages/browser-db-sqlite-persistence/tests/query-rejected-write-cutoff-browser-fidelity.test.ts b/packages/browser-db-sqlite-persistence/tests/query-rejected-write-cutoff-browser-fidelity.test.ts new file mode 100644 index 0000000000..0fbf66f687 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/tests/query-rejected-write-cutoff-browser-fidelity.test.ts @@ -0,0 +1,173 @@ +// Actual Chromium calibrations for the receiving transport, not product laws. +import { afterAll, beforeAll, expect, it } from 'vitest' +import { deliverQueryHistory } from '../e2e/query-ownership-browser-support' +import { + cutoffViolations, + obligations, +} from '../../query-db-collection/tests/persisted-rejected-write-cutoff-oracle' +import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { + captureBrowserHistory, + startAdapterChromium, +} from './adapter-chromium-host' +import type { + History, + Trace, +} from '../../query-db-collection/tests/persisted-rejected-write-cutoff-oracle' +import type { ReceiverFault } from '../e2e/query-rejected-write-cutoff.opfs' +import type { QueryReceivingResult } from '../e2e/query-ownership-browser-support' + +let browser: Awaited> | undefined +let sequence = 0 +const history: History = { + shape: 'raw', + write: 'during-update', + firstValue: 1, + nextValue: 2, + staleMs: 10, +} +beforeAll(async () => { + browser = await startAdapterChromium({ + entry: + '/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff.opfs.html', + ready: 'runCutoffCandidate', + }) +}, 120000) +afterAll(async () => { + await browser?.close() +}, 120000) +async function receive(fault?: ReceiverFault) { + if (!browser) throw new Error('Missing Chromium receiver') + const host = browser + const result = await captureBrowserHistory(host, () => + host.page.evaluate( + ({ h, id, kind }) => + ( + window as unknown as { + runCutoffCandidate: ( + h: History, + id: string, + fault?: ReceiverFault, + ) => Promise + } + ).runCutoffCandidate(h, id, kind), + { h: history, id: 'cutoff-fidelity-' + sequence++, kind: fault }, + ), + ) + const trace: Trace = [{ checkpoint: 'browserReceiver', value: true }] + let error: unknown + try { + deliverQueryHistory(result, (checkpoint, value) => + trace.push({ checkpoint, value }), + ) + } catch (thrown) { + error = thrown + } + return { result, trace, error, failures: cutoffViolations(history, trace) } +} +async function detached(message: string) { + if (!browser) throw new Error('Missing Chromium receiver') + const host = browser + const before = host.errors.length + await host.page.evaluate((text) => { + setTimeout(() => { + throw new Error(text) + }, 0) + }, message) + await untilStartup(() => host.errors.length > before) +} +it('rejects a bootstrap page error even when the entry becomes ready', async () => { + await expect( + startAdapterChromium({ + entry: + '/packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-bootstrap-error.html', + ready: 'runCutoffCandidate', + }), + ).rejects.toThrow('Browser startup failed') +}, 120000) +it('keeps a recorded product mismatch before a cleanup failure', async () => { + const captured = await receive('mismatch-cleanup') + expect(captured.failures[0]?.checkpoint).toBe('peerFetches') + expect(captured.error).toBeInstanceOf(Error) + expect((captured.error as Error).message).toBe('cutoff cleanup fault') + expect(captured.result.trace.some((o) => o.checkpoint === 'completed')).toBe( + true, + ) +}, 120000) +it('keeps the cleanup failure when the product trace passes', async () => { + const captured = await receive('cleanup') + expect(captured.failures).toEqual([]) + expect(captured.error).toBeInstanceOf(Error) + expect((captured.error as Error).message).toBe('cutoff cleanup fault') +}, 120000) +it('keeps a product mismatch before a detached browser error', async () => { + await detached('detached-product-neighbor') + const captured = await receive('mismatch-only') + expect(captured.failures[0]?.checkpoint).toBe('peerFetches') + expect(captured.error).toBeUndefined() + expect(captured.failures[0]?.difference).toContain( + 'detached-product-neighbor', + ) + expect(captured.result.browserErrors).toContain('detached-product-neighbor') +}, 120000) +it('reports a detached browser error against a clean product trace', async () => { + await detached('detached-clean-neighbor') + const captured = await receive() + expect(captured.failures[0]?.checkpoint).toBe('browserErrors') + expect(captured.failures[0]?.law).toBe('browser receiving fidelity') +}, 120000) +it('consumes between-history browser errors once', async () => { + await detached('between-history-error') + const first = await receive() + expect(first.result.browserErrors).toContain('between-history-error') + const second = await receive() + expect(second.result.browserErrors).toEqual([]) + expect(second.failures).toEqual([]) +}, 120000) + +it('keeps an earlier recorded count mismatch when later facts never arrive', async () => { + const captured = await receive('prefix-cleanup') + expect(captured.failures[0]?.checkpoint).toBe('firstFetches') + expect(captured.result.trace.some((o) => o.checkpoint === 'afterStale')).toBe( + false, + ) + expect(captured.error).toBeInstanceOf(Error) +}, 120000) +it('retries a transient busy cleanup and records it', async () => { + const captured = await receive('transient-busy') + expect(captured.error).toBeUndefined() + expect(captured.failures).toEqual([]) + expect( + captured.result.trace.filter((o) => o.checkpoint === 'opfsCleanupRetry'), + ).toHaveLength(1) +}, 120000) +it('preserves a permanent busy cleanup failure', async () => { + const captured = await receive('permanent-busy') + expect(captured.failures).toEqual([]) + expect(captured.error).toBeInstanceOf(Error) + expect((captured.error as Error).name).toBe('NoModificationAllowedError') + expect( + captured.result.trace.filter((o) => o.checkpoint === 'opfsCleanupRetry'), + ).toHaveLength(3) +}, 120000) +it('keeps the product mismatch when busy cleanup never succeeds', async () => { + const captured = await receive('mismatch-permanent-busy') + expect(captured.failures[0]?.checkpoint).toBe('peerFetches') + expect((captured.error as Error).name).toBe('NoModificationAllowedError') + expect( + captured.result.trace.filter((o) => o.checkpoint === 'opfsCleanupRetry'), + ).toHaveLength(3) +}, 120000) + +it('retains browser diagnostics beside a missing constructed state', async () => { + await detached('detached-constructed-neighbor') + const captured = await receive() + const built = { ...history } + obligations.set(built, ['stale-callback-delivered']) + const trace = captured.trace.filter( + (o) => o.checkpoint !== 'reach' || o.value !== 'stale-callback-delivered', + ) + const failures = cutoffViolations(built, trace) + expect(failures[0]?.checkpoint).toBe('constructed states') + expect(failures[0]?.difference).toContain('detached-constructed-neighbor') +}, 120000) diff --git a/packages/browser-db-sqlite-persistence/tests/trailbase-browser-fidelity.test.ts b/packages/browser-db-sqlite-persistence/tests/trailbase-browser-fidelity.test.ts new file mode 100644 index 0000000000..50b1f670e2 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/tests/trailbase-browser-fidelity.test.ts @@ -0,0 +1,205 @@ +// @vitest-environment node +/** Supplemental recorder calibration; all product answers use shared checks. */ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, beforeAll, expect, it } from 'vitest' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { hydrationLifecycleVerdict } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle-suite' +import { + constructTrailBaseEagerHistory, + trailBaseEagerVerdict, +} from '../../trailbase-db-collection/tests/eager-live-source-framework-oracle' +import { + captureBrowserHistory, + startAdapterChromium, +} from './adapter-chromium-host' +import type { HydrationLifecycleObservation } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' +import type { EagerHistoryResult } from '../../trailbase-db-collection/tests/eager-live-source-framework-oracle' +import type { EagerLiveSourceHistory } from '../../db-adapter-conformance/src' + +let host: Awaited> | undefined +beforeAll(async () => { + host = await startAdapterChromium() +}, 120000) +afterAll(async () => { + await host?.close() +}) +let sequence = 0 +async function emitBrowserError() { + const received = host!.page.waitForEvent(`pageerror`) + await host!.page.evaluate(() => { + setTimeout(() => { + throw new Error(`calibrated detached browser error`) + }, 0) + }) + await received +} + +it(`preserves hydration failure and limits browser errors to their history`, async () => { + const history = { + previous: 3, + old: 1, + replacement: 6, + oldList: `complete` as const, + resumeAt: `replacement-ready` as const, + initialRender: `default` as const, + } + const receive = (corruptRows: boolean, pageError: boolean) => + captureBrowserHistory(host!, async () => { + const raw = await host!.page.evaluate( + async ({ history: h, id }) => { + return ( + window as unknown as { + runTrailBaseHydration: ( + history: typeof h, + id: string, + ) => Promise + } + ).runTrailBaseHydration(h, id) + }, + { history, id: `fidelity-${sequence++}` }, + ) + if (pageError) await emitBrowserError() + return corruptRows + ? { + ...raw, + retiredResume: { ...raw.retiredResume, rows: [] }, + } + : raw + }) + const judge = (value: Awaited>) => + hydrationLifecycleVerdict(history, [{ checkpoint: `completed`, value }], []) + const primary = judge(await receive(true, false)) + const simultaneous = judge(await receive(true, true)) + expect(primary.kind).toBe(`violation`) + expect(simultaneous).toMatchObject({ + kind: primary.kind, + ...(primary.kind === `violation` + ? { law: primary.law, checkpoint: primary.checkpoint } + : {}), + }) + if (simultaneous.kind === `violation`) + expect(simultaneous.difference).toContain( + `secondary browser errors: calibrated detached browser error`, + ) + expect(judge(await receive(false, true))).toMatchObject({ + kind: `violation`, + law: `detached-browser-error`, + checkpoint: `browser`, + }) + await emitBrowserError() + expect(judge(await receive(false, false))).toMatchObject({ + kind: `violation`, + law: `detached-browser-error`, + checkpoint: `browser`, + }) + expect(judge(await receive(false, false))).toEqual({ kind: `pass` }) +}, 120000) + +it(`preserves eager failure and limits browser errors to their history`, async () => { + const history = constructTrailBaseEagerHistory([], 0) + const receive = (corruptValues: boolean, pageError: boolean) => + captureBrowserHistory(host!, async () => { + const raw = await host!.page.evaluate( + async ({ history: h, corruptValues: corrupt }) => { + return ( + window as unknown as { + runTrailBaseEager: ( + history: EagerLiveSourceHistory, + corrupt: boolean, + ) => Promise + } + ).runTrailBaseEager(h, corrupt) + }, + { history, corruptValues }, + ) + if (pageError) await emitBrowserError() + return raw + }) + const judge = (value: Awaited>) => + trailBaseEagerVerdict([{ checkpoint: `completed`, value }], []) + const primary = judge(await receive(true, false)) + const simultaneous = judge(await receive(true, true)) + expect(primary.kind).toBe(`violation`) + expect(simultaneous).toMatchObject({ + kind: primary.kind, + ...(primary.kind === `violation` + ? { law: primary.law, checkpoint: primary.checkpoint } + : {}), + }) + if (simultaneous.kind === `violation`) + expect(simultaneous.difference).toContain( + `secondary browser errors: calibrated detached browser error`, + ) + expect(judge(await receive(false, true))).toMatchObject({ + kind: `violation`, + law: `detached-browser-error`, + checkpoint: `browser`, + }) + await emitBrowserError() + expect(judge(await receive(false, false))).toMatchObject({ + kind: `violation`, + law: `detached-browser-error`, + checkpoint: `browser`, + }) + expect(judge(await receive(false, false))).toEqual({ kind: `pass` }) +}, 120000) + +it(`rejects a browser error during receiver startup`, async () => { + await expect( + startAdapterChromium({ + entry: `/packages/browser-db-sqlite-persistence/e2e/trailbase-bootstrap-error.html`, + }), + ).rejects.toThrow(`Browser startup failed: calibrated bootstrap error`) +}, 120000) + +it(`keeps loaded receiving modules stable when an imported file changes`, async () => { + const directory = await mkdtemp(join(tmpdir(), `db-receiving-isolation-`)) + let isolated: Awaited> | undefined + await withOracleCleanup(async () => { + const imported = join(directory, `value.js`) + await Promise.all([ + writeFile( + join(directory, `index.html`), + ``, + ), + writeFile( + join(directory, `entry.js`), + `import { value } from './value.js'; window.receivingFixtureValue = value`, + ), + writeFile(imported, `export const value = 1`), + ]) + isolated = await startAdapterChromium({ + entry: `/@fs${directory}/index.html`, + ready: `receivingFixtureValue`, + allow: [directory], + }) + // This positive watcher observation prevents a delayed reload from making + // a short negative browser check pass when watching is still enabled. + expect(isolated.watchedPaths()).toEqual({}) + await isolated.page.evaluate(() => { + Object.assign(window, { receivingFixtureIdentity: `original-page` }) + }) + await writeFile(imported, `export const value = 2`) + expect(await readFile(imported, `utf8`)).toContain(`value = 2`) + await isolated.page.evaluate( + () => + new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())) + }), + ) + expect( + await isolated.page.evaluate(() => { + const fixture = window as unknown as { + receivingFixtureValue: number + receivingFixtureIdentity: string + } + return [fixture.receivingFixtureValue, fixture.receivingFixtureIdentity] + }), + ).toEqual([1, `original-page`]) + }, [ + () => isolated?.close(), + () => rm(directory, { recursive: true, force: true }), + ]) +}, 120000) diff --git a/packages/browser-db-sqlite-persistence/tests/trailbase-eager-live-source-oracle.test.ts b/packages/browser-db-sqlite-persistence/tests/trailbase-eager-live-source-oracle.test.ts new file mode 100644 index 0000000000..bffc69ce51 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/tests/trailbase-eager-live-source-oracle.test.ts @@ -0,0 +1,42 @@ +// @vitest-environment node +import { afterAll, beforeAll } from 'vitest' +import { registerTrailBaseEager } from '../../trailbase-db-collection/tests/eager-live-source-oracle-suite' +import { trailBaseConformanceDriver } from '../../trailbase-db-collection/tests/conformance-driver' +import { + captureBrowserHistory, + startAdapterChromium, +} from './adapter-chromium-host' +import type { EagerLiveSourceHistory } from '../../db-adapter-conformance/src' +import type { EagerHistoryResult } from '../../trailbase-db-collection/tests/eager-live-source-framework-oracle' + +const property = `adapter-conformance.eager-live-source.trailbase.chromium` + +let host: Awaited> | undefined +beforeAll(async () => { + host = await startAdapterChromium({ ready: `runTrailBaseEager` }) +}, 120000) +afterAll(async () => { + await host?.close() +}) +registerTrailBaseEager( + { property, defaultRuns: 600 }, + trailBaseConformanceDriver(), + async (history, corruptValues) => { + return captureBrowserHistory(host!, () => + host!.page.evaluate( + async ({ history: h, corruptValues: corrupt }) => { + const receive = ( + window as unknown as { + runTrailBaseEager: ( + history: EagerLiveSourceHistory, + corruptValues: boolean, + ) => Promise + } + ).runTrailBaseEager + return receive(h, corrupt ?? false) + }, + { history, corruptValues }, + ), + ) + }, +) diff --git a/packages/browser-db-sqlite-persistence/tests/trailbase-hydration-oracle.test.ts b/packages/browser-db-sqlite-persistence/tests/trailbase-hydration-oracle.test.ts new file mode 100644 index 0000000000..2e3074d7f7 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/tests/trailbase-hydration-oracle.test.ts @@ -0,0 +1,36 @@ +// @vitest-environment node +import { afterAll, beforeAll } from 'vitest' +import { registerHydrationLifecycle } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle-suite' +import { + captureBrowserHistory, + startAdapterChromium, +} from './adapter-chromium-host' +import type { HydrationLifecycleObservation } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' + +let host: Awaited> | undefined +beforeAll(async () => { + host = await startAdapterChromium() +}, 120000) +afterAll(async () => { + await host?.close() +}) +const property = `trailbase.lifecycle.persisted-hydration.chromium-opfs` + +registerHydrationLifecycle(property, async (history, id) => { + return captureBrowserHistory(host!, () => + host!.page.evaluate( + async ({ history: h, id: name }) => { + const receive = ( + window as unknown as { + runTrailBaseHydration: ( + history: typeof h, + id: string, + ) => Promise + } + ).runTrailBaseHydration + return receive(h, name) + }, + { history, id }, + ), + ) +}) diff --git a/packages/browser-db-sqlite-persistence/tsconfig.json b/packages/browser-db-sqlite-persistence/tsconfig.json index 647a24d72a..f737e3816a 100644 --- a/packages/browser-db-sqlite-persistence/tsconfig.json +++ b/packages/browser-db-sqlite-persistence/tsconfig.json @@ -30,6 +30,7 @@ "vite.config.ts", "vite.opfs.config.ts", "vitest.e2e.config.ts", + "vitest.adapter-oracles.config.ts", "playwright.opfs.config.ts", "playwright.opfs-electric.config.ts" ], diff --git a/packages/browser-db-sqlite-persistence/vite.config.ts b/packages/browser-db-sqlite-persistence/vite.config.ts index 878290d98b..3378cb2acc 100644 --- a/packages/browser-db-sqlite-persistence/vite.config.ts +++ b/packages/browser-db-sqlite-persistence/vite.config.ts @@ -1,12 +1,19 @@ import { readFileSync } from 'node:fs' import { createRequire } from 'node:module' -import { defineConfig, mergeConfig } from 'vitest/config' +import { configDefaults, defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' import packageJson from './package.json' import type { Plugin } from 'vite' const waSqliteWasmImport = `@journeyapps/wa-sqlite/dist/wa-sqlite.wasm?url&no-inline` const waSqliteWasmModule = `\0tanstack-wa-sqlite-wasm-url` +const adapterBrowserTests = [ + `tests/trailbase-hydration-oracle.test.ts`, + `tests/trailbase-eager-live-source-oracle.test.ts`, + `tests/trailbase-browser-fidelity.test.ts`, + `tests/query-rejected-write-cutoff-browser-fidelity.test.ts`, + `tests/persisted-baseline-lifecycle-oracle.test.ts`, +] function packageWaSqliteWasm(): Plugin { return { @@ -93,11 +100,14 @@ const config = defineConfig({ test: { name: packageJson.name, include: [`tests/**/*.test.ts`], + // These properties launch real Chromium in the dedicated browser CI group. + exclude: [...configDefaults.exclude, ...adapterBrowserTests], environment: `node`, coverage: { enabled: true, provider: `istanbul`, include: [`src/**/*`] }, typecheck: { enabled: true, include: [`tests/**/*.test.ts`, `tests/**/*.test-d.ts`], + exclude: adapterBrowserTests, }, }, }) diff --git a/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts b/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts new file mode 100644 index 0000000000..601d6ed0b2 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts @@ -0,0 +1,41 @@ +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' + +const packageDirectory = dirname(fileURLToPath(import.meta.url)) + +// These Vitest properties still drive real Chromium and OPFS. They run in the +// adapter-browser CI group, after Playwright installs its Chromium runtime. +export default defineConfig({ + root: packageDirectory, + resolve: { + alias: { + '@tanstack/db': resolve(packageDirectory, `../db/src`), + '@tanstack/db-ivm': resolve(packageDirectory, `../db-ivm/src`), + '@tanstack/db-sqlite-persistence-core': resolve( + packageDirectory, + `../db-sqlite-persistence-core/src`, + ), + '@tanstack/query-db-collection': resolve( + packageDirectory, + `../query-db-collection/src`, + ), + }, + }, + test: { + name: `adapter-oracles-chromium`, + include: [ + `tests/trailbase-hydration-oracle.test.ts`, + `tests/trailbase-eager-live-source-oracle.test.ts`, + `tests/trailbase-browser-fidelity.test.ts`, + `tests/query-rejected-write-cutoff-browser-fidelity.test.ts`, + `tests/persisted-baseline-lifecycle-oracle.test.ts`, + `e2e/query-*-oracle.browser.test.ts`, + ], + environment: `jsdom`, + fileParallelism: false, + testTimeout: 120_000, + hookTimeout: 120_000, + coverage: { enabled: false }, + }, +}) diff --git a/packages/db-adapter-conformance/MIGRATION.md b/packages/db-adapter-conformance/MIGRATION.md index 882beb12f2..54cfec34fe 100644 --- a/packages/db-adapter-conformance/MIGRATION.md +++ b/packages/db-adapter-conformance/MIGRATION.md @@ -14,16 +14,16 @@ reviewed change. ## Oracle families -| Family | Status | Laws | -| ------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| Eager live source | Built (`src/eager-live-source-oracle.ts`) | READY-2/3/4/8/10, HEALTH-1/2/5, PUB-1/2/3/5/7/9, FID-1/4, LIFE-2/5/10, EVID-1/2/3, RETRY-1 (eager), RECON-1, RENEW-1 | -| Mutations and confirmation | Needed | MUT-1/2/4/5/7/10/13, FID-5 | -| On-demand loading | Needed | DEM-1 to DEM-23, MAP-1 to MAP-4 | -| Reload and retry | Needed | RELOAD-1 to RELOAD-5; RETRY-1 and HEALTH-2 beyond the eager source's retried failures | -| Client cache and refetch | Needed | CACHE-1 to CACHE-8, REF-1 to REF-7, DIR-1 to DIR-5 | -| Source shapes | Needed | RESET-1, RESUME-1 to RESUME-3, PROG-1, MEMB-1/2, PAGE-1 to PAGE-3, OWN-1/2, META-1 | -| Local and shared stores | Needed | SELF-1 to SELF-3, STORE-1 to STORE-4, PEER-1 | -| Persistence wrapper and storage hosts | Startup replacement built (`src/persisted-startup-oracle.ts`); other laws needed | `P-*`, `S-*` | +| Family | Status | Laws | +| ------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| Eager live source | Built (`src/eager-live-source-oracle.ts`) | READY-2/3/4/8/10, HEALTH-1/2/5, PUB-1/2/3/5/7/9, FID-1/4, LIFE-2/5/10, EVID-1/2/3, RETRY-1 (eager), RECON-1, RENEW-1 | +| Mutations and confirmation | Needed | MUT-1/2/4/5/7/10/13, FID-5 | +| On-demand loading | Needed | DEM-1 to DEM-23, MAP-1 to MAP-4 | +| Reload and retry | Needed | RELOAD-1 to RELOAD-5; RETRY-1 and HEALTH-2 beyond the eager source's retried failures | +| Client cache and refetch | Needed | CACHE-1 to CACHE-8, REF-1 to REF-7, DIR-1 to DIR-5 | +| Source shapes | Needed | RESET-1, RESUME-1 to RESUME-3, PROG-1, MEMB-1/2, PAGE-1 to PAGE-3, OWN-1/2, META-1 | +| Local and shared stores | Needed | SELF-1 to SELF-3, STORE-1 to STORE-4, PEER-1 | +| Persistence wrapper and storage hosts | Startup replacement built (`tests/persisted-startup-oracle.ts`); other laws needed | `P-*`, `S-*` | ## Adapters @@ -38,7 +38,7 @@ reviewed change. | query-db | client cache and refetch, on-demand, mutations, reload and retry, persistence | Persistence startup: GREEN (2026-11-09), all five origins, scoped peers, controlled/Node SQLite/OPFS. Other families not started; not a push source, so the eager family does not apply | | local-only | local and shared stores, mutations | Not started | | localStorage | local and shared stores, mutations | Not started | -| Persisted wrapper | persistence wrapper | Startup replacement: GREEN through Query and Convex; other laws not started | +| Persisted wrapper | persistence wrapper | Startup replacement: GREEN through Query, Convex, Electric, TrailBase, RxDB and PowerSync on controlled persistence, Node SQLite and Chromium worker/OPFS. RxDB/PowerSync also receive startup work and hydration lifecycle laws; other laws retain their owners | | Storage hosts (8) | storage hosts | Startup receivers: core SQLite on Node and Browser worker/OPFS; remaining host/path laws not started | ## Suggested order diff --git a/packages/db-adapter-conformance/README.md b/packages/db-adapter-conformance/README.md index e2a3ba08ee..46983aefda 100644 --- a/packages/db-adapter-conformance/README.md +++ b/packages/db-adapter-conformance/README.md @@ -59,12 +59,14 @@ Implement `EagerLiveSourceDriver`: - **`observedView`:** if your rows or keys differ from the conformance row, wrap the Collection with `observedView` and a one-to-one mapping. `conformanceKey` maps string keys; `toSqlRow` and `fromSqlRow` map an absent `note` to and from SQL `NULL`. Declare only the capabilities your adapter supports; the grammar generates -only the steps they allow. Then run `eagerLiveSourceProperty(driver, reach)` -with `fc.assert`. `reach` is a `ReachCounter` (`beginHistory`, `reach`); -count the states `reachStatesFor(driver)` lists and fail the run when one -was reached fewer than five times, so histories that miss a state the laws -need do not pass silently. The in-repo campaigns use `createOracleReach` -from `packages/db/tests/oracle-reach.ts`. +only the steps they allow. New and converted receivers register through +`oracleProperty`. The shared `tests/eager-live-source-oracle-suite.ts` registers +the fixed and fresh campaigns, with randomized constructed histories for every +required state. Each campaign requires at least five constructions per state +and checks generated constructions separately. RxDB, PowerSync and TrailBase +use this path; their conversion preserves the existing laws and budgets. +See the [conversion evidence](../../docs/contributing/oracle-reviews/2026-11-10-eager-source-primary-conversions.md). +Remaining legacy receivers keep their listed compatibility path until conversion. ## Persisted startup conformance @@ -86,7 +88,8 @@ owner's one-row-read law. A scoped adapter checks exact source/peer ownership. Query receives the shared property with five cache/fetch origins, ownerless saved rows, peer ownership, and peer-only overlaps. Convex, Electric, TrailBase, RxDB and PowerSync also receive it through their installed SDK or controlled provider boundary. Chromium worker/OPFS runs -116 Query histories and 26 histories for each other adapter. The browser repeats +116 Query histories, 104 PowerSync histories, 32 RxDB histories and 26 histories +for each of Convex, Electric and TrailBase. The browser repeats these histories in one profile and removes fixture-owned OPFS files. It also runs 22 protocol-admission histories for each of RxDB and PowerSync. Every receiver crosses ordinary and network-first rendering, empty and nonempty baselines, @@ -97,6 +100,20 @@ persistence capability protocol through the real RxDB/PowerSync sync boundary. The persistence-loss calibration wraps hydration-scoped adapters, requires an actual dropped write, and fails if its clean candidate fails. +PowerSync's first persisted startup keeps source commits bounded by +`syncBatchSize`; RxDB keeps its existing full-load source commit. After +hydration, both delete only saved omissions in bounded commits. The work +judgment rejects startup snapshot replacement and truncate, excess source +writes and excess deletes. PowerSync also crosses provider IDs transformed +by deserialization. Retained nonempty reloads keep atomic snapshots. +See the [batching evidence](../../docs/contributing/oracle-reviews/2026-11-10-persisted-startup-batching.md). + +`tests/persisted-baseline-lifecycle-oracle.ts` separately judges cleanup, +reload and terminal source failure while the hydration continuation is held. +RxDB and PowerSync receive it on controlled persistence, Node SQLite and actual +Chromium worker/OPFS, using the same model and comparison. See the +[lifecycle evidence](../../docs/contributing/oracle-reviews/2026-11-10-persisted-baseline-lifecycle.md). + Package-local RxDB/PowerSync registrations also invoke the shared capability suite, so package-scoped test discovery and mutation audits include the protocol owner. The 22-history grammar and judgment stay in this package. diff --git a/packages/db-adapter-conformance/src/eager-live-source-oracle.ts b/packages/db-adapter-conformance/src/eager-live-source-oracle.ts index 779c52d60d..b1045fd3c1 100644 --- a/packages/db-adapter-conformance/src/eager-live-source-oracle.ts +++ b/packages/db-adapter-conformance/src/eager-live-source-oracle.ts @@ -557,9 +557,17 @@ function valueArb( if (examples.length === 0) { throw new Error(`every ${kind} example is excluded`) } - return fc - .integer({ min: 0, max: examples.length - 1 }) - .map((index) => examples[index]!()) + return fc.integer({ min: 0, max: examples.length - 1 }).map( + (index) => examples[index]!(), + (value) => { + const index = examples.findIndex( + (example) => describeValue(example()) === describeValue(value), + ) + if (index < 0) + throw new Error(`value is outside the declared examples`) + return index + }, + ) }), ) } @@ -600,19 +608,54 @@ function changeArb( excludes?: ValueExclusion, ): fc.Arbitrary { const changes: Array> = [ - rowArb(kinds, fieldKinds, excludes).map((row) => ({ - type: `upsert` as const, - row, - })), - fc - .integer({ min: 1, max: 5 }) - .map((id) => ({ type: `delete` as const, id })), + rowArb(kinds, fieldKinds, excludes).map( + (row) => ({ + type: `upsert` as const, + row, + }), + (value) => { + if ( + !value || + typeof value !== `object` || + !(`type` in value) || + value.type !== `upsert` || + !(`row` in value) + ) + throw new Error(`not an upsert`) + return value.row as ConformanceRow + }, + ), + fc.integer({ min: 1, max: 5 }).map( + (id) => ({ type: `delete` as const, id }), + (value) => { + if ( + !value || + typeof value !== `object` || + !(`type` in value) || + value.type !== `delete` || + !(`id` in value) + ) + throw new Error(`not a delete`) + return value.id as number + }, + ), ] if (kinds.length > 0 || fieldKinds.length > 0) { changes.push( - fc - .integer({ min: 1, max: 5 }) - .map((id) => ({ type: `retouch` as const, id })), + fc.integer({ min: 1, max: 5 }).map( + (id) => ({ type: `retouch` as const, id }), + (value) => { + if ( + !value || + typeof value !== `object` || + !(`type` in value) || + value.type !== `retouch` || + !(`id` in value) + ) + throw new Error(`not a retouch`) + return value.id as number + }, + ), ) } return fc.oneof(...changes) @@ -630,6 +673,21 @@ export type GrammarDomain = Pick< | `rejectedDelivery` > +/** Inverse maps admit externally constructed histories without changing draws. */ +function declaredStep( + value: unknown, + kind: K, +): Extract { + if ( + !value || + typeof value !== `object` || + !(`kind` in value) || + value.kind !== kind + ) + throw new Error(`not a ${kind} step`) + return value as Extract +} + /** Generates histories whose steps use only the driver's declarations. */ export function eagerLiveSourceHistoryArb( domain: GrammarDomain, @@ -654,10 +712,18 @@ export function eagerLiveSourceHistoryArb( ), seen: fc.integer({ min: 0, max: 3 }), }) - .map(({ rows, seen }) => - capabilities.has(`listen-before-fetch`) - ? { kind: `snapshot` as const, rows, seen } - : { kind: `snapshot` as const, rows }, + .map( + ({ rows, seen }) => + capabilities.has(`listen-before-fetch`) + ? { kind: `snapshot` as const, rows, seen } + : { kind: `snapshot` as const, rows }, + (value) => { + const step = declaredStep(value, `snapshot`) + return { + rows: step.rows as Array, + seen: step.seen ?? 0, + } + }, ), }, { @@ -666,14 +732,34 @@ export function eagerLiveSourceHistoryArb( .array(changeArb(kinds, fieldKinds, domain.excludesValue), { maxLength: 3, }) - .map((changes) => ({ kind: `changes` as const, changes })), + .map( + (changes) => ({ kind: `changes` as const, changes }), + (value) => + ({ ...declaredStep(value, `changes`) }) + .changes as Array, + ), + }, + { + weight: 2, + arbitrary: fc.constant(null).map( + () => ({ kind: `fail` as const }), + (value) => { + declaredStep(value, `fail`) + return null + }, + ), }, - { weight: 2, arbitrary: fc.constant({ kind: `fail` as const }) }, { weight: 1, arbitrary: capabilities.has(`prompt-after-restart`) ? fc.boolean().map((prompt) => ({ kind: `restart` as const, prompt })) - : fc.constant({ kind: `restart` as const }), + : fc.constant(null).map( + () => ({ kind: `restart` as const }), + (value) => { + declaredStep(value, `restart`) + return null + }, + ), }, ] if (capabilities.has(`duplicate-key-delivery`)) { @@ -691,11 +777,20 @@ export function eagerLiveSourceHistoryArb( ), duplicate: fc.nat(2), }) - .map(({ rows, duplicate }) => ({ - kind: `malformed` as const, - rows, - duplicate, - })), + .map( + ({ rows, duplicate }) => ({ + kind: `malformed` as const, + rows, + duplicate, + }), + (value) => { + const step = declaredStep(value, `malformed`) + return { + rows: step.rows as Array, + duplicate: step.duplicate, + } + }, + ), }) } if (capabilities.has(`await-evidence`)) { @@ -808,7 +903,14 @@ export function eagerLiveSourceHistoryArb( fc.oneof( ...steps.map(({ weight, arbitrary }) => ({ weight, - arbitrary: arbitrary.map((step) => [step]), + arbitrary: arbitrary.map( + (step) => [step], + (value) => { + if (!Array.isArray(value) || value.length !== 1) + throw new Error(`not one step`) + return value[0] as EagerLiveSourceStep + }, + ), })), ...outages.map((arbitrary, index) => ({ weight: index === 0 ? 3 : 1, @@ -817,7 +919,13 @@ export function eagerLiveSourceHistoryArb( ), { minLength: 1, maxLength: 12 }, ) - .map((elements) => elements.flat()) + .map( + (elements) => elements.flat(), + (value) => { + if (!Array.isArray(value)) throw new Error(`not a history`) + return value.map((step: EagerLiveSourceStep) => [step]) + }, + ) // EVID-2 and a prompt restart after a failure need sequences that random // steps rarely produce, so a history can open with one: no prefix weighs 5 // and each declared scenario 1, so with one scenario one history in six @@ -879,10 +987,24 @@ export function eagerLiveSourceHistoryArb( minLength: 1, maxLength: 3, }) - .map((changes) => ({ kind: `changes` as const, changes })) - return fc - .tuple(fc.array(startup, { maxLength: 3 }), history) - .map(([burst, rest]) => [...burst, ...rest]) + .map( + (changes) => ({ kind: `changes` as const, changes }), + (value) => declaredStep(value, `changes`).changes as Array, + ) + return fc.tuple(fc.array(startup, { maxLength: 3 }), history).map( + ([burst, rest]) => [...burst, ...rest], + (value) => { + if (!Array.isArray(value)) throw new Error(`not a history`) + // A startup burst contributes at most three steps before the ordinary + // twelve-step grammar. This inverse changes admission, never draws. + const burstLength = Math.max(0, value.length - 12) + if (burstLength > 3) throw new Error(`startup burst is too long`) + return [value.slice(0, burstLength), value.slice(burstLength)] as [ + Array<{ kind: `changes`; changes: Array }>, + Array, + ] + }, + ) } // --------------------------------------------------------------------------- diff --git a/packages/db-adapter-conformance/tests/eager-live-source-framework-oracle.ts b/packages/db-adapter-conformance/tests/eager-live-source-framework-oracle.ts new file mode 100644 index 0000000000..361c54a67a --- /dev/null +++ b/packages/db-adapter-conformance/tests/eager-live-source-framework-oracle.ts @@ -0,0 +1,128 @@ +/** + * The shared eager-source checker remains the independent judge at every + * original checkpoint. This bridge transports its law and checkpoint to the + * framework. Setup errors escape; cleanup diagnostics remain secondary. + */ +import { ConformanceViolation, checkEagerLiveSourceHistory } from '../src' +import type { + EagerLiveSourceDriver, + EagerLiveSourceHistory, + EagerLiveSourceReachState, +} from '../src' +import type { Observation, Verdict } from '../../db/tests/oracle-property' + +export type EagerHistoryResult = { + browserErrors?: ReadonlyArray + reached: Array + result?: { accepted: number; skipped: number } + failure?: { + law: string + step: number + expected: unknown + actual: unknown + cleanup: Array + } +} + +export async function observeEagerLiveSourceHistory( + driver: EagerLiveSourceDriver, + history: EagerLiveSourceHistory, + corruptValues = false, +): Promise { + const reached: Array = [] + const actualDriver = corruptValues + ? { + ...driver, + create: () => { + const fixture = driver.create() + const collection = fixture.collection + return { + ...fixture, + collection: { + ...collection, + get status() { + return collection.status + }, + get health() { + return collection.health + }, + *values() { + for (const row of collection.values()) + yield { ...row, value: { incorrect: true } } + }, + }, + } + }, + } + : driver + try { + const result = await checkEagerLiveSourceHistory(actualDriver, history, { + reach: (state) => reached.push(state), + }) + return { reached, result } + } catch (error) { + const primary = error instanceof AggregateError ? error.cause : error + if (!(primary instanceof ConformanceViolation)) throw error + return { + reached, + failure: { + law: primary.law, + step: primary.step, + expected: primary.expected, + actual: primary.actual, + cleanup: + error instanceof AggregateError ? error.errors.map(String) : [], + }, + } + } +} + +export function eagerLiveSourceVerdict( + trace: ReadonlyArray, + required: ReadonlyArray, +): Verdict { + const completed = trace.find((entry) => entry.checkpoint === `completed`) + const actual = completed?.value as EagerHistoryResult | undefined + const browserErrors = actual?.browserErrors ?? [] + const secondary = browserErrors.length + ? `; secondary browser errors: ${browserErrors.join(`; `)}` + : `` + if (actual?.failure) { + const { law, step, expected, actual: observed, cleanup } = actual.failure + return { + kind: `violation`, + checkpoint: `step:${step}`, + law, + difference: `${law} at original shared checkpoint ${step}${cleanup.length ? `; secondary cleanup: ${cleanup.join(`; `)}` : ``}${secondary}`, + values: { expected, actual: observed }, + } + } + const reached = new Set( + trace + .filter((entry) => entry.checkpoint === `reach`) + .map((entry) => entry.value), + ) + const missing = required.filter((state) => !reached.has(state)) + if (missing.length) + return { + kind: `violation`, + checkpoint: `reach`, + law: `constructed builder reach`, + difference: `generated construction omitted ${missing.join(`, `)}${secondary}`, + } + if (!actual?.result) + return { + kind: `violation`, + checkpoint: `completed`, + law: `eager source comparison`, + difference: `shared comparison did not complete${secondary}`, + } + if (browserErrors.length) + return { + kind: `violation`, + checkpoint: `browser`, + law: `detached-browser-error`, + difference: browserErrors.join(`; `), + } + return { kind: `pass` } +} diff --git a/packages/db-adapter-conformance/tests/eager-live-source-oracle-suite.ts b/packages/db-adapter-conformance/tests/eager-live-source-oracle-suite.ts new file mode 100644 index 0000000000..4303058d60 --- /dev/null +++ b/packages/db-adapter-conformance/tests/eager-live-source-oracle-suite.ts @@ -0,0 +1,87 @@ +/** + * Receiving campaigns retain the shared Map/phase model and each original + * public checkpoint. Five randomized builders enforce minimum-five reach. + * Their local obligation ledger also checks fresh generated constructions: + * the pinned framework only associates its seed-derived objects with enters. + */ +import { eagerLiveSourceHistoryArb, reachStatesFor } from '../src' +import { + eagerLiveSourceVerdict, + observeEagerLiveSourceHistory, +} from './eager-live-source-framework-oracle' +import type { EagerLiveSourceDriver, EagerLiveSourceHistory } from '../src' +import type { Observation } from '../../db/tests/oracle-property' + +export function eagerLiveSourceCampaign( + driver: EagerLiveSourceDriver, + construct: ( + generated: EagerLiveSourceHistory, + salt: number, + ) => EagerLiveSourceHistory, +) { + const states = reachStatesFor(driver) + const required = new WeakMap< + EagerLiveSourceHistory, + { salt: number; states: ReadonlyArray } + >() + const constructed = Array.from({ length: 5 }, (_, salt) => ({ + name: `all eager states, randomized construction ${salt + 1}`, + enters: states, + build: (generated: EagerLiveSourceHistory) => { + const history = construct(generated, salt) + required.set(history, { salt, states }) + return history + }, + })) + const drive = + (corruptValues = false) => + async ( + history: EagerLiveSourceHistory, + observe: (checkpoint: string, value: unknown) => void, + ) => { + const result = await observeEagerLiveSourceHistory( + driver, + history, + corruptValues, + ) + for (const state of result.reached) observe(`reach`, state) + observe(`completed`, result) + } + return { + arbitrary: eagerLiveSourceHistoryArb(driver), + states, + constructed, + model: (history: EagerLiveSourceHistory) => ({ + required: required.get(history)?.states ?? [], + }), + driver: drive(), + check: ( + _history: EagerLiveSourceHistory, + trace: ReadonlyArray, + expected: { required: ReadonlyArray }, + ) => eagerLiveSourceVerdict(trace, expected.required), + calibrations: [ + { + name: `shared value judgment rejects wrong public values`, + law: `FID-1 (values)`, + driver: drive(true), + }, + ...constructed.flatMap((_builder, salt) => + states.map((state) => ({ + name: `construction ${salt + 1} omits ${state}`, + law: `constructed builder reach`, + corrupt: ( + trace: ReadonlyArray, + history: EagerLiveSourceHistory, + ) => + required.get(history)?.salt === salt + ? trace.filter( + (entry) => + !(entry.checkpoint === `reach` && entry.value === state), + ) + : undefined, + })), + ), + ], + } +} diff --git a/packages/db-adapter-conformance/tests/eager-reload-driver.ts b/packages/db-adapter-conformance/tests/eager-reload-driver.ts index edfbe32a98..d05c4d701b 100644 --- a/packages/db-adapter-conformance/tests/eager-reload-driver.ts +++ b/packages/db-adapter-conformance/tests/eager-reload-driver.ts @@ -35,6 +35,18 @@ export type EagerReloadAdapter = { open: (initial: Array, id: string) => Promise } +// With no optimistic transaction, the external gate's release order has no +// contract meaning. Canonicalize only that absent-mutation fact; every observed +// mutation=true flag remains available to the unchanged raw judgment. +export function projectEagerReloadObservation( + history: EagerReloadHistory, + observed: EagerReloadObservation, +): EagerReloadObservation { + return history.mutation + ? observed + : { ...observed, mutationReleasedBeforeFulfillment: false } +} + export async function observeEagerReload( adapter: EagerReloadAdapter, h: EagerReloadHistory, @@ -145,7 +157,7 @@ export async function observeEagerReload( await pending await Promise.all(receipts.map(whenSyncAccepted)) await Promise.all(receipts) - return { + return projectEagerReloadObservation(h, { before, beforeStatus, settledBeforeSource, @@ -157,6 +169,6 @@ export async function observeEagerReload( mutationSettledBeforeFulfillment, final: sortedStartupRows(collection.values()), ...(error === undefined ? {} : { error }), - } + }) }, [() => releaseMutation(), () => collection.cleanup(), () => sdk.dispose()]) } diff --git a/packages/db-adapter-conformance/tests/eager-reload-projection.test.ts b/packages/db-adapter-conformance/tests/eager-reload-projection.test.ts new file mode 100644 index 0000000000..28b6e1b868 --- /dev/null +++ b/packages/db-adapter-conformance/tests/eager-reload-projection.test.ts @@ -0,0 +1,78 @@ +/** Recorder calibration: projection preserves the shared raw judgment. */ +import { describe, expect, it } from 'vitest' +import { projectEagerReloadObservation } from './eager-reload-driver' +import { + eagerReloadHistories, + eagerReloadModel, + eagerReloadViolations, +} from './eager-reload-oracle' +import type { + EagerReloadHistory, + EagerReloadObservation, +} from './eager-reload-oracle' + +function observation( + history: EagerReloadHistory, + released: boolean, +): EagerReloadObservation { + const expected = eagerReloadModel(history) + return { + before: expected.initial, + beforeStatus: history.start === `fresh` ? `loading` : `ready`, + settledBeforeSource: false, + submittedRows: expected.source.length, + submittedCommits: 1, + acceptedCommits: 1, + atFulfillment: expected.source, + mutationReleasedBeforeFulfillment: released, + mutationSettledBeforeFulfillment: !history.mutation, + final: expected.source, + } +} + +describe(`eager reload observation projection`, () => { + // This includes the exact no-row/no-mutation history from the CLI capture. + // A bad before-source row remains a bad row under either meaningless flag. + for (const history of eagerReloadHistories.filter((h) => !h.mutation)) { + it(`preserves judgments for absent mutation in ${JSON.stringify(history)}`, () => { + for (const wrongRows of [false, true]) { + const projected = [false, true].map((released) => { + const raw = observation(history, released) + if (wrongRows) + raw.before = [...raw.before, { id: `unexpected`, value: -1 }] + const original = structuredClone(raw) + const actual = projectEagerReloadObservation(history, raw) + expect(eagerReloadViolations(history, actual)).toEqual( + eagerReloadViolations(history, raw), + ) + expect(eagerReloadViolations(history, actual)).toEqual( + wrongRows ? [`before-source rows`] : [], + ) + expect(raw).toEqual(original) + expect(actual.mutationReleasedBeforeFulfillment).toBe(false) + expect({ + ...actual, + mutationReleasedBeforeFulfillment: + raw.mutationReleasedBeforeFulfillment, + }).toEqual(raw) + return actual + }) + expect(projected[0]).toEqual(projected[1]) + } + }) + } + for (const history of eagerReloadHistories.filter((h) => h.mutation)) { + it(`retains the mutation timing distinction in ${JSON.stringify(history)}`, () => { + for (const released of [false, true]) { + const raw = observation(history, released) + raw.mutationSettledBeforeFulfillment = false + const actual = projectEagerReloadObservation(history, raw) + expect(actual).toBe(raw) + expect(actual.mutationReleasedBeforeFulfillment).toBe(released) + expect(eagerReloadViolations(history, actual)).toEqual( + released ? [] : [`pending optimistic overlay disappeared`], + ) + } + }) + } +}) diff --git a/packages/db-adapter-conformance/tests/persisted-baseline-candidate-plugins.ts b/packages/db-adapter-conformance/tests/persisted-baseline-candidate-plugins.ts new file mode 100644 index 0000000000..29de6f6b87 --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-baseline-candidate-plugins.ts @@ -0,0 +1,45 @@ +/** Immutable test-only overlay; original module IDs preserve real imports. */ +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import type { PluginOption } from 'vite' + +type Candidate = { + originalSha: string + candidateSha: string + candidateName: string +} +export function baselineCandidatePlugins(root: string): Array { + const directory = process.env.TANSTACK_DB_BASELINE_CANDIDATE_DIR + if (!directory) return [] + const candidates = JSON.parse( + readFileSync(resolve(directory, `manifest.json`), `utf8`), + ) as Record + const hash = (source: string) => + createHash(`sha256`).update(source).digest(`hex`) + return [ + { + name: `immutable-baseline-lifecycle-candidate-${hash(JSON.stringify(candidates)).slice(0, 16)}`, + enforce: `pre`, + transform(source, id) { + const target = id.split(`?`)[0] + for (const [file, entry] of Object.entries(candidates)) { + if (target !== resolve(root, file)) continue + if (hash(source) !== entry.originalSha) + throw new Error(`Original source hash changed: ${file}`) + const candidate = readFileSync( + resolve(directory, entry.candidateName), + `utf8`, + ) + if (hash(candidate) !== entry.candidateSha) + throw new Error(`Frozen candidate hash changed: ${file}`) + process.stdout.write( + `Baseline lifecycle candidate applied: ${file} ${entry.candidateSha}\n`, + ) + return { code: candidate, map: null } + } + return undefined + }, + }, + ] +} diff --git a/packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-driver.ts b/packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-driver.ts new file mode 100644 index 0000000000..617c05915f --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-driver.ts @@ -0,0 +1,387 @@ +/** + * Composition for the eager owner's LIFE-2/5 and HEALTH-5 receiving extension. + * Real provider reads and actual persistence establish the rows. Gates delay + * only the acknowledgment returned to the adapter, after the underlying + * receipt applies, and the completion returned by genuine baseline hydration. + * Those are distinct premises: old code need not call hydrateBaseline. + */ +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { hydrationLifecycleRows } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' +import { startupTurn, untilStartup } from './persisted-startup-driver' +import type { CollectionConfig, SyncConfig } from '@tanstack/db' +import type { StandardSchemaV1 } from '@standard-schema/spec' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' +import type { HeldCall } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' +import type { + BaselineLifecycleCut, + BaselineLifecycleHistory, + BaselineLifecycleObservation, +} from './persisted-baseline-lifecycle-oracle' +import type { HydrationLifecycleRow } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' + +export function baselineGate() { + let resolve!: (value: T) => void + let reject!: (error: unknown) => void + const promise = new Promise((yes, no) => { + resolve = yes + reject = no + }) + void promise.catch(() => undefined) + return { promise, resolve, reject } +} +export interface BaselineLifecycleProvider { + options: CollectionConfig< + HydrationLifecycleRow, + string, + StandardSchemaV1 + > + sessions: () => number + deliver: (run: number) => void + fail: (run: number, error: Error) => void + streamFail?: (run: number, error: Error) => void + replace: (rows: Array) => Promise + event: (row: HydrationLifecycleRow) => Promise + releases: () => Array + hookReleases: () => Array + hookAcquisitions: () => number + diagnostics: () => Array<{ message: string; error: unknown }> + dispose: () => Promise +} +export type BaselineLifecycleAdapter = ( + h: BaselineLifecycleHistory, + id: string, +) => Promise + +export async function observeBaselineLifecycle( + adapter: BaselineLifecycleAdapter, + persistence: PersistedCollectionPersistence, + holdBaseline: (id: string) => HeldCall, + h: BaselineLifecycleHistory, + id: string, + requireHydrateAwait = false, +): Promise { + const seed = createCollection( + persistedCollectionOptions({ + id, + getKey: (row: HydrationLifecycleRow) => row.id, + persistence, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (const row of hydrationLifecycleRows(h.previous, 1)) + write({ type: `insert`, value: row }) + commit() + markReady() + }, + }, + }), + ) + await withOracleCleanup(() => seed.preload(), [() => seed.cleanup()]) + const saved = await persistence.adapter.loadResumeSnapshot( + { kind: `eager`, collectionId: id }, + { includeRows: true }, + ) + const provider = await adapter(h, id) + const acknowledgment = baselineGate() + const baseline = baselineGate() + const underlying: Array | true> = [] + const replacementReceipts: Array | true> = [] + let settled = 0 + let baselineCompleted = false + let hydrateCalls = 0 + let hydrateHeld = false + let run = 0 + let genuineBaseline: Promise | undefined + let rejectedIdentity = false + const error = new Error(`injected source failure`) + const reports: Array = [] + const oldHold = h.action === `source-fail` ? holdBaseline(id) : undefined + const oldConsole = { error: console.error, warn: console.warn } + const diagnostic = (level: `error` | `warn`, args: Array) => { + const text = args.map(String).join(` `) + reports.push(`${level}: ${text}`) + } + console.error = (...args: Array) => diagnostic(`error`, args) + console.warn = (...args: Array) => diagnostic(`warn`, args) + const detached = (reason: unknown) => + reports.push(`unhandled rejection: ${String(reason)}`) + const browserDetached = (event: PromiseRejectionEvent) => + detached(event.reason) + const node = + typeof process !== `undefined` && typeof process.on === `function` + if (node) process.on(`unhandledRejection`, detached) + else globalThis.addEventListener(`unhandledrejection`, browserDetached) + const wrappedOptions = { + ...provider.options, + persistence, + ...(h.initialRender === `network-first` + ? { + initialRender: { + strategy: `network-first` as const, + networkTimeoutMs: 60_000, + }, + } + : {}), + sync: { + ...provider.options.sync, + sync: ( + params: Parameters< + SyncConfig[`sync`] + >[0], + ) => { + const old = run++ === 0 + const capability = params.metadata!.persistence! + if (old) + genuineBaseline = capability.hydrateBaseline().then(() => { + baselineCompleted = true + }) + return provider.options.sync.sync({ + ...params, + metadata: { + ...params.metadata!, + persistence: { + ...capability, + hydrateBaseline: async () => { + await capability.hydrateBaseline() + if (old) { + hydrateCalls++ + hydrateHeld = true + await baseline.promise + hydrateHeld = false + } + }, + }, + }, + commit: (...args) => { + const receipt = params.commit(...args) + if (!old) { + replacementReceipts.push(receipt) + return receipt + } + underlying.push(receipt) + const applied = Promise.resolve(receipt).then(() => { + settled++ + }) + // The plain Promise is a legal SyncAppliedReceipt fallback. It + // intentionally delays acceptance acknowledgment as well as visible + // settlement; it does not change the already-applied obligation. + return applied.then(() => acknowledgment.promise) + }, + fail: (failure) => { + if (old) rejectedIdentity = failure === error + return params.fail(failure) + }, + }) + }, + }, + } + const collection = wrappedOptions.schema + ? createCollection( + persistedCollectionOptions({ + ...wrappedOptions, + schema: wrappedOptions.schema, + }), + ) + : createCollection( + persistedCollectionOptions({ ...wrappedOptions, schema: undefined }), + ) + let oldPreload = `pending` + const oldReady = collection.preload() + void oldReady.then( + () => { + oldPreload = `fulfilled` + }, + (failure: unknown) => { + oldPreload = + failure === error + ? error.message + : failure instanceof Error + ? failure.name + : String(failure) + }, + ) + const snapshot = async (useSaved = false): Promise => { + const stored = useSaved + ? saved + : await persistence.adapter.loadResumeSnapshot( + { kind: `eager`, collectionId: id }, + { includeRows: true }, + ) + const sort = (rows: Iterable) => + [...rows] + .map(({ id: key, value }) => ({ id: key, value })) + .sort((a, b) => a.id.localeCompare(b.id)) + return { + rows: sort(collection.values()), + durable: sort( + stored.rows.map((row) => row.value as HydrationLifecycleRow), + ), + status: collection.status, + healthError: collection.health.error + ? collection.health.error.message + : null, + failureReason: collection.health.failureReason + ? collection.health.failureReason.message + : null, + } + } + const publicSnapshot = (cut: BaselineLifecycleCut) => ({ + rows: cut.rows, + durable: cut.durable, + status: cut.status, + }) + let observation: BaselineLifecycleObservation | undefined + let cleanup: Promise | undefined + let replacementReady: Promise | undefined + return withOracleCleanup(async () => { + await untilStartup( + () => provider.sessions() === 1 && genuineBaseline !== undefined, + ) + if (oldHold) await untilStartup(oldHold.isHeld) + else await genuineBaseline + if (h.action !== `source-fail`) { + provider.deliver(0) + await untilStartup(() => underlying.length > 0) + if (requireHydrateAwait) + await untilStartup(() => hydrateHeld && hydrateCalls === 1) + // The provider's finite full read emits all batches before its hydration + // wait. Allow task delivery, then require every genuine receipt settled. + for (let turn = 0; turn < 3; turn++) await startupTurn() + await Promise.all(underlying) + await untilStartup(() => settled === underlying.length) + } + const before = await snapshot(h.action === `source-fail`) + const candidateHydrateHeld = hydrateHeld + const reached: Array = [] + if (underlying.length > 0 && settled === underlying.length) + reached.push(`accepted-before-retirement`) + let cleanupStartedBeforeRelease = false + if (h.action === `cleanup`) { + cleanup = collection.cleanup() + cleanupStartedBeforeRelease = true + reached.push(`cleanup-held-continuation`) + } else if (h.action === `reload`) { + replacementReady = collection.reload() + void replacementReady.catch(() => undefined) + cleanupStartedBeforeRelease = true + reached.push(`reload-held-continuation`) + } else if (h.action === `source-fail`) { + provider.fail(0, error) + reached.push(`source-failure-held-baseline`) + } else if (h.action === `stream-fail`) { + if (!provider.streamFail) + throw new Error(`Provider has no terminal stream-failure capability`) + provider.streamFail(0, error) + reached.push(`stream-failure-held-continuation`) + } else { + acknowledgment.reject(error) + reached.push(`receipt-rejection-held-baseline`) + } + // Provider failure is judged while its independent baseline peer remains + // held. A failed acknowledgment alone has no prompt-failure promise. + for (let turn = 0; turn < 3; turn++) await startupTurn() + const failureWhileHeld = { + status: collection.status, + identity: rejectedIdentity && collection.health.error?.cause === error, + reported: rejectedIdentity, + preload: oldPreload, + } + oldHold?.release() + await genuineBaseline + baseline.resolve() + if (h.action !== `receipt-reject`) acknowledgment.resolve() + if (cleanup) await cleanup + if ( + h.action === `source-fail` || + h.action === `receipt-reject` || + h.action === `stream-fail` + ) + await untilStartup(() => oldPreload !== `pending`) + await Promise.all(underlying) + for (let turn = 0; turn < 3; turn++) await startupTurn() + const resumed = await snapshot() + const failureIdentity = + rejectedIdentity && collection.health.error?.cause === error + if (h.action !== `reload`) { + await collection.cleanup() + replacementReady = collection.preload() + void replacementReady.catch(() => undefined) + await untilStartup(() => provider.sessions() === 2) + } + await untilStartup(() => provider.sessions() === 2) + await provider.replace(hydrationLifecycleRows(h.replacement, 3)) + provider.deliver(1) + await replacementReady + await Promise.all(replacementReceipts) + const replacement = publicSnapshot(await snapshot()) + if (h.replacement === 0) reached.push(`empty-replacement`) + if (h.action !== `source-fail`) { + const delivered = before.rows.filter((row) => row.value === 2) + if ( + delivered.some((row) => (h.replacement & (1 << Number(row.id))) !== 0) + ) + reached.push(`replacement-overlap`) + if ( + delivered.some((row) => (h.replacement & (1 << Number(row.id))) === 0) + ) + reached.push(`replacement-omits-old`) + } + const eventReceipt = replacementReceipts.length + await provider.event({ id: `3`, value: 4 }) + await untilStartup(() => replacementReceipts.length > eventReceipt) + await Promise.all(replacementReceipts) + const afterEvent = publicSnapshot(await snapshot()) + observation = { + before, + resumed, + replacement, + afterEvent, + oldPreload, + releases: [], + hookReleases: [], + hookAcquisitions: 0, + reports, + providerDiagnostics: [], + failureIdentity, + failureWhileHeld, + cleanupStartedBeforeRelease, + underlyingReceipts: underlying.length, + underlyingSettled: settled, + candidateHydrateCalls: hydrateCalls, + candidateHydrateHeld, + baselineCompleted, + reached, + } + return observation + }, [ + () => oldHold?.uninstall(), + () => baseline.resolve(), + () => acknowledgment.resolve(), + () => provider.deliver(0), + () => provider.deliver(1), + () => collection.cleanup(), + async () => { + await startupTurn() + if (observation) { + observation.releases = provider.releases() + observation.hookReleases = provider.hookReleases() + observation.hookAcquisitions = provider.hookAcquisitions() + observation.providerDiagnostics = provider + .diagnostics() + .map((entry) => ({ + message: entry.message, + causeIsInjected: entry.error === error, + })) + } + }, + () => provider.dispose(), + () => { + console.error = oldConsole.error + console.warn = oldConsole.warn + if (node) process.off(`unhandledRejection`, detached) + else globalThis.removeEventListener(`unhandledrejection`, browserDetached) + }, + ]) +} diff --git a/packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle-suite.ts new file mode 100644 index 0000000000..2df2c4ef29 --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle-suite.ts @@ -0,0 +1,236 @@ +/** Registration only: one judgment and grammar reach every receiving host. */ +import { oracleProperty, pass, violation } from '../../db/tests/oracle-property' +import { + baselineLifecycleArbitrary, + baselineLifecycleHistories, + baselineLifecycleStates, + baselineLifecycleViolations, +} from './persisted-baseline-lifecycle-oracle' +import type { + BaselineLifecycleHistory, + BaselineLifecycleObservation, +} from './persisted-baseline-lifecycle-oracle' +import type { Observation } from '../../db/tests/oracle-property' + +export function registerBaselineLifecycle( + config: { property: string; defaultRuns: number; streamFailure: boolean }, + receive: ( + h: BaselineLifecycleHistory, + id: string, + ) => Promise, +) { + const id = config.property + const candidate = process.env.TANSTACK_DB_BASELINE_EXPECT_HYDRATE !== `0` + const builds = [ + { + state: `accepted-before-retirement`, + build: (h: BaselineLifecycleHistory) => ({ + ...h, + action: `cleanup` as const, + }), + }, + { + state: `cleanup-held-continuation`, + build: (h: BaselineLifecycleHistory) => ({ + ...h, + action: `cleanup` as const, + }), + }, + { + state: `reload-held-continuation`, + build: (h: BaselineLifecycleHistory) => ({ + ...h, + action: `reload` as const, + }), + }, + { + state: `source-failure-held-baseline`, + build: (h: BaselineLifecycleHistory) => ({ + ...h, + action: `source-fail` as const, + initialRender: `default` as const, + }), + }, + { + state: `receipt-rejection-held-baseline`, + build: (h: BaselineLifecycleHistory) => ({ + ...h, + action: `receipt-reject` as const, + initialRender: `default` as const, + }), + }, + { + state: `empty-replacement`, + build: (h: BaselineLifecycleHistory) => ({ ...h, replacement: 0 }), + }, + { + state: `replacement-overlap`, + build: (h: BaselineLifecycleHistory) => ({ + ...h, + action: `cleanup` as const, + old: h.old | 1, + replacement: h.replacement | 1, + }), + }, + { + state: `replacement-omits-old`, + build: (h: BaselineLifecycleHistory) => ({ + ...h, + action: `cleanup` as const, + old: h.old | 1, + replacement: h.replacement & ~1, + }), + }, + ...(candidate + ? [ + { + state: `adapter-hydrate-continuation`, + build: (h: BaselineLifecycleHistory) => ({ + ...h, + action: `cleanup` as const, + }), + }, + ] + : []), + ...(config.streamFailure + ? [ + { + state: `stream-failure-held-continuation`, + build: (h: BaselineLifecycleHistory) => ({ + ...h, + action: `stream-fail` as const, + initialRender: `default` as const, + }), + }, + ] + : []), + ] + const required = new WeakMap>() + let sequence = 0 + oracleProperty({ + id, + law: { + statement: `Cleanup retires source authority while persisted-baseline completion or an adapter acknowledgment is held. Accepted rows remain owed, replacement results are complete, resources release once, and failures remain handled.`, + authority: `Shared eager-source LIFE-2/5/10, READY-4, HEALTH-5; glossary accepted sync transaction, cleanup and reload; existing TrailBase persisted-hydration row projection.`, + owner: `Shared eager-source lifecycle receiving extension`, + limits: `Three keys, one retirement/reload and replacement, delayed adapter acknowledgment distinct from application, local SDK data. Explicit hydrate-call reach required only for candidate mode; no remote provider service or internal-work law.`, + }, + histories: { + arbitrary: baselineLifecycleArbitrary(config.streamFailure), + runs: config.defaultRuns, + fixedSeeds: [239467], + states: [ + ...baselineLifecycleStates, + ...(candidate ? [`adapter-hydrate-continuation`] : []), + ...(config.streamFailure ? [`stream-failure-held-continuation`] : []), + ], + constructed: builds.map(({ state, build }) => ({ + name: `generated ${state}`, + enters: [state], + build: (generated: BaselineLifecycleHistory) => { + const history = { ...build(generated) } + required.set(history, [state]) + return history + }, + })), + fixed: [ + ...baselineLifecycleHistories, + ...(config.streamFailure + ? baselineLifecycleHistories + .filter((history) => history.action === `cleanup`) + .map((history) => ({ + ...history, + action: `stream-fail` as const, + })) + : []), + ].map((history) => ({ name: JSON.stringify(history), history })), + }, + model: (history) => ({ required: required.get(history) ?? [] }), + driver: async (history, observe) => { + let actual: BaselineLifecycleObservation + try { + actual = await receive(history, `${id}-${sequence++}`) + } catch (error) { + observe(`receiving-failure`, String(error)) + throw error + } + for (const state of actual.reached) observe(`reach`, state) + if (actual.candidateHydrateCalls === 1 && actual.candidateHydrateHeld) + observe(`reach`, `adapter-hydrate-continuation`) + observe(`completed`, actual) + }, + check: (history, trace, expected) => { + const record = trace.find((item) => item.checkpoint === `completed`) + if (!record) + return trace.some((item) => item.checkpoint === `receiving-failure`) + ? pass + : violation( + `completed`, + `No completed observation`, + `receiving execution`, + ) + const actual = record.value as BaselineLifecycleObservation + const differences = baselineLifecycleViolations( + history, + actual, + candidate, + ) + if (differences.length) + return violation( + `lifecycle`, + differences.join(`; `), + `persisted source lifecycle`, + ) + if (actual.browserErrors?.length) + return violation( + `browser`, + actual.browserErrors.join(`; `), + `detached-browser-error`, + ) + for (const state of expected.required) + if ( + !trace.some( + (item) => item.checkpoint === `reach` && item.value === state, + ) + ) + return violation( + `reach`, + `generated construction omitted ${state}`, + `constructed builder reach`, + ) + return pass + }, + calibrations: [ + { + name: `lost durable accepted rows`, + law: `persisted source lifecycle`, + corrupt: (trace, history) => + trace.map((item) => { + if (item.checkpoint !== `completed` || history.action !== `cleanup`) + return item + const actual = item.value as BaselineLifecycleObservation + if (actual.before.durable.length === 0) return item + return { + ...item, + value: { ...actual, resumed: { ...actual.resumed, durable: [] } }, + } + }), + }, + ...builds.map(({ state }) => ({ + name: `missing generated builder state ${state}`, + law: `constructed builder reach`, + corrupt: ( + trace: ReadonlyArray, + history: BaselineLifecycleHistory, + ) => + required.get(history)?.includes(state) + ? trace.filter( + (entry) => + !(entry.checkpoint === `reach` && entry.value === state), + ) + : undefined, + })), + ], + timeout: 120_000, + }) +} diff --git a/packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle.ts b/packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle.ts new file mode 100644 index 0000000000..6cea147f36 --- /dev/null +++ b/packages/db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle.ts @@ -0,0 +1,275 @@ +/** + * Persisted baseline work does not extend a retired sync run's authority. + * LIFE-2/5 and HEALTH-5 remain the shared eager-source owner's laws. This + * receiver adds their provider/persistence crossing, not a lifecycle machine. + * + * The retirement check is a differential law. First settle every underlying + * accepted receipt, then retire the run while its acknowledgment or genuine + * hydrateBaseline completion remains held. Releasing that continuation cannot + * change the replacement's rows, readiness or health. Durable accepted rows + * remain owed. A subsequent complete result uses the existing TrailBase + * hydration projection's independent row judgment. + * + * Limits: three keys, one old run and one replacement, default rendering for + * injected failures, local PowerSync/RxDB providers and one persistence store. + * A delayed adapter acknowledgment does not delay application. Its reach is + * distinct from a candidate's explicit hydrateBaseline await. No internal-work + * bound, provider service, arbitrary concurrent mutation or I/O crash claim. + */ +import fc from 'fast-check' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { + expectedHydrationLifecycle, + hydrationLifecycleRows, +} from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' +import type { HydrationLifecycleSnapshot } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' + +export type BaselineLifecycleHistory = { + previous: number + old: number + replacement: number + batchSize: number + action: + `cleanup` | `reload` | `source-fail` | `receipt-reject` | `stream-fail` + initialRender: `default` | `network-first` +} +const memberships = { + previous: fc.integer({ min: 0, max: 7 }), + // Both providers commit an empty initial read as well as a nonempty one. + old: fc.integer({ min: 0, max: 7 }), + replacement: fc.integer({ min: 0, max: 7 }), + batchSize: fc.integer({ min: 1, max: 2 }), +} +export const baselineLifecycleHistoryArb: fc.Arbitrary = + fc.oneof( + fc.record({ + ...memberships, + action: fc.constantFrom(`cleanup` as const, `reload` as const), + initialRender: fc.constantFrom( + `default` as const, + `network-first` as const, + ), + }), + fc.record({ + ...memberships, + action: fc.constantFrom( + `source-fail` as const, + `receipt-reject` as const, + ), + initialRender: fc.constant(`default` as const), + }), + ) +export function baselineLifecycleArbitrary(streamFailure: boolean) { + return streamFailure + ? fc.oneof( + baselineLifecycleHistoryArb, + fc.record({ + ...memberships, + action: fc.constant(`stream-fail` as const), + initialRender: fc.constant(`default` as const), + }), + ) + : baselineLifecycleHistoryArb +} +export const baselineLifecycleHistories: Array = ( + [`cleanup`, `reload`, `source-fail`, `receipt-reject`] as const +).flatMap((action) => + [0, 6].map((replacement) => ({ + previous: 3, + old: 1, + replacement, + batchSize: 1, + action, + initialRender: `default`, + })), +) +export type BaselineLifecycleCut = HydrationLifecycleSnapshot & { + healthError: string | null + failureReason: string | null +} +export type BaselineLifecycleObservation = { + before: BaselineLifecycleCut + resumed: BaselineLifecycleCut + replacement: HydrationLifecycleSnapshot + afterEvent: HydrationLifecycleSnapshot + oldPreload: string + releases: Array + hookReleases: Array + hookAcquisitions: number + reports: Array + providerDiagnostics: Array<{ message: string; causeIsInjected: boolean }> + failureIdentity: boolean + failureWhileHeld: { + status: string + identity: boolean + reported: boolean + preload: string + } + cleanupStartedBeforeRelease: boolean + underlyingReceipts: number + underlyingSettled: number + candidateHydrateCalls: number + candidateHydrateHeld: boolean + baselineCompleted: boolean + reached: Array + browserErrors?: ReadonlyArray +} +export const baselineLifecycleStates = [ + `accepted-before-retirement`, + `cleanup-held-continuation`, + `reload-held-continuation`, + `source-failure-held-baseline`, + `receipt-rejection-held-baseline`, + `empty-replacement`, + `replacement-overlap`, + `replacement-omits-old`, +] as const + +// This is the existing hydration owner's complete-result judgment. The +// differential prefix deliberately predicts no source batching strategy. +export function baselineLifecycleExpected(h: BaselineLifecycleHistory) { + return expectedHydrationLifecycle({ + previous: h.previous, + old: h.old, + replacement: h.replacement, + oldList: `complete`, + resumeAt: `replacement-ready`, + initialRender: h.initialRender, + }) +} +export function baselineLifecycleViolations( + h: BaselineLifecycleHistory, + actual: BaselineLifecycleObservation, + requireHydrateAwait = false, +): Array { + const violations: Array = [] + const equal = (left: unknown, right: unknown) => + JSON.stringify(left) === JSON.stringify(right) + if (!actual.baselineCompleted) + violations.push(`premise: genuine baseline did not complete`) + if (actual.underlyingReceipts !== actual.underlyingSettled) + violations.push(`premise: accepted receipt obligations remain unsettled`) + if ( + requireHydrateAwait && + h.action !== `source-fail` && + (!actual.candidateHydrateHeld || actual.candidateHydrateCalls !== 1) + ) + violations.push( + `premise: adapter hydrateBaseline continuation was not held`, + ) + if (h.action === `cleanup` || h.action === `reload`) { + if (!actual.cleanupStartedBeforeRelease) + violations.push(`premise: cleanup did not retire the held continuation`) + const expected = + h.action === `cleanup` + ? { + rows: [], + durable: actual.before.durable, + status: `cleaned-up`, + healthError: null, + failureReason: null, + } + : actual.before + if (!equal(actual.resumed, expected)) + violations.push( + `LIFE-2/LIFE-5: obsolete continuation changed retired state`, + ) + // Cleanup ends a preload with AbortError. Reload retains the Collection's + // shared readiness promise; the replacement's readiness fulfills it. + if ( + actual.oldPreload !== + (h.action === `cleanup` ? `AbortError` : `fulfilled`) + ) + violations.push(`LIFE-5: retired preload ${actual.oldPreload}`) + } else { + if ( + !actual.failureIdentity || + actual.resumed.healthError !== `injected source failure` || + actual.resumed.failureReason !== null + ) + violations.push(`READY-4: source failure identity`) + if (actual.resumed.status !== `error`) + violations.push(`READY-4: source failure did not end initial load`) + const acceptedRows = + h.action === `source-fail` + ? hydrationLifecycleRows(h.previous, 1) + : actual.before.rows + if (h.action === `receipt-reject`) { + // A failed acknowledgment does not retire source authority by itself. + // Reconciliation may accept omission deletes before fail() observes it. + // Source rows already applied stay; omitted saved rows may stay or leave. + const required = hydrationLifecycleRows(h.old, 2) + const optional = hydrationLifecycleRows(h.previous & ~h.old, 1) + const permitted = [...required, ...optional] + if ( + !equal(actual.resumed.rows, actual.resumed.durable) || + required.some( + (row) => !actual.resumed.rows.some((seen) => equal(row, seen)), + ) || + actual.resumed.rows.some( + (row) => !permitted.some((allowed) => equal(row, allowed)), + ) + ) + violations.push(`receipt failure: invalid accepted source prefix`) + } else if ( + !equal(actual.resumed.rows, acceptedRows) || + !equal(actual.resumed.durable, actual.before.durable) + ) + violations.push( + `accepted obligations changed after terminal source failure`, + ) + if (actual.oldPreload !== `injected source failure`) + violations.push(`READY-4: failed preload ${actual.oldPreload}`) + // Pending persisted restore can defer public failure. The adapter still + // reports its exact error and releases the failed provider while held. + if (h.action === `source-fail` && !actual.failureWhileHeld.reported) + violations.push( + `READY-4: provider failure was not reported while restore remained held`, + ) + if ( + h.action === `stream-fail` && + (actual.failureWhileHeld.status !== `error` || + !actual.failureWhileHeld.identity || + actual.failureWhileHeld.preload !== `injected source failure`) + ) + violations.push( + `READY-4: provider failure did not end loading while baseline remained held`, + ) + } + const expected = baselineLifecycleExpected(h) + if (!equal(actual.replacement, expected.ready)) + violations.push(`replacement: complete eager result`) + if (!equal(actual.afterEvent, expected.afterEvent)) + violations.push(`afterEvent: replacement source authority`) + if ( + actual.releases.length !== 2 || + actual.releases.some((count) => count !== 1) + ) + violations.push(`LIFE-2/LIFE-10: releases ${actual.releases}`) + if ( + actual.hookReleases.length !== actual.hookAcquisitions || + actual.hookReleases.some((count) => count !== 1) + ) + violations.push(`LIFE-2: load hook releases ${actual.hookReleases}`) + if (actual.reports.length) + violations.push(`HEALTH-5: ${actual.reports.join(`; `)}`) + for (const diagnostic of actual.providerDiagnostics) + if ( + (h.action !== `source-fail` && h.action !== `receipt-reject`) || + !diagnostic.causeIsInjected || + !diagnostic.message.startsWith(`Could not start syncing process for `) + ) + violations.push( + `HEALTH-5: unexpected provider diagnostic ${diagnostic.message}`, + ) + return violations +} +declareOracle({ + grammar: [ + baselineLifecycleHistoryArb, + baselineLifecycleArbitrary, + baselineLifecycleHistories, + ], + model: [baselineLifecycleExpected], + check: [baselineLifecycleViolations], +}) diff --git a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts index e1f03ee651..bfee738a7f 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-driver.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-driver.ts @@ -28,6 +28,7 @@ import type { StartupRow, StartupScope, StartupSnapshot, + StartupSyncTransaction, } from './persisted-startup-oracle' export function isStartupRowRead(sql: string): boolean { @@ -55,6 +56,7 @@ export interface PersistedStartupSource { dispose: () => unknown checkStartup?: () => Array syncOutcome?: Promise<{ error?: unknown }> + sourceTransactions?: () => ReadonlyArray } export interface PersistedStartupAdapter { scope: StartupScope @@ -86,6 +88,9 @@ export function trackStartupSync( let hydrateBaseline: (() => Promise) | undefined let committedRows = 0 let committed = false + const sourceTransactions: Array = [] + let sourceTransaction: StartupSyncTransaction | undefined + let recordingInitialResult = true let resolveCommit!: () => void const firstCommit = new Promise((resolve) => { resolveCommit = resolve @@ -109,12 +114,34 @@ export function trackStartupSync( metadata: metadataTransform( params.metadata, ) as typeof params.metadata, + begin: (options) => { + params.begin(options) + sourceTransaction = recordingInitialResult + ? { + snapshot: options?.snapshot === true, + truncate: false, + rowWrites: 0, + deleteWrites: 0, + } + : undefined + }, + truncate: (options) => { + params.truncate(options) + if (sourceTransaction) sourceTransaction.truncate = true + }, write: (message) => { params.write(message) + if (sourceTransaction) { + if (message.type === `delete`) sourceTransaction.deleteWrites++ + else sourceTransaction.rowWrites++ + } writes++ }, commit: (signal) => { const receipt = params.commit(signal) + if (sourceTransaction) + sourceTransactions.push({ ...sourceTransaction }) + sourceTransaction = undefined receipts.push(receipt) committedRows += writes writes = 0 @@ -125,6 +152,7 @@ export function trackStartupSync( }, markReady: () => { params.markReady() + recordingInitialResult = false submit() void Promise.all(receipts).then(() => resolve(), reject) }, @@ -145,6 +173,7 @@ export function trackStartupSync( firstSubmission, hasCommitted: () => committed, committedRowCount: () => committedRows, + sourceTransactions: () => sourceTransactions.map((tx) => ({ ...tx })), waitForRestore: async () => { if (!hydrateBaseline) throw new Error(`Persisted receiver has no baseline capability`) @@ -332,6 +361,7 @@ export async function observePersistedStartup< assertCut(`startup`, startup) // Work is captured before the observation's own durable read. const startupRowReads = readRowCount() - initialReadCount - observationReads + const sourceTransactions = current.sourceTransactions?.() const premiseViolations = [ ...persistedStartupPremiseViolations(h, { responseBeforeRestore, @@ -362,6 +392,7 @@ export async function observePersistedStartup< const observation = { responseBeforeRestore, startupRowReads, + ...(sourceTransactions ? { sourceTransactions } : {}), ...(fallback ? { fallback, fallbackDurable } : {}), startup, startupDurable, diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts index 537296361b..15f520a17a 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts @@ -59,7 +59,15 @@ export function registerPersistedStartupConformance< origins?: Array origin?: (history: H) => string | undefined }) { - const states = [...startupRequiredStates(adapter.scope), ...origins] + const states = [ + ...startupRequiredStates( + adapter.scope, + histories.some((h) => h.batchSize !== undefined), + histories.some((h) => h.singleSourceCommit), + histories.some((h) => h.sourceKeys), + ), + ...origins, + ] // The pinned framework remembers required states only for seed examples. // Keep the same obligations on every fresh generated or shrunk construction. // They describe its inputs, never production's expected rows. @@ -115,6 +123,36 @@ export function registerPersistedStartupConformance< case `persisting-mutation`: history.mutation = `persisting` break + case `source-below-batch`: + history.batchSize = 2 + history.next &= key + break + case `source-at-batch`: + history.batchSize = 1 + history.next = key + break + case `source-above-batch`: + history.batchSize = 2 + history.next = 7 + break + case `omissions-above-batch`: + history.batchSize = 2 + history.previous = 7 + history.next = 0 + break + case `paged-single-source`: + history.singleSourceCommit = true + history.next = 7 + break + case `omissions-above-read-batch`: + history.singleSourceCommit = true + history.previous = 7 + history.next = 0 + break + case `source-key-transform`: + history.sourceKeys = `prefixed` + history.next |= key + break } if ( state.startsWith(`network-first-`) || @@ -126,6 +164,7 @@ export function registerPersistedStartupConformance< history.peer = false delete history.previousOwner } + if (history.next === 0) delete history.sourceKeys constructionStates.set(history, [state]) return history }, @@ -196,7 +235,9 @@ export function registerPersistedStartupConformance< violations.join(`\n`), violations[0]?.startsWith(`startupDurable:`) ? `startup durability` - : `persisted startup`, + : violations[0]?.startsWith(`startup batching:`) + ? `startup batching` + : `persisted startup`, ) } const completed = trace.find( @@ -219,7 +260,9 @@ export function registerPersistedStartupConformance< violations.join(`\n`), violations[0]?.startsWith(`startupDurable:`) ? `startup durability` - : `persisted startup`, + : violations[0]?.startsWith(`startup batching:`) + ? `startup batching` + : `persisted startup`, ) const reached = new Set( trace @@ -320,8 +363,8 @@ export function registerPersistedStartupConformance< oracleProperty({ id: property, law: { - statement: `A complete eager source result replaces its persisted baseline and is durable before later refresh; network-first fallback preserves the saved snapshot.`, - authority: `READY-3/PUB-7; docs/collections/query-collection.md, Direct Writes and Query Sync and Initial Data; glossary, network-first initial render; maintainer shared-owner decision 2026-11-09.`, + statement: `A complete eager source result replaces its persisted baseline and is durable before later refresh; network-first fallback preserves the saved snapshot. Opted-in receivers preserve ordinary startup commits without a full replacement.`, + authority: `READY-3/PUB-7; docs/collections/query-collection.md, Direct Writes and Query Sync and Initial Data; glossary, network-first initial render; maintainer shared-owner decision 2026-11-09 and batching decision 2026-11-10.`, owner: `Adapter conformance: persisted startup (#2367)`, limits: `Three string keys, eager startup, one source plus an optional peer, a disjoint persisting mutation, local-only reopen; no concurrent writers, failed I/O, on-demand windows, or intermediate-publication claim.`, }, @@ -342,6 +385,42 @@ export function registerPersistedStartupConformance< driver: drive, check, calibrations: [ + ...(histories.some( + (h) => h.batchSize !== undefined || h.singleSourceCommit, + ) + ? [ + { + name: `replaces the ordinary initial commit with an explicit truncate`, + law: `startup batching`, + corrupt: (trace: ReadonlyArray, history: H) => + history.batchSize === undefined && + !history.singleSourceCommit + ? undefined + : trace.map((entry) => { + if (entry.checkpoint !== `completed`) return entry + const observation = + entry.value as PersistedStartupObservation + return { + ...entry, + value: { + ...observation, + sourceTransactions: + observation.sourceTransactions?.map( + (tx, index) => + index === 0 + ? { + ...tx, + snapshot: false, + truncate: true, + } + : tx, + ), + }, + } + }), + }, + ] + : []), ...states.map((state) => ({ name: `omits constructed ${state}`, law: `constructed reach`, diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts index a3e30dffdf..cd9d95f603 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle.ts @@ -16,6 +16,9 @@ * source response, saved rows and ownership stay intact. Authority: the glossary's * network-first initial-render contract and persisted-readiness oracle. That * owner retains empty/failed restore, remote-failure, and exact deadline laws. + * Opted-in PowerSync/RxDB receivers also preserve ordinary first-startup + * commits without full replacement (maintainer decision, 2026-11-10). + * PowerSync payload batches are bounded; total memory and elapsed time are not. * * The model abstracts source scopes to `source` and `peer`. These are ownership * edges, not subscriptions or provider identities. Whole-Collection adapters @@ -43,6 +46,18 @@ export type PersistedStartupHistory = { response: `before-restore` | `after-restore` | `after-deadline` mutation?: `persisting` initialRender?: `network-first` + /** The initial source and omission commits retain this configured row bound. */ + batchSize?: 1 | 2 + /** RxDB retains its existing one full-load commit, followed by omissions. */ + singleSourceCommit?: true + /** PowerSync provider IDs can deserialize to the same model Collection keys. */ + sourceKeys?: `prefixed` +} +export type StartupSyncTransaction = { + snapshot: boolean + truncate: boolean + rowWrites: number + deleteWrites: number } export type StartupRow = { id: string; value: number } export type StartupScope = `collection` | `scoped` @@ -60,6 +75,45 @@ export type PersistedStartupObservation = { refresh: StartupSnapshot durable: StartupSnapshot reopened: Array + sourceTransactions?: ReadonlyArray +} + +// Maintainer decision 2026-11-10: preserve PowerSync's existing startup batch +// commits and RxDB's one full-load commit without adding a full replacement. +// This receiving opt-in does not impose an elapsed-time/heap threshold or +// progressive publication on retained reloads. Each ordinary initial source +// commit, including an omission-only commit, writes at most the configured B +// rows. Source writes and omission deletes cannot exceed their input counts; +// an implementation may skip equal rows. The existing set judgment, rather +// than a required work count, rejects missing changes at every public/durable cut. +export function persistedStartupBatchViolations( + h: PersistedStartupHistory, + transactions: ReadonlyArray | undefined, +): Array { + if (h.batchSize === undefined && !h.singleSourceCommit) return [] + const batchSize = h.batchSize + if (transactions === undefined) + return [`startup batching: source transaction observations are missing`] + const violations: Array = [] + const rows = startupRows(h.next, 2).length + const omitted = startupRows(h.previous & ~h.next, 1).length + if (transactions.some((tx) => tx.snapshot || tx.truncate)) + violations.push(`startup batching: initial source selected replacement`) + if ( + batchSize !== undefined && + transactions.some((tx) => tx.rowWrites + tx.deleteWrites > batchSize) + ) + violations.push(`startup batching: a commit exceeded ${batchSize} rows`) + if ( + h.singleSourceCommit && + transactions.filter((tx) => tx.rowWrites > 0).length > 1 + ) + violations.push(`startup batching: the single source commit was split`) + if (transactions.reduce((n, tx) => n + tx.rowWrites, 0) > rows) + violations.push(`startup batching: source row writes exceeded N`) + if (transactions.reduce((n, tx) => n + tx.deleteWrites, 0) > omitted) + violations.push(`startup batching: deletes exceeded saved omissions`) + return violations } export const startupRows = (mask: number, value: number): Array => @@ -147,6 +201,9 @@ export function withStartupRender( // Cache-origin constraints remain in the receiving Query grammar. export const persistedStartupHistoryArb = ( scope: StartupScope, + batching = false, + singleSourceCommit = false, + sourceKeys = false, ): fc.Arbitrary => fc .record( @@ -161,11 +218,28 @@ export const persistedStartupHistoryArb = ( ), mutation: fc.constant(`persisting` as const), initialRender: fc.constant(`network-first` as const), + ...(batching + ? { batchSize: fc.constantFrom(1 as const, 2 as const) } + : {}), + ...(singleSourceCommit + ? { singleSourceCommit: fc.constant(true as const) } + : {}), + ...(sourceKeys ? { sourceKeys: fc.constant(`prefixed` as const) } : {}), + }, + { + requiredKeys: [ + `previous`, + `next`, + `peer`, + `response`, + ...(batching ? [`batchSize` as const] : []), + ...(singleSourceCommit ? [`singleSourceCommit` as const] : []), + ], }, - { requiredKeys: [`previous`, `next`, `peer`, `response`] }, ) .filter( (h) => + (!h.sourceKeys || h.next !== 0) && (!h.peer || h.previous !== 0) && (h.response !== `after-deadline` || (h.initialRender === `network-first` && h.previous !== 0)), @@ -217,8 +291,32 @@ export const startupReachedStates = ( (h.peer ? (h.previous & (h.previous - 1)) !== 0 : h.previous !== 0) ? [`ownerless-baseline`] : []), + ...(h.batchSize === undefined + ? [] + : [ + startupRows(h.next, 2).length < h.batchSize + ? `source-below-batch` + : startupRows(h.next, 2).length === h.batchSize + ? `source-at-batch` + : `source-above-batch`, + ...(startupRows(h.previous & ~h.next, 1).length > h.batchSize + ? [`omissions-above-batch`] + : []), + ]), + ...(h.singleSourceCommit && startupRows(h.next, 2).length > 2 + ? [`paged-single-source`] + : []), + ...(h.singleSourceCommit && startupRows(h.previous & ~h.next, 1).length > 2 + ? [`omissions-above-read-batch`] + : []), + ...(h.sourceKeys && h.next !== 0 ? [`source-key-transform`] : []), ] -export const startupRequiredStates = (scope: StartupScope): Array => [ +export const startupRequiredStates = ( + scope: StartupScope, + batching = false, + singleSourceCommit = false, + sourceKeys = false, +): Array => [ `response-before-restore`, `restore-before-response`, `network-first-source-result`, @@ -229,6 +327,18 @@ export const startupRequiredStates = (scope: StartupScope): Array => [ `omitted-row`, `persisting-mutation`, ...(scope === `scoped` ? [`peer-owner`, `ownerless-baseline`] : []), + ...(batching + ? [ + `source-below-batch`, + `source-at-batch`, + `source-above-batch`, + `omissions-above-batch`, + ] + : []), + ...(singleSourceCommit + ? [`paged-single-source`, `omissions-above-read-batch`] + : []), + ...(sourceKeys ? [`source-key-transform`] : []), ] export const persistedStartupHistories = uniqueStartupHistories( @@ -241,6 +351,46 @@ export const persistedStartupHistories = uniqueStartupHistories( ), ) +// Three saved omissions distinguish a safe omission sweep from one that skips +// a later key when applying a deletion changes the iterator's backing map. +const multipleOmissionHistories = withStartupMutations([ + { + previous: 7, + next: 0, + peer: false, + response: `before-restore` as const, + }, + { previous: 7, next: 0, peer: false, response: `after-restore` as const }, + { + previous: 7, + next: 0, + peer: false, + response: `after-deadline` as const, + initialRender: `network-first` as const, + }, +]) + +// B=1 and B=2 distinguish below/at/above the source commit boundary. A saved +// three-row/empty-source history also needs more than one omission commit. +export const persistedStartupBatchHistories = uniqueStartupHistories( + [...persistedStartupHistories, ...multipleOmissionHistories].flatMap((h) => + [1 as const, 2 as const].flatMap((batchSize) => [ + { ...h, batchSize }, + ...(h.next !== 0 + ? [{ ...h, batchSize, sourceKeys: `prefixed` as const }] + : []), + ]), + ), +) + +export const persistedStartupSingleCommitHistories = [ + ...persistedStartupHistories, + ...multipleOmissionHistories, +].map((h) => ({ + ...h, + singleSourceCommit: true as const, +})) + // Before any source result, timeout changes readiness only. Saved rows and // ownership must survive intact, both publicly and durably. Reusing replacement // with the old membership/value derives this cut without a second state model. @@ -324,7 +474,10 @@ export function persistedStartupViolations( scope: StartupScope, observation: PersistedStartupObservation, ): Array { - const violations = persistedStartupPremiseViolations(h, observation) + const violations = [ + ...persistedStartupPremiseViolations(h, observation), + ...persistedStartupBatchViolations(h, observation.sourceTransactions), + ] for (const cut of [`fallback`, `fallbackDurable`] as const) violations.push( ...persistedStartupFallbackViolations(h, scope, cut, observation[cut]), @@ -361,5 +514,6 @@ declareOracle({ persistedStartupCutViolations, persistedStartupPremiseViolations, persistedStartupViolations, + persistedStartupBatchViolations, ], }) diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index a13863512e..d992d02ba1 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -308,6 +308,12 @@ const staticOracleProperties = [ `adapter-conformance.eager-live-source.trailbase`, `adapter-conformance.eager-live-source.rxdb`, `adapter-conformance.eager-live-source.powersync`, + `adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled`, + `adapter-conformance.persisted-baseline-lifecycle.rxdb.node-sqlite`, + `adapter-conformance.persisted-baseline-lifecycle.rxdb.chromium-opfs`, + `adapter-conformance.persisted-baseline-lifecycle.powersync.controlled`, + `adapter-conformance.persisted-baseline-lifecycle.powersync.node-sqlite`, + `adapter-conformance.persisted-baseline-lifecycle.powersync.chromium-opfs`, `adapter-conformance.eager-live-source.electric`, `adapter-conformance.eager-live-source.convex`, `trailbase.lifecycle`, @@ -366,6 +372,44 @@ const staticOracleProperties = [ `query.persisted-write-precedence`, `query.retained-scoped-hydration`, `query.cached-peer-claim`, + `trailbase.lifecycle.boundaries`, + `trailbase.lifecycle.persisted-hydration.chromium-opfs`, + `adapter-conformance.eager-live-source.trailbase.chromium`, + `query.persisted-direct-ownership`, + `query.persisted-direct-ownership.chromium-opfs`, + `query.immediate-eager-materialization`, + `query.immediate-eager-materialization.chromium-opfs`, + `query.persisted-rejected-write-cutoff`, + `query.persisted-rejected-write-cutoff.chromium-opfs`, + `query.scope-omission-reacquisition`, + `query.scope-omission-reacquisition.chromium-opfs`, + `query-db.ownership.reacquisition`, + `query-db.ownership.published-owners`, + `query-db.ownership.direct-lifetime`, + `query-db.ownership.direct-order`, + `query-db.ownership.overlap`, + `query-db.ownership.application-failure`, + `query-db.ownership.handler-failure`, + `query-db.ownership.empty-result`, + `query-db.ownership.reentrant-receipt`, + `query-db.ownership.invalid-result`, + `query-db.ownership.deferred-result`, + `query-db.ownership.deferred-cleanup`, + `query-db.ownership.recovered-result`, + `query-db.ownership.eager-recovery`, + `query-db.ownership.retained-invalid`, + `query-db.ownership.overlapping-deferral`, + `query-db.ownership.independent-result`, + `query-db.ownership.older-fetch-merge`, + `query-db.ownership.retained-empty-work`, + `query-db.ownership.cold-owner-work`, + `query-db.ownership.superseded-result`, + `query-db.ownership.explicit-refetch`, + `query-db.ownership.refresh-overlap`, + `query-db.ownership.handler-identity`, + `query-db.ownership.work`, + `query-db.ownership.mutation-confirmation`, + `query-db.ownership.post-handler`, `adapter.rxdb.eager-reload`, `adapter.powersync.eager-reload`, `adapter.rxdb.capability.controlled`, diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index 3c665544fd..9659028209 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -14,6 +14,80 @@ import { registeredOracleProperties } from './oracle-config.js' const ownerGroups: ReadonlyArray< readonly [string, string, string] | readonly [string, string, string, string] > = [ + [ + `query-db-collection/tests/scope-omission-reacquisition-oracle.test.ts`, + `query`, + `scope-omission-reacquisition`, + `query-db-collection/tests/scope-omission-reacquisition-oracle.ts`, + ], + [ + `browser-db-sqlite-persistence/e2e/query-scope-omission-oracle.browser.test.ts`, + `query`, + `scope-omission-reacquisition.chromium-opfs`, + `query-db-collection/tests/scope-omission-reacquisition-oracle.ts`, + ], + [ + `rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts`, + `adapter-conformance.persisted-baseline-lifecycle.rxdb`, + `controlled node-sqlite`, + ], + [ + `powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts`, + `adapter-conformance.persisted-baseline-lifecycle.powersync`, + `controlled node-sqlite`, + ], + [ + `browser-db-sqlite-persistence/tests/persisted-baseline-lifecycle-oracle.test.ts`, + `adapter-conformance.persisted-baseline-lifecycle`, + `rxdb.chromium-opfs powersync.chromium-opfs`, + ], + [ + `query-db-collection/tests/persisted-rejected-write-cutoff-oracle.test.ts`, + `query`, + `persisted-rejected-write-cutoff`, + ], + [ + `browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-oracle.browser.test.ts`, + `query`, + `persisted-rejected-write-cutoff.chromium-opfs`, + ], + [ + `query-db-collection/tests/persisted-direct-ownership-oracle.test.ts`, + `query`, + `persisted-direct-ownership`, + `query-db-collection/tests/persisted-direct-ownership-oracle.ts`, + ], + [ + `browser-db-sqlite-persistence/e2e/query-persisted-direct-ownership-oracle.browser.test.ts`, + `query`, + `persisted-direct-ownership.chromium-opfs`, + ], + [ + `query-db-collection/tests/immediate-eager-materialization-oracle.test.ts`, + `query`, + `immediate-eager-materialization`, + `query-db-collection/tests/immediate-eager-materialization-oracle.ts`, + ], + [ + `browser-db-sqlite-persistence/e2e/query-immediate-cache-oracle.browser.test.ts`, + `query`, + `immediate-eager-materialization.chromium-opfs`, + ], + [ + `trailbase-db-collection/tests/lifecycle-oracle.property.test.ts`, + `trailbase.lifecycle`, + `boundaries`, + ], + [ + `browser-db-sqlite-persistence/tests/trailbase-hydration-oracle.test.ts`, + `trailbase.lifecycle.persisted-hydration`, + `chromium-opfs`, + ], + [ + `browser-db-sqlite-persistence/tests/trailbase-eager-live-source-oracle.test.ts`, + `adapter-conformance.eager-live-source.trailbase`, + `chromium`, + ], [ `../scripts/legacy-campaign-baseline-oracle.property.test.js`, `scripts`, @@ -286,6 +360,31 @@ const ownerGroups: ReadonlyArray< `trailbase`, `lifecycle lifecycle.persisted-hydration.controlled lifecycle.persisted-hydration.node-sqlite`, ], + [ + `query-db-collection/tests/ownership-lifecycle.oracle.test.ts`, + `query-db.ownership`, + `reacquisition published-owners direct-lifetime direct-order overlap`, + ], + [ + `query-db-collection/tests/ownership-settlement.oracle.test.ts`, + `query-db.ownership`, + `application-failure handler-failure empty-result reentrant-receipt invalid-result deferred-result deferred-cleanup recovered-result eager-recovery retained-invalid overlapping-deferral independent-result`, + ], + [ + `query-db-collection/tests/ownership-bottom.oracle.test.ts`, + `query-db.ownership`, + `older-fetch-merge retained-empty-work cold-owner-work superseded-result`, + ], + [ + `query-db-collection/tests/ownership-upper.oracle.test.ts`, + `query-db.ownership`, + `explicit-refetch refresh-overlap handler-identity work`, + ], + [ + `query-db-collection/tests/ownership-confirmation.oracle.test.ts`, + `query-db.ownership`, + `mutation-confirmation post-handler`, + ], [ `query-db-collection/tests/persisted-precedence-oracle.test.ts`, `query`, @@ -302,7 +401,6 @@ const ownerGroups: ReadonlyArray< `${adapter}-db-collection/tests/eager-reload-conformance-oracle.property.test.ts`, `adapter.${adapter}`, `eager-reload`, - `db-adapter-conformance/tests/eager-reload-oracle-suite.ts`, ] as const, ), ...[`rxdb`, `powersync`].map( @@ -1084,6 +1182,14 @@ export type OracleReplayManifestEntry = { // Pagination cells carry their property name in the test title so the guard // can select one cell without running the rest of the matrix. const replayTestNames = new Map([ + [ + `query-db.ownership.mutation-confirmation`, + `query-db\\.ownership\\.mutation-confirmation.*random seed`, + ], + [ + `query-db.ownership.post-handler`, + `query-db\\.ownership\\.post-handler.*random seed`, + ], [ `query.persisted-write-precedence`, `query\\.persisted-write-precedence.*random seed`, @@ -1366,6 +1472,37 @@ const reseededProperties = new Set([`evaluators.like`]) // Keep their literal registry names here; the owner groups still supply their // receiving files and any shared source module. const enumeratedProperties = new Set([ + ...[`rxdb`, `powersync`].flatMap((adapter) => + [`controlled`, `node-sqlite`].map( + (host) => `adapter.${adapter}.capability.${host}`, + ), + ), + `trailbase.lifecycle.boundaries`, + `query-db.ownership.reacquisition`, + `query-db.ownership.published-owners`, + `query-db.ownership.direct-lifetime`, + `query-db.ownership.direct-order`, + `query-db.ownership.overlap`, + `query-db.ownership.application-failure`, + `query-db.ownership.handler-failure`, + `query-db.ownership.empty-result`, + `query-db.ownership.reentrant-receipt`, + `query-db.ownership.invalid-result`, + `query-db.ownership.deferred-result`, + `query-db.ownership.deferred-cleanup`, + `query-db.ownership.recovered-result`, + `query-db.ownership.eager-recovery`, + `query-db.ownership.retained-invalid`, + `query-db.ownership.overlapping-deferral`, + `query-db.ownership.independent-result`, + `query-db.ownership.older-fetch-merge`, + `query-db.ownership.retained-empty-work`, + `query-db.ownership.cold-owner-work`, + `query-db.ownership.superseded-result`, + `query-db.ownership.explicit-refetch`, + `query-db.ownership.refresh-overlap`, + `query-db.ownership.handler-identity`, + `query-db.ownership.work`, `reverse-index.large-reads`, `preact.bound-options`, `preact.bound-schema-options`, @@ -1374,8 +1511,32 @@ const enumeratedProperties = new Set([ ]) function directReplayTestName(property: string): string | undefined { + if (property.startsWith(`adapter-conformance.persisted-baseline-lifecycle.`)) + return `${property.replaceAll(`.`, `\\.`)}.*random seed` if (enumeratedProperties.has(property)) return `${property.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`)} > ` + if ( + [ + `query.persisted-direct-ownership`, + `query.persisted-direct-ownership.chromium-opfs`, + `query.immediate-eager-materialization`, + `query.immediate-eager-materialization.chromium-opfs`, + `query.persisted-rejected-write-cutoff`, + `query.persisted-rejected-write-cutoff.chromium-opfs`, + `query.scope-omission-reacquisition`, + `query.scope-omission-reacquisition.chromium-opfs`, + `trailbase.lifecycle`, + `trailbase.page-event`, + `trailbase.lifecycle.persisted-hydration.controlled`, + `trailbase.lifecycle.persisted-hydration.node-sqlite`, + `trailbase.lifecycle.persisted-hydration.chromium-opfs`, + `adapter-conformance.eager-live-source.trailbase`, + `adapter-conformance.eager-live-source.trailbase.chromium`, + `adapter-conformance.eager-live-source.powersync`, + `adapter-conformance.eager-live-source.rxdb`, + ].includes(property) + ) + return `${property.replaceAll(`.`, `\\.`)}.*random seed` if (property.startsWith(`pagination.limit-admission-`)) { const suffix = property.slice(`pagination.limit-admission-`.length) const negative = suffix.endsWith(`-negative`) @@ -1439,6 +1600,34 @@ const outsideReplayConfigs = new Map([ * runs only under its own config. */ const ownerReplayConfigs = new Map([ + [ + `packages/browser-db-sqlite-persistence/e2e/query-scope-omission-oracle.browser.test.ts`, + `vitest.adapter-oracles.config.ts`, + ], + [ + `packages/browser-db-sqlite-persistence/tests/persisted-baseline-lifecycle-oracle.test.ts`, + `vitest.adapter-oracles.config.ts`, + ], + [ + `packages/browser-db-sqlite-persistence/e2e/query-rejected-write-cutoff-oracle.browser.test.ts`, + `vitest.adapter-oracles.config.ts`, + ], + [ + `packages/browser-db-sqlite-persistence/e2e/query-persisted-direct-ownership-oracle.browser.test.ts`, + `vitest.adapter-oracles.config.ts`, + ], + [ + `packages/browser-db-sqlite-persistence/e2e/query-immediate-cache-oracle.browser.test.ts`, + `vitest.adapter-oracles.config.ts`, + ], + [ + `packages/browser-db-sqlite-persistence/tests/trailbase-hydration-oracle.test.ts`, + `vitest.adapter-oracles.config.ts`, + ], + [ + `packages/browser-db-sqlite-persistence/tests/trailbase-eager-live-source-oracle.test.ts`, + `vitest.adapter-oracles.config.ts`, + ], [ `packages/db-adapter-conformance/tests/neon-realtime-oracle.property.test.ts`, `vitest.neon.config.ts`, diff --git a/packages/db/tests/oracle-replay.test.ts b/packages/db/tests/oracle-replay.test.ts index e5bccc42db..a65e1471d4 100644 --- a/packages/db/tests/oracle-replay.test.ts +++ b/packages/db/tests/oracle-replay.test.ts @@ -640,6 +640,36 @@ describe(`named oracle replay manifest`, () => { expect(source).toContain( 'electric.sdk-resume-checkpoint.${tagged}.${consumer}.${emptySecond}.${delivery}', ) + } else if ( + /^adapter\.(rxdb|powersync)\.capability\.(controlled|node-sqlite)$/.test( + entry.property, + ) + ) { + const [, adapter, , host] = entry.property.split(`.`) + expect([`rxdb`, `powersync`]).toContain(adapter) + expect([`controlled`, `node-sqlite`]).toContain(host) + expect(source).toContain('id: `adapter.${name}.capability.${host}`') + expect(source).toContain( + 'for (const host of [`controlled`, `node-sqlite`] as const)', + ) + const receivingSource = readFileSync( + resolve(repositoryDirectory, entry.file), + `utf8`, + ) + expect(receivingSource).toContain( + `registerCapabilityConformance(\`${adapter}\`,`, + ) + } else if ( + /^trailbase\.lifecycle\.persisted-hydration\.(controlled|node-sqlite)$/.test( + entry.property, + ) + ) { + expect(source).toContain( + '`trailbase.lifecycle.persisted-hydration.${host}`', + ) + expect(source).toContain( + 'for (const host of [`controlled`, `node-sqlite`] as const)', + ) } else if (entry.property.startsWith(`react.ssr-effects-`)) { const [runtime, ...parts] = entry.property .slice(`react.ssr-effects-`.length) diff --git a/packages/powersync-db-collection/src/powersync.ts b/packages/powersync-db-collection/src/powersync.ts index 817cb37f07..2c557ccde7 100644 --- a/packages/powersync-db-collection/src/powersync.ts +++ b/packages/powersync-db-collection/src/powersync.ts @@ -352,6 +352,11 @@ function createPowerSyncCollectionConfig< } const abortController = new AbortController() + // Persisted first startup keeps ordinary batches even after timeout + // fallback. A retained reload keeps its atomic replacement. + const replacesRetainedRows = + collection.health.reloading && + !collection._state.acceptedSyncedEntries().next().done let disposeTracking: ((options?: { context?: LockContext }) => Promise) | null = null let trackingSetup: Promise | null = null @@ -436,6 +441,8 @@ function createPowerSyncCollectionConfig< // The load reads every row and replaces the rows the Collection // holds. snapshot?: boolean + afterRead?: () => Promise + onInitialRow?: (value: OutputType) => void when: Record writeType: (rowId: string) => OperationType batchQuery: ( @@ -446,7 +453,15 @@ function createPowerSyncCollectionConfig< }, appliedReceipts: Array, ) { - const { setupContext, snapshot, when, writeType, batchQuery } = options + const { + setupContext, + snapshot, + afterRead, + onInitialRow, + when, + writeType, + batchQuery, + } = options return await database.triggers.createDiffTrigger({ source: viewName, @@ -456,21 +471,22 @@ function createPowerSyncCollectionConfig< when, hooks: { beforeCreate: async (context) => { - // A load of every row is one snapshot transaction: during a - // reload it replaces the kept rows, so a row the table no - // longer has goes. A subset load commits each batch. A + // A retained reload uses one snapshot across its source reads. + // Other initial loads and subset loads keep batch commits. A // transaction begins only once its rows are read, so a batch // that fails leaves none open. const batches: Array> = [] const apply = () => { begin({ snapshot }) for (const row of batches.splice(0).flat()) { - write({ - type: writeType(row.id), - value: deserializeSyncRow(row), - }) + const type = writeType(row.id) + const value = deserializeSyncRow(row) + onInitialRow?.(value) + write({ type, value }) } - appliedReceipts.push(commit()) + const applied = commit() + if (applied !== true) void applied.catch(() => undefined) + appliedReceipts.push(applied) } let currentBatchCount = syncBatchSize let cursor = 0 @@ -486,6 +502,7 @@ function createPowerSyncCollectionConfig< if (!snapshot) apply() } if (snapshot) apply() + if (afterRead) await afterRead() database.logger.log({ level: LogLevels.info, message: `Sync is ready for ${viewName} into ${trackedTableName}`, @@ -632,18 +649,59 @@ function createPowerSyncCollectionConfig< } const appliedReceipts: Array = [] + const listed = + persistence && !replacesRetainedRows ? new Set() : undefined await establishTracking( { - // Replace retained rows, including persisted rows whose restore - // has not completed when this load starts. + // Retained reloads replace atomically; fresh persisted startup + // keeps source batches and reconciles only restored omissions. snapshot: - persistence !== null || - !collection._state.acceptedSyncedEntries().next().done, + persistence === null + ? !collection._state.acceptedSyncedEntries().next().done + : replacesRetainedRows, + afterRead: + listed && persistence + ? async () => { + await persistence.hydrateBaseline() + if (abortController.signal.aborted) return + // Copy omitted keys before committing: a synchronous + // commit can change the accepted-row iterator's backing map. + // Source rows remain batched; only omitted keys are retained. + const gone: Array = [] + for (const [ + key, + ] of collection._state.acceptedSyncedEntries()) { + if (!listed.has(key)) gone.push(key) + } + listed.clear() + for ( + let first = 0; + first < gone.length; + first += syncBatchSize + ) { + begin() + for (const key of gone.slice( + first, + first + syncBatchSize, + )) + write({ type: `delete`, key }) + const applied = commit() + if (applied !== true) + void applied.catch(() => undefined) + appliedReceipts.push(applied) + } + } + : undefined, when: { [DiffTriggerOperation.INSERT]: `TRUE`, [DiffTriggerOperation.UPDATE]: `TRUE`, [DiffTriggerOperation.DELETE]: `TRUE`, }, + onInitialRow: listed + ? (value) => { + listed.add(asPowerSyncRecord(value).id) + } + : undefined, writeType: (_rowId: string) => `insert`, batchQuery: ( lockContext: LockContext, diff --git a/packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts b/packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts index 5aa52a2e0a..e6407f824c 100644 --- a/packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts +++ b/packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts @@ -1,85 +1,51 @@ -import { fc } from '@fast-check/vitest' -import { describe, it } from 'vitest' -import { - oracleFixedSeed, - oraclePropertyOptions, - oracleReplayTest, - oracleRoute, - oracleRouteOf, - oracleRuns, -} from '../../db/tests/oracle-config' +/** + * The shared eager-source model independently judges the original status, + * health, rows, field values, event-fold and every publication checkpoint. + * PowerSync runs over real local SQLite through @powersync/node. Its typed + * columns cover string, integer, float, boolean and Date. Its 15-significant- + * digit storage exclusion remains unchanged; the diff-trigger propagation cut + * and per-batch publication law remain. No sync-service claim is made. + * Five generated constructions enforce the original minimum-five reach. Each + * builder returns fresh identities and preserves generated input variation. + */ +import { oracleProperty } from '../../db/tests/oracle-property' import { declareOracle } from '../../db/tests/oracle-declaration' -import { createOracleReach } from '../../db/tests/oracle-reach' import { checkEagerLiveSourceHistory, - eagerLiveSourceHistoryArb, eagerLiveSourceModel, - eagerLiveSourceProperty, - reachStatesFor, } from '../../db-adapter-conformance/src' +import { eagerLiveSourceCampaign } from '../../db-adapter-conformance/tests/eager-live-source-oracle-suite' +import { constructPowerSyncEagerHistory } from './eager-live-source-history-oracle' import { powerSyncConformanceDriver } from './conformance-driver' -/** - * # PowerSync on the shared eager live-source conformance oracle - * - * A receiving driver for `packages/db-adapter-conformance`. The shared - * module states the laws, the model, the history grammar, and the check; - * this file supplies only the PowerSync production driver in - * `conformance-driver.ts` and the campaigns. It states no expected results. - * - * PowerSync runs here over a real local SQLite database. It declares none of - * the oracle's optional capabilities: it reads its table once per sync run, - * its changes carry no txid, and it has no provider failure after ready. Its - * propagation step is the diff-trigger watch, which the fixture's `settle` - * awaits. The claim is limited to local SQLite writes; a claim about the - * PowerSync sync service needs a receiving witness against it. - */ - -const property = `adapter-conformance.eager-live-source.powersync` -const driver = powerSyncConformanceDriver() -const historyArb = eagerLiveSourceHistoryArb(driver) -// `field-removed` needs a held row with a note and a later upsert without -// one: 100 histories reached it only four times. Each history runs on a -// fresh SQLite database and waits for the change watcher after every batch, -// so 400 histories take about a minute. -const runs = 400 -// Startup bursts lengthen histories, so a campaign takes several seconds. -const campaignTimeoutMs = 600_000 - -async function campaign(options: fc.Parameters<[unknown]>) { - const reach = createOracleReach(property, reachStatesFor(driver), { - minimum: 5, - }) - oracleRoute(oracleRouteOf(options)) - await fc.assert(eagerLiveSourceProperty(driver, reach), options) - return reach -} - -describe(`eager live-source conformance: PowerSync`, () => { - oracleReplayTest(it)( - `conforms on a fixed-seed campaign`, - async () => { - const reach = await campaign( - oracleFixedSeed(2118, { numRuns: oracleRuns(runs) }), - ) - reach.assertReached() - }, - campaignTimeoutMs, - ) - - oracleReplayTest(it, property)( - `conforms on a random campaign or a replayed seed`, - async () => { - const reach = await campaign(oraclePropertyOptions(runs, property)) - reach.assertReached() - }, - campaignTimeoutMs, - ) +const campaign = eagerLiveSourceCampaign( + powerSyncConformanceDriver(), + constructPowerSyncEagerHistory, +) +oracleProperty({ + id: `adapter-conformance.eager-live-source.powersync`, + law: { + statement: `Initial readiness, complete row and field fidelity, coherent change publications, failure identity, post-ready health, restart and exactly-once resource release follow the shared eager live-source laws.`, + authority: `db-adapter-conformance/src/eager-live-source-oracle.ts; adapter law set READY-2/3/4/8/10, HEALTH-1/5, PUB-1/2/3/5/9, FID-1/4, LIFE-2/5/10, as supported by this receiver's declarations.`, + owner: `Adapter conformance: eager live sources (#2118), powersync receiver`, + limits: `Real local PowerSync SQLite, eager mode, per-batch changes, five typed field kinds, original 15-significant-digit numeric exclusion and twelve-step grammar. No remote sync-service, mutation, reload or on-demand claim.`, + }, + histories: { + arbitrary: campaign.arbitrary, + states: campaign.states, + runs: 400, + fixedSeeds: [2118], + constructed: campaign.constructed, + }, + model: campaign.model, + driver: campaign.driver, + check: campaign.check, + calibrations: campaign.calibrations, + timeout: 600000, }) - declareOracle({ owns: [`src/powersync.ts#powerSyncCollectionOptions`], - check: [checkEagerLiveSourceHistory, eagerLiveSourceProperty], + check: [checkEagerLiveSourceHistory, campaign.check], model: [eagerLiveSourceModel.step, eagerLiveSourceModel.expected], - grammar: [historyArb], + grammar: [campaign.arbitrary], }) diff --git a/packages/powersync-db-collection/tests/eager-live-source-history-oracle.ts b/packages/powersync-db-collection/tests/eager-live-source-history-oracle.ts new file mode 100644 index 0000000000..0e7b108b71 --- /dev/null +++ b/packages/powersync-db-collection/tests/eager-live-source-history-oracle.ts @@ -0,0 +1,49 @@ +/** + * PowerSync constructions preserve its twelve-step grammar and all original + * fields. A failed initial gate and restart precede typed initial/live rows. + * Generated coordinates and suffixes remain random; the shared model owns + * expected row truth. Every invocation returns fresh rows and arrays. + */ +import { describeValue } from '../../db-adapter-conformance/src' +import type { + ConformanceRow, + EagerLiveSourceHistory, +} from '../../db-adapter-conformance/src' + +export function constructPowerSyncEagerHistory( + generated: EagerLiveSourceHistory, + salt: number, +): EagerLiveSourceHistory { + let hash = salt + 1 + for (const char of describeValue(generated)) + hash = (Math.imul(hash, 31) + char.charCodeAt(0)) >>> 0 + const row = (index: number, note: boolean): ConformanceRow => ({ + id: ((hash + index) % 5) + 1, + title: ([`a`, `b`, `c`] as const)[(hash + index) % 3]!, + ...(note ? { note: hash % 2 ? `x` : `y` } : {}), + typed: { + string: [``, `plain`, `two\nlines`, `ünïcødé ✓`][(hash + index) % 4]!, + integer: [0, -7, 2 ** 31][(hash + index) % 3]!, + float: [0.5, -1.25, 1e-7][(hash + index) % 3]!, + boolean: (hash + index) % 2 === 0, + date: new Date( + [0, Date.UTC(2026, 9, 29, 12, 34, 56, 789), -86400000][ + (hash + index) % 3 + ]!, + ), + }, + }) + return [ + { kind: `fail` }, + { kind: `restart` }, + { kind: `snapshot`, rows: [row(0, true)] }, + { + kind: `changes`, + changes: [ + { type: `upsert`, row: row(0, false) }, + { type: `upsert`, row: row(1, false) }, + ], + }, + ...structuredClone(generated.slice(0, 8)), + ] +} diff --git a/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-browser.ts b/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-browser.ts new file mode 100644 index 0000000000..6a01da53b1 --- /dev/null +++ b/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-browser.ts @@ -0,0 +1,19 @@ +import { PowerSyncDatabase } from '@powersync/web' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { powerSyncBaselineLifecycleAdapterFor } from './persisted-baseline-lifecycle-common' + +export const powerSyncBrowserBaselineLifecycleAdapter = + powerSyncBaselineLifecycleAdapterFor((schema) => { + const database = new PowerSyncDatabase({ + schema, + database: { dbFilename: `powersync-baseline-lifecycle.sqlite` }, + }) + return Promise.resolve({ + database, + dispose: () => + withOracleCleanup( + () => database.disconnectAndClear(), + [() => database.close()], + ), + }) + }) diff --git a/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-common.ts b/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-common.ts new file mode 100644 index 0000000000..ce27256588 --- /dev/null +++ b/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-common.ts @@ -0,0 +1,106 @@ +/** Real PowerSync SQLite supplies rows; its public onLoad gate orders runs. */ +import { LogLevels, Schema, Table, column } from '@powersync/common' +import { z } from 'zod' +import { baselineGate } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-driver' +import { hydrationLifecycleRows } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' +import { powerSyncCollectionOptions } from '../src' +import type { CommonPowerSyncDatabase } from '@powersync/common' +import type { BaselineLifecycleAdapter } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-driver' +import type { HydrationLifecycleRow } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' + +const schema = new Schema({ rows: new Table({ value: column.integer }) }) +export function powerSyncBaselineLifecycleAdapterFor( + open: (schema: Schema) => Promise<{ + database: CommonPowerSyncDatabase + dispose: () => Promise + }>, +): BaselineLifecycleAdapter { + return async (h, id) => { + const { database, dispose } = await open(schema) + const diagnostics: Array<{ message: string; error: unknown }> = [] + const log = database.logger.log.bind(database.logger) + database.logger.log = (entry) => { + if (entry.level === LogLevels.error) + // The provider generates a random private tracking-table suffix per run. + // Record the diagnostic relation, error identity, order and count; + // neither the public failure law nor its check depends on that name. + diagnostics.push({ + message: String(entry.message).replace( + /__rows_tracking_[0-9a-f]+/g, + `__rows_tracking_`, + ), + error: entry.error, + }) + else log(entry) + } + const replace = (rows: Array) => + database.writeTransaction(async (tx) => { + await tx.execute(`DELETE FROM rows`) + for (const row of rows) + await tx.execute(`INSERT INTO rows (id, value) VALUES (?, ?)`, [ + row.id, + row.value, + ]) + }) + await replace(hydrationLifecycleRows(h.old, 2)) + const sessions: Array<{ releases: number }> = [] + const hooks: Array<{ + gate: ReturnType> + releases: number + acquired: boolean + }> = [] + const register = database.onChangeWithCallback.bind(database) + database.onChangeWithCallback = (handler, options) => { + const session = { releases: 0 } + sessions.push(session) + options?.signal?.addEventListener( + `abort`, + () => { + session.releases++ + }, + { once: true }, + ) + return register(handler, options) + } + const options = powerSyncCollectionOptions({ + id, + database, + table: schema.props.rows, + schema: z.object({ id: z.string(), value: z.number() }), + syncBatchSize: h.batchSize, + onDeserializationError: () => {}, + onLoad: async () => { + const hook = { + gate: baselineGate(), + releases: 0, + acquired: false, + } + hooks.push(hook) + await hook.gate.promise + hook.acquired = true + return () => { + hook.releases++ + } + }, + }) + return { + options, + sessions: () => hooks.length, + deliver: (run) => hooks[run]?.gate.resolve(), + fail: (run, error) => hooks[run]!.gate.reject(error), + replace, + event: async (row) => { + await database.execute(`INSERT INTO rows (id, value) VALUES (?, ?)`, [ + row.id, + row.value, + ]) + }, + releases: () => sessions.map((session) => session.releases), + hookReleases: () => + hooks.filter((hook) => hook.acquired).map((hook) => hook.releases), + hookAcquisitions: () => hooks.filter((hook) => hook.acquired).length, + diagnostics: () => diagnostics, + dispose, + } + } +} diff --git a/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-driver.ts b/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-driver.ts new file mode 100644 index 0000000000..951d2f30a5 --- /dev/null +++ b/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-driver.ts @@ -0,0 +1,35 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { PowerSyncDatabase } from '@powersync/node' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { powerSyncBaselineLifecycleAdapterFor } from './persisted-baseline-lifecycle-common' +import { TEST_DATABASE_IMPLEMENTATION } from './test-db-implementation' + +export const powerSyncBaselineLifecycleAdapter = + powerSyncBaselineLifecycleAdapterFor(async (schema) => { + if (!TEST_DATABASE_IMPLEMENTATION) + throw new Error(`No PowerSync SQLite implementation`) + const directory = await mkdtemp( + join(tmpdir(), `powersync-baseline-lifecycle-`), + ) + const database = new PowerSyncDatabase({ + schema, + database: { + dbFilename: `source.sqlite`, + dbLocation: directory, + implementation: TEST_DATABASE_IMPLEMENTATION, + }, + }) + return { + database, + dispose: () => + withOracleCleanup( + () => database.disconnectAndClear(), + [ + () => database.close(), + () => rm(directory, { recursive: true, force: true }), + ], + ), + } + }) diff --git a/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts b/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts new file mode 100644 index 0000000000..4ca97e5c24 --- /dev/null +++ b/packages/powersync-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts @@ -0,0 +1,47 @@ +import { declareOracle } from '../../db/tests/oracle-declaration' +import { baselineLifecycleViolations } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' +import { observeBaselineLifecycle } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-driver' +import { registerBaselineLifecycle } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle-suite' +import { powerSyncBaselineLifecycleAdapter } from './persisted-baseline-lifecycle-driver' + +declareOracle({ + owns: [`src/powersync.ts#powerSyncCollectionOptions`], + check: [baselineLifecycleViolations], +}) + +for (const { host, config } of [ + { + host: `controlled`, + config: { + property: `adapter-conformance.persisted-baseline-lifecycle.powersync.controlled`, + defaultRuns: 24, + streamFailure: false, + }, + }, + { + host: `node-sqlite`, + config: { + property: `adapter-conformance.persisted-baseline-lifecycle.powersync.node-sqlite`, + defaultRuns: 24, + streamFailure: false, + }, + }, +] as const) { + registerBaselineLifecycle(config, async (h, id) => { + const storage = createStartupHost(host) + return withOracleCleanup( + () => + observeBaselineLifecycle( + powerSyncBaselineLifecycleAdapter, + storage.persistence, + storage.holdBaseline, + h, + id, + process.env.TANSTACK_DB_BASELINE_EXPECT_HYDRATE !== `0`, + ), + [() => storage.close()], + ) + }) +} diff --git a/packages/powersync-db-collection/tests/persisted-startup-common.ts b/packages/powersync-db-collection/tests/persisted-startup-common.ts index 7a4571591c..be46b9db26 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-common.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-common.ts @@ -5,6 +5,8 @@ * source SQLite is not a reason to waive replacement in a wrapped Collection. * Reload reads the unchanged table again. Remote PowerSync replication is * outside this local-table witness. + * A representation axis prefixes provider IDs and deserializes them back to + * the same model Collection keys. Expected rows do not depend on that encoding. */ import { Schema, Table, column } from '@powersync/common' import { createCollection } from '@tanstack/db' @@ -37,10 +39,13 @@ export function powerSyncStartupAdapterFor( scope: `collection`, async create(persistence, history, id, metadataTransform) { const { database, dispose } = await open(schema) + const sourceKey = (key: string) => + history.sourceKeys ? `provider:${key}` : key + const deserializedSourceKeys = new Set() await database.writeTransaction(async (tx) => { for (const row of startupRows(history.next, 2)) await tx.execute(`INSERT INTO rows (id, value) VALUES (?, ?)`, [ - row.id, + sourceKey(row.id), row.value, ]) }) @@ -49,8 +54,21 @@ export function powerSyncStartupAdapterFor( id, database, table: schema.props.rows, - syncBatchSize: 1, + syncBatchSize: history.batchSize ?? 1, schema: rowSchema, + ...(history.sourceKeys + ? { + deserializationSchema: z.object({ + id: z.string().transform((key) => { + if (!key.startsWith(`provider:`)) + throw new Error(`PowerSync source key encoding was lost`) + deserializedSourceKeys.add(key) + return key.slice(`provider:`.length) + }), + value: z.number(), + }), + } + : {}), onDeserializationError: () => {}, onLoad: () => new Promise((resolve) => { @@ -81,7 +99,15 @@ export function powerSyncStartupAdapterFor( syncOutcome: startup.syncOutcome, initialResult: startup.initialResult, firstSubmission: startup.firstSubmission, + sourceTransactions: startup.sourceTransactions, waitForRestore: startup.waitForRestore, + checkStartup: () => + history.sourceKeys && + startupRows(history.next, 2).some( + (row) => !deserializedSourceKeys.has(sourceKey(row.id)), + ) + ? [`PowerSync source-key transformation was not reached`] + : [], snapshot: () => ({ rows: sortedStartupRows(collection.values()) }), deliver, responseAvailable, diff --git a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index 7d5b31af3f..27eab097b8 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -5,13 +5,13 @@ import { describe } from 'vitest' import { declareOracle } from '../../db/tests/oracle-declaration' import { - persistedStartupHistories, + persistedStartupBatchHistories, persistedStartupHistoryArb, } from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { powerSyncStartupAdapter } from './persisted-startup-driver' -const historyArb = persistedStartupHistoryArb(`collection`) +const historyArb = persistedStartupHistoryArb(`collection`, true, false, true) // The receiving path judges persisted startup through this adapter entry. // The shared model owns replacement; this list lets the gap CLI credit it. declareOracle({ @@ -22,7 +22,7 @@ describe(`persisted startup conformance: PowerSync`, () => { registerPersistedStartupConformance({ property: `adapter.powersync.persisted-startup`, adapter: powerSyncStartupAdapter, - histories: persistedStartupHistories, + histories: persistedStartupBatchHistories, arbitrary: historyArb, scopedStartupCommit: true, }) diff --git a/packages/query-db-collection/package.json b/packages/query-db-collection/package.json index ef4b5cee8d..29c30996d4 100644 --- a/packages/query-db-collection/package.json +++ b/packages/query-db-collection/package.json @@ -21,7 +21,7 @@ "dev": "vite build --watch", "lint": "eslint . --fix", "test": "vitest run", - "test:oracles": "vitest run --coverage.enabled=false tests/includes-work-counter-oracle.test.ts tests/ownership-lifecycle.oracle.test.ts tests/persisted-direct-write-window.oracle.test.ts tests/persisted-precedence-oracle.test.ts tests/retained-scoped-hydration-oracle.test.ts tests/ssr-dehydration-oracle.test.ts tests/cursor-pagination.oracle.test.ts tests/cursor-pagination.cache-oracle.test.ts tests/cursor-pagination.publication-oracle.test.ts tests/cursor-pagination.boundary-oracle.test.ts tests/cursor-pagination-oracle.integration.test.ts tests/persisted-startup-conformance-oracle.test.ts tests/offline-executor-refresh-oracle.test.ts", + "test:oracles": "vitest run --coverage.enabled=false tests/includes-work-counter-oracle.test.ts tests/ownership-lifecycle.oracle.test.ts tests/ownership-settlement.oracle.test.ts tests/ownership-bottom.oracle.test.ts tests/ownership-upper.oracle.test.ts tests/ownership-confirmation.oracle.test.ts tests/ownership-models.test.ts tests/upper-model-checks.test.ts tests/persisted-direct-write-window.oracle.test.ts tests/persisted-precedence-oracle.test.ts tests/retained-scoped-hydration-oracle.test.ts tests/ssr-dehydration-oracle.test.ts tests/cursor-pagination.oracle.test.ts tests/cursor-pagination.cache-oracle.test.ts tests/cursor-pagination.publication-oracle.test.ts tests/cursor-pagination.boundary-oracle.test.ts tests/cursor-pagination-oracle.integration.test.ts tests/persisted-startup-conformance-oracle.test.ts tests/offline-executor-refresh-oracle.test.ts tests/persisted-direct-ownership-oracle.test.ts tests/immediate-eager-materialization-oracle.test.ts tests/query-receiving-fidelity.test.ts tests/persisted-rejected-write-cutoff-oracle.test.ts tests/scope-omission-reacquisition-oracle.test.ts", "test:e2e": "vitest run --config vitest.e2e.config.ts" }, "type": "module", diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 04544c93bb..abf4824a8e 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -2735,9 +2735,8 @@ export function queryCollectionOptions( } previouslyOwnedRows.forEach((key) => { - const oldItem = shouldUsePersistedBaseline - ? persistedBaseline.get(key)?.value - : currentSyncedItems.get(key) + const oldItem = + persistedBaseline?.get(key)?.value ?? currentSyncedItems.get(key) const newItem = newItemsMap.get(key) if (!newItem) { const directPosition = unownedDirectWritePositions.get(key) diff --git a/packages/query-db-collection/tests/immediate-eager-materialization-driver.ts b/packages/query-db-collection/tests/immediate-eager-materialization-driver.ts new file mode 100644 index 0000000000..884afea3ad --- /dev/null +++ b/packages/query-db-collection/tests/immediate-eager-materialization-driver.ts @@ -0,0 +1,96 @@ +/** Observe the documented immediate nonpersisted startup publication. + * createCollection({startSync:true}) returns before this driver first awaits. + * The copied observation therefore cannot be repaired by a later microtask. + */ +import { QueryClient } from '@tanstack/query-core' +import { createCollection } from '@tanstack/db' +import { queryCollectionOptions } from '../src/query' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import type { + ImmediateMaterializationHistory, + ImmediateMaterializationTrace, + MaterializationResponse as Response, + MaterializedRow as Row, +} from './immediate-eager-materialization-oracle' + +const ordered = (rows: Array) => + rows + .map(({ id, value }) => ({ id, value })) + .sort((a, b) => a.id.localeCompare(b.id)) +export async function observeImmediateEagerMaterialization( + h: ImmediateMaterializationHistory, + id: string, + observe?: (checkpoint: string, value: unknown) => void, +): Promise { + const trace: ImmediateMaterializationTrace = [] + const record = (checkpoint: string, value: unknown) => { + trace.push({ checkpoint, value }) + observe?.(checkpoint, value) + } + const client = new QueryClient({ + defaultOptions: { + queries: { retry: false, staleTime: Infinity, gcTime: Infinity }, + }, + }) + const encode = (rows: Array): Response => + h.shape === 'raw' + ? rows.map((r) => ({ ...r })) + : { edges: rows.map((node) => ({ node: { ...node } })) } + const decode = (response: Response) => + Array.isArray(response) + ? response + : response.edges.map(({ node }) => ({ ...node })) + const queryKey = [id] + if (h.origin === 'cache') client.setQueryData(queryKey, encode(h.rows)) + let initializerCalls = 0, + sourceCalls = 0 + const initial = encode( + h.origin === 'initialData' + ? h.rows + : [{ id: 'initializer-only', value: 99 }], + ) + const collection = createCollection( + queryCollectionOptions({ + id, + queryKey, + queryClient: client, + getKey: (row: Row) => row.id, + startSync: true, + enabled: false, + staleTime: Infinity, + select: decode, + initialData: + h.initializer === 'literal' + ? initial + : () => { + initializerCalls++ + return initial + }, + queryFn: () => { + sourceCalls++ + return Promise.resolve(encode([])) + }, + }), + ) + // Capture before the first await. Cleanup cannot erase this publication cut. + record('creation-return', { + rows: ordered(Array.from(collection.values())), + status: collection.status, + sourceCalls, + initializerCalls, + }) + for (const state of [ + h.origin, + h.shape, + h.rows.length ? 'nonempty' : 'empty', + ...(h.initializer === 'function' ? ['function-initializer'] : []), + ...(h.origin === 'cache' ? ['cache-precedence'] : []), + ]) + record('reach', state) + await withOracleCleanup( + () => undefined, + [() => collection.cleanup(), () => client.clear()], + ) + record('completed', true) + return trace +} diff --git a/packages/query-db-collection/tests/immediate-eager-materialization-oracle.test.ts b/packages/query-db-collection/tests/immediate-eager-materialization-oracle.test.ts new file mode 100644 index 0000000000..1f2e7f1548 --- /dev/null +++ b/packages/query-db-collection/tests/immediate-eager-materialization-oracle.test.ts @@ -0,0 +1,15 @@ +/** Node receiver of the established synchronous eager materialization cut. */ +import { declareOracle } from '../../db/tests/oracle-declaration' +import { oracleProperty } from '../../db/tests/oracle-property' +import { immediateEagerMaterializationOptions } from './immediate-eager-materialization-oracle' +import { observeImmediateEagerMaterialization } from './immediate-eager-materialization-driver' + +const options = immediateEagerMaterializationOptions(async (h, id, observe) => { + await observeImmediateEagerMaterialization(h, id, observe) +}, 'node') +oracleProperty({ + ...options, + check: (h, trace, expected) => options.check(h, trace, expected), +}) + +declareOracle({ owns: ['src/query.ts#queryCollectionOptions'] }) diff --git a/packages/query-db-collection/tests/immediate-eager-materialization-oracle.ts b/packages/query-db-collection/tests/immediate-eager-materialization-oracle.ts new file mode 100644 index 0000000000..e6cc75eed0 --- /dev/null +++ b/packages/query-db-collection/tests/immediate-eager-materialization-oracle.ts @@ -0,0 +1,331 @@ +/** + * Shared Node and Chromium receiving judgment. + * Receiving extension of the primary Query cache-to-Collection projection law. + * + * Query guide Initial Data says nonpersisted eager initialData is immediately + * materialized. Existing exact-key cache takes precedence over an initializer. + * The ordinary cached-query receiver already checks rows and ready status when + * createCollection({startSync:true}) returns. This judgment protects that + * synchronous publication cut, rather than introducing preload promise timing. + * + * The primary ownership oracle owns acquisition and cache projection. This + * small input projection adds the previously missing first-startup observation: + * authored source rows determine rows, existing cache suppresses initialization, + * and a fresh disabled Query starts no request. It does not duplicate observer + * scheduling or invent another subsystem state machine. + * + * Histories cross initialData/cache, raw/derived response documents, + * literal/function initialization, and empty/nonempty unique numeric rows. + * No persistence, mutations, stale data, fetching, or promise timing is claimed. + */ +import fc from 'fast-check' +import { declareOracle } from '../../db/tests/oracle-declaration' +import type { + ConstructedHistory, + Observation, + OraclePropertyOptions, + Verdict, +} from '../../db/tests/oracle-property' + +export type MaterializedRow = { id: string; value: number } +export type MaterializationResponse = + Array | { edges: Array<{ node: MaterializedRow }> } +export type ImmediateMaterializationHistory = { + origin: 'initialData' | 'cache' + shape: 'raw' | 'derived' + initializer: 'literal' | 'function' + rows: Array +} +export type ImmediateMaterializationTrace = Array +export const immediateMaterializationHistoryArb = fc.record({ + origin: fc.constantFrom('initialData' as const, 'cache' as const), + shape: fc.constantFrom('raw' as const, 'derived' as const), + initializer: fc.constantFrom('literal' as const, 'function' as const), + rows: fc.uniqueArray( + fc.record({ + id: fc.constantFrom('1', '2', '3', '4'), + value: fc.integer({ min: -10, max: 10 }), + }), + { selector: (row) => row.id, maxLength: 4 }, + ), +}) +export const immediateMaterializationStates = [ + 'initialData', + 'cache', + 'raw', + 'derived', + 'empty', + 'nonempty', + 'function-initializer', + 'cache-precedence', +] as const +export const immediateMaterializationHistories: Array = + (['initialData', 'cache'] as const).flatMap((origin) => + (['raw', 'derived'] as const).flatMap((shape) => + (['literal', 'function'] as const).flatMap((initializer) => + [false, true].map((nonempty) => ({ + origin, + shape, + initializer, + rows: nonempty + ? [ + { id: '1', value: 7 }, + { id: '2', value: -3 }, + ] + : [], + })), + ), + ), + ) +const ordered = (rows: Array) => + rows + .map(({ id, value }) => ({ id, value })) + .sort((a, b) => a.id.localeCompare(b.id)) +// Inputs supply normalized authored rows. The response envelope affects the +// production extraction path, but must not change this projection's answer. +export const expectedImmediateMaterialization = ( + h: ImmediateMaterializationHistory, +) => ({ + rows: ordered(h.rows), + status: 'ready', + sourceCalls: 0, + initializerCalls: + h.origin === 'initialData' && h.initializer === 'function' ? 1 : 0, +}) +export type ImmediateMaterializationExpected = ReturnType< + typeof expectedImmediateMaterialization +> +export function checkImmediateMaterialization( + h: ImmediateMaterializationHistory, + trace: ReadonlyArray, + expected = expectedImmediateMaterialization(h), +): Verdict { + for (const o of trace) + if ( + o.checkpoint === 'creation-return' && + JSON.stringify(o.value) !== JSON.stringify(expected) + ) + return { + kind: 'violation', + checkpoint: o.checkpoint, + law: 'immediate eager materialization', + difference: + 'expected ' + + JSON.stringify(expected) + + ', observed ' + + JSON.stringify(o.value), + } + if (!trace.some((o) => o.checkpoint === 'completed')) + return { + kind: 'violation', + checkpoint: 'completed', + law: 'receiving completion', + difference: + 'receiving driver returned without completing its observations', + } + if (!trace.some((o) => o.checkpoint === 'creation-return')) + return { + kind: 'violation', + checkpoint: 'creation-return', + law: 'immediate receiving observation', + difference: 'the synchronous creation-return observation is missing', + } + return { kind: 'pass' } +} +export type ImmediateMaterializationReceiver = ( + h: ImmediateMaterializationHistory, + id: string, + observe: (checkpoint: string, value: unknown) => void, +) => Promise + +export function immediateEagerMaterializationOptions( + receive: ImmediateMaterializationReceiver, + receiver: string, +): OraclePropertyOptions< + ImmediateMaterializationHistory, + ImmediateMaterializationExpected +> { + const obligations = new WeakMap< + ImmediateMaterializationHistory, + ReadonlyArray + >() + // These are authored input premises, not production classifications. Fixed + // replays owe their observed reaches too; all generated builders below keep + // their separate fresh-output obligations. + for (const h of immediateMaterializationHistories) + obligations.set(h, [ + h.origin, + h.shape, + h.rows.length ? 'nonempty' : 'empty', + ...(h.initializer === 'function' ? ['function-initializer'] : []), + ...(h.origin === 'cache' ? ['cache-precedence'] : []), + ]) + const constructed: Array< + ConstructedHistory + > = immediateMaterializationStates.map((state) => ({ + name: state, + enters: [state], + build(h) { + const built = { ...h, rows: h.rows.map((row) => ({ ...row })) } + if (state === 'initialData' || state === 'cache') built.origin = state + if (state === 'raw' || state === 'derived') built.shape = state + if (state === 'empty') built.rows = [] + if (state === 'nonempty' && built.rows.length === 0) + built.rows = [{ id: '1', value: 0 }] + if (state === 'function-initializer') built.initializer = 'function' + if (state === 'cache-precedence') built.origin = 'cache' + obligations.set(built, [state]) + return built + }, + })) + let sequence = 0 + return { + id: 'query.immediate-eager-materialization', + law: { + statement: + 'Fresh nonpersisted eager initialData and existing exact-key cache materialize rows and readiness when synchronous startup returns.', + authority: + 'docs/collections/query-collection.md Initial Data; query.test.ts cached-query immediate-read receiver.', + owner: + 'packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts', + limits: + 'Fresh disabled eager Query, no persistence or mutations, raw/derived response, at most four unique string-keyed numeric rows. Creation-return cut; no preload promise timing claim.', + }, + histories: { + arbitrary: immediateMaterializationHistoryArb, + runs: 40, + fixedSeeds: [1468], + states: immediateMaterializationStates, + constructed, + fixed: immediateMaterializationHistories.map((history) => ({ + name: [ + history.origin, + history.shape, + history.initializer, + history.rows.length ? 'nonempty' : 'empty', + ].join('-'), + history, + })), + }, + model: expectedImmediateMaterialization, + driver: (h, observe) => + receive(h, 'immediate-' + receiver + '-' + sequence++, observe), + check: (h, trace, expected) => { + const product = checkImmediateMaterialization(h, trace, expected) + const errors = trace + .filter((o) => o.checkpoint === 'browserErrors') + .flatMap((o) => o.value as ReadonlyArray) + if (product.kind === 'violation') + return errors.length + ? { + ...product, + difference: + String(product.difference) + + '; secondary browser diagnostics: ' + + errors.join('; '), + } + : product + const reached = new Set( + trace.filter((o) => o.checkpoint === 'reach').map((o) => o.value), + ) + const missing = (obligations.get(h) ?? []).filter( + (state) => !reached.has(state), + ) + if (missing.length) + return { + kind: 'violation', + checkpoint: 'constructed states', + law: 'constructed reach', + difference: + missing.join(', ') + + (errors.length + ? '; secondary browser diagnostics: ' + errors.join('; ') + : ''), + } + return errors.length + ? { + kind: 'violation', + checkpoint: 'browser diagnostics', + law: 'detached-browser-error', + difference: errors.join('; '), + } + : { kind: 'pass' } + }, + calibrations: [ + { + name: 'rejects an empty normal return', + law: 'receiving completion', + corrupt: () => [], + }, + { + name: 'rejects a truncated normal return', + law: 'receiving completion', + corrupt: (trace) => trace.filter((o) => o.checkpoint !== 'completed'), + }, + ...immediateMaterializationStates.map((state) => ({ + name: 'omits constructed ' + state, + law: 'constructed reach', + corrupt: ( + trace: ReadonlyArray, + h: ImmediateMaterializationHistory, + ) => + obligations.get(h)?.includes(state) + ? trace.filter((o) => o.checkpoint !== 'reach' || o.value !== state) + : undefined, + })), + { + name: 'delays immediate rows', + law: 'immediate eager materialization', + corrupt: (trace, h) => + h.rows.length + ? trace.map((o) => + o.checkpoint === 'creation-return' + ? { ...o, value: { ...(o.value as object), rows: [] } } + : o, + ) + : undefined, + }, + { + name: 'delays ready status', + law: 'immediate eager materialization', + corrupt: (trace) => + trace.map((o) => + o.checkpoint === 'creation-return' + ? { ...o, value: { ...(o.value as object), status: 'loading' } } + : o, + ), + }, + { + name: 'reports detached browser error', + law: 'detached-browser-error', + corrupt: (trace) => [ + ...trace, + { checkpoint: 'browserErrors', value: ['detached browser failure'] }, + ], + }, + { + name: 'preserves product failure before browser diagnostics', + law: 'immediate eager materialization', + corrupt: (trace) => [ + ...trace.map((o) => + o.checkpoint === 'creation-return' + ? { ...o, value: { ...(o.value as object), status: 'loading' } } + : o, + ), + { + checkpoint: 'browserErrors', + value: ['secondary detached failure'], + }, + ], + }, + ], + } +} +declareOracle({ + model: [expectedImmediateMaterialization], + grammar: [ + immediateMaterializationHistoryArb, + immediateMaterializationHistories, + ], + check: [checkImmediateMaterialization], +}) diff --git a/packages/query-db-collection/tests/ownership-bottom.oracle.test.ts b/packages/query-db-collection/tests/ownership-bottom.oracle.test.ts new file mode 100644 index 0000000000..43b9f923cd --- /dev/null +++ b/packages/query-db-collection/tests/ownership-bottom.oracle.test.ts @@ -0,0 +1,716 @@ +/** + * # How do retained rows and late results cross an ownership boundary? + * + * An older fetch supplies untouched keys while a later accepted direct write + * supplies its own key. A superseded application cannot publish after its + * successor or fulfill its retired caller. The retained-baseline work rule is + * bounded: one unchanged sync run reuses established empty-baseline evidence; + * a cold run must check for durable owners committed while it was absent. + * These retain the original overlay law, result-settlement contract and + * explicitly bounded work proposal from the ownership oracle. + * + * Independent models use keyed overlay, a call ledger and a one-bit empty + * baseline history. Seven finite histories drive real QueryClient, Collection + * and controlled persistence paths. Every original public, durable, cache, + * publication and scan-count cut is recorded. They make no claim about native + * SQLite failure, arbitrary peer writes or a global scan complexity bound. + */ +import { hashKey } from '@tanstack/query-core' +import { createCollection } from '@tanstack/db' +import { afterEach, vi } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration.js' +import { + differs, + oracleProperty, + pass, + violation, +} from '../../db/tests/oracle-property.js' + +import { createDeferred } from '../../db/src/deferred.js' +import { queryCollectionOptions } from '../src/query.js' +import { + observeExplicitRefetchApplication, + reduceExplicitRefetchApplication, +} from './ownership-model-oracle' +import { mergeDirectWritesOverFetch } from './direct-write-precedence-oracle.js' +import { + cleanups, + createOwnershipFixture, + createOwnershipStorage, + createPersistedOwnershipFixture, + createQueryClient, + itemIds, + rows, + runCleanups, + shared, + storedItems, +} from './ownership-lifecycle-support.js' +import type { Observation, Verdict } from '../../db/tests/oracle-property.js' +import type { ExplicitRefetchApplicationModel } from './ownership-model-oracle' +import type { Item } from './ownership-lifecycle-support.js' + +function diagnosticField( + error: unknown, + field: 'name' | 'message', +): string | undefined { + if (typeof error !== 'object' || error === null || !(field in error)) + return undefined + const value = (error as Record)[field] + return typeof value === 'string' ? value : undefined +} +afterEach(runCleanups) +type RecordFact = (checkpoint: string, value: unknown) => void + +/** Wait for a production boundary without computing an expected row. If the + * boundary does not arrive, record the last fact and return; the property's + * check identifies that cut rather than losing the mismatch to a setup error. */ +async function boundary(test: () => boolean): Promise { + for (let turn = 0; turn < 200; turn++) { + if (test()) return true + await new Promise((resolve) => setTimeout(resolve, 5)) + } + return test() +} + +/** Domain checks retain every observation cut in the old driver. No expected + * value is recorded by a production callback: models build the owed facts + * from authored history data, and this check compares the ordered trace. */ +function checkCuts( + actual: ReadonlyArray, + expected: ReadonlyArray, + law: string, +): Verdict { + for (let i = 0; i < expected.length; i++) { + const wanted = expected[i]! + const got = actual[i] + if (!got) + return violation( + wanted.checkpoint, + 'the promised observation cut was not reached', + law, + ) + const difference = differs(wanted.checkpoint, law, got, wanted) + if (difference) return difference + } + return differs('trace-length', law, actual.length, expected.length) ?? pass +} + +async function observeBoundary( + observe: RecordFact, + checkpoint: string, + read: () => T, + ready: (value: T) => boolean, +): Promise { + const reached = await boundary(() => ready(read())) + observe(checkpoint, read()) + return reached +} +async function settle( + promise: PromiseLike, +): Promise< + { outcome: 'resolved'; value: T } | { outcome: 'rejected'; error: unknown } +> { + try { + return { outcome: 'resolved', value: await promise } + } catch (error) { + return { outcome: 'rejected', error } + } +} + +type MergeCut = + | 'initial-load' + | 'handler-refetch-other-key' + | 'handler-refetch-same-key' + | 'user-refetch' +const mergeCuts: Array = [ + 'initial-load', + 'handler-refetch-other-key', + 'handler-refetch-same-key', + 'user-refetch', +] +/** The original keyed overlay model judges source keys and later direct keys. + * It uses authored source snapshots; it reads neither cache nor Collection. */ +function mergeCutModel(cut: MergeCut): Array { + const fetched = + cut === 'initial-load' + ? [ + { id: '1', value: 'a' }, + { id: '2', value: 'b' }, + ] + : cut === 'user-refetch' + ? [ + { id: '1', value: 'new1' }, + { id: '2', value: 'new2' }, + ] + : [ + { id: '1', value: 'A' }, + { id: '2', value: 'old2' }, + ] + const written = + cut === 'initial-load' || cut === 'user-refetch' + ? { id: '3', value: 'c' } + : { id: cut === 'handler-refetch-same-key' ? '1' : '2', value: 'B' } + const expected = mergeDirectWritesOverFetch(fetched, [written]) + return [ + { checkpoint: 'held-fetches', value: 1 }, + ...(cut === 'initial-load' + ? [{ checkpoint: 'pending-status', value: 'pending' }] + : []), + ...(cut === 'user-refetch' + ? [{ checkpoint: 'waiter-visible', value: expected }] + : []), + { checkpoint: 'visible', value: expected }, + { checkpoint: 'cache', value: expected }, + ] +} +/** Empty-baseline evidence belongs to one sync run. Each emitted result and + * restart below is an authored action, not a production scan classifier. */ +const expectedEmptyBaselineScans = ( + history: ReadonlyArray<'empty-result' | 'restart'>, +): number => { + let scans = 0, + known = false + for (const action of history) { + if (action === 'restart') known = false + else if (!known) { + scans++ + known = true + } + } + return scans +} +function emptyBaselineModel(): Array { + const history: Array<'empty-result' | 'restart'> = [] + const expected: Array = [] + for (let i = 0; i < 3; i++) { + history.push('empty-result') + expected.push({ + checkpoint: 'baseline:' + i, + value: { rows: [], scans: expectedEmptyBaselineScans(history) }, + }) + } + return [...expected, { checkpoint: 'query-calls', value: 3 }] +} +function coldBaselineModel(): Array { + return [ + { checkpoint: 'hot-storage', value: [] }, + { + checkpoint: 'cold-baseline', + value: { + queryCalls: 1, + rows: [], + stored: [], + scans: expectedEmptyBaselineScans(['restart', 'empty-result']), + }, + }, + ] +} +function supersededModel(): Array { + let model: ExplicitRefetchApplicationModel = { + calls: new Map(), + publicValues: new Map([['query', 'none']]), + } + for (const [callId, name] of [ + [1, 'First'], + [2, 'Second'], + ] as const) { + model = reduceExplicitRefetchApplication(model, { + type: 'start-call', + callId, + resultIds: ['query'], + throwOnError: true, + }) + model = reduceExplicitRefetchApplication(model, { + type: 'accept-result', + callId, + resultId: 'query', + value: name, + }) + } + model = reduceExplicitRefetchApplication(model, { + type: 'retire-application', + callId: 1, + resultId: 'query', + }) + model = reduceExplicitRefetchApplication(model, { + type: 'fulfill-application', + callId: 2, + resultId: 'query', + }) + return [ + { checkpoint: 'initial-scans', value: 1 }, + { checkpoint: 'initial-queries', value: 1 }, + { checkpoint: 'first-work', value: { queries: 2, scans: 2 } }, + { checkpoint: 'successor-applied', value: { queries: 3, name: 'Second' } }, + { + checkpoint: 'successor', + value: observeExplicitRefetchApplication(model, 2), + }, + { + checkpoint: 'retired-error', + value: { outcome: 'rejected', name: 'AbortError' }, + }, + { + checkpoint: 'retired', + value: observeExplicitRefetchApplication(model, 1), + }, + { checkpoint: 'final-rows', value: ['second'] }, + ] +} +function supersededCheck( + _h: unknown, + trace: ReadonlyArray, + expected: ReadonlyArray, +): Verdict { + const ordinary = checkCuts( + trace.slice(0, -1), + expected, + 'accepted result supersession', + ) + if (ordinary.kind === 'violation') return ordinary + const last = trace.at(-1) + if (last?.checkpoint !== 'publications') + return violation( + 'publications', + 'publication trace missing', + 'accepted result supersession', + ) + const publications = last.value as Array> + const obsolete = publications.filter( + (keys) => + keys.length === 1 && (keys[0] === 'initial' || keys[0] === 'first'), + ) + return ( + differs('publications', 'accepted result supersession', obsolete, []) ?? + pass + ) +} + +declareOracle({ + owns: [`src/query.ts#queryCollectionOptions`], + model: [ + mergeCutModel, + emptyBaselineModel, + coldBaselineModel, + supersededModel, + ], + check: [checkCuts, supersededCheck], +}) +oracleProperty({ + id: 'query-db.ownership.older-fetch-merge', + law: { + statement: + 'keeps server rows a direct write did not touch when an older fetch settles: %s', + authority: + 'docs/collections/query-collection.md#direct-writes-and-query-sync and existing ownership retention/result-settlement contracts.', + limits: + 'Original finite history and every retained work/cache/publication checkpoint; controlled persistence rather than native SQLite.', + owner: 'Query DB ownership lifecycle', + }, + histories: mergeCuts, + model: mergeCutModel, + driver: async (cut, observe) => { + try { + type Row = { id: string; value: string } + const id = `older-fetch-merge-${cut}` + let server: Array = + cut === `initial-load` + ? [ + { id: `1`, value: `a` }, + { id: `2`, value: `b` }, + ] + : [ + { id: `1`, value: `old1` }, + { id: `2`, value: `old2` }, + ] + const held: Array>> = [] + let hold = cut === `initial-load` + const queryClient = createQueryClient() + const writtenKey = cut === `handler-refetch-same-key` ? `1` : `2` + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn: async () => { + const snapshot = server.map((row) => ({ ...row })) + if (hold) { + const gate = createDeferred() + held.push(gate) + await gate.promise + } + return snapshot + }, + getKey: (row) => row.id, + startSync: true, + onUpdate: async ({ transaction, collection: handlerCollection }) => { + const modified = transaction.mutations[0].modified + if (modified.value === `A`) { + // Handler A explicitly reads back the accepted server change. + server = server.map((row) => + row.id === `1` ? { id: `1`, value: `A` } : row, + ) + await handlerCollection.utils.refetch() + return + } + server = server.map((row) => + row.id === writtenKey ? { id: writtenKey, value: `B` } : row, + ) + await handlerCollection.utils.writeUpdate({ + id: writtenKey, + value: `B`, + }) + }, + }), + ) + cleanups.push(async () => { + held.forEach((gate) => gate.resolve()) + await collection.cleanup() + queryClient.clear() + }) + const visible = () => + Object.fromEntries( + Array.from(collection.values(), (row) => [row.id, row.value]), + ) + const cached = () => + Object.fromEntries( + (queryClient.getQueryData>([id]) ?? []).map((row) => [ + row.id, + row.value, + ]), + ) + + if (cut === `initial-load`) { + const ready = collection.preload() + if ( + !(await observeBoundary( + observe, + 'held-fetches', + () => held.length, + (count) => count === 1, + )) + ) + return + await collection.utils.writeInsert({ id: `3`, value: `c` }) + observe( + 'pending-status', + queryClient.getQueryCache().find({ queryKey: [id] })?.state.status, + ) + held[0]!.resolve() + await ready + } else if (cut === `user-refetch`) { + await collection.preload() + server = [ + { id: `1`, value: `new1` }, + { id: `2`, value: `new2` }, + ] + + hold = true + const refetch = collection.utils.refetch() + if ( + !(await observeBoundary( + observe, + 'held-fetches', + () => held.length, + (count) => count === 1, + )) + ) + return + hold = false + await collection.utils.writeInsert({ id: `3`, value: `c` }) + held[0]!.resolve() + await refetch + + // The refetch settles only once the merged rows are visible. + observe('waiter-visible', visible()) + } else { + await collection.preload() + hold = true + const first = collection.update(`1`, (draft) => { + draft.value = `A` + }) + if ( + !(await observeBoundary( + observe, + 'held-fetches', + () => held.length, + (count) => count === 1, + )) + ) + return + hold = false + + await collection + .update(writtenKey, (draft) => { + draft.value = `B-optimistic` + }) + .when(`settled`) + held[0]!.resolve() + await first.when('settled') + } + // The original cut is after the source/application settles. Await its work + // queue, then record the public row map without consulting the model. + await boundary( + () => collection._state.pendingSyncedTransactions.length === 0, + ) + observe('visible', visible()) + observe('cache', cached()) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'row authority and retained work'), +}) + +oracleProperty({ + id: 'query-db.ownership.retained-empty-work', + law: { + statement: + 'does not rescan a stable empty persisted baseline on retained reacquisition', + authority: + 'docs/collections/query-collection.md#direct-writes-and-query-sync and existing ownership retention/result-settlement contracts.', + limits: + 'Original finite history and every retained work/cache/publication checkpoint; controlled persistence rather than native SQLite.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: emptyBaselineModel, + driver: async (_history, observe) => { + try { + const id = `empty-persisted-baseline-work` + const queryHash = hashKey([id]) + const scanPersistedRows = vi.fn(() => Promise.resolve([])) + const { collection, queryFn } = createOwnershipFixture({ + id, + results: [[], [], []], + scanPersistedRows, + persistedGcTime: Number.POSITIVE_INFINITY, + setupMetadata: (metadata) => + metadata.collection.set(`queryCollection:gc:${queryHash}`, { + queryHash, + mode: `until-revalidated`, + }), + }) + + await collection._sync.loadSubset({}) + + observe('baseline:0', { + rows: rows(collection), + scans: scanPersistedRows.mock.calls.length, + }) + + for (let result = 0; result < 2; result++) { + collection._sync.unloadSubset({}) + await collection._sync.loadSubset({ refetch: true }) + + observe('baseline:' + (result + 1), { + rows: rows(collection), + scans: scanPersistedRows.mock.calls.length, + }) + } + observe('query-calls', queryFn.mock.calls.length) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'row authority and retained work'), +}) + +oracleProperty({ + id: 'query-db.ownership.cold-owner-work', + law: { + statement: + 'scans a cold durable owner that appeared after an empty sync run', + authority: + 'docs/collections/query-collection.md#direct-writes-and-query-sync and existing ownership retention/result-settlement contracts.', + limits: + 'Original finite history and every retained work/cache/publication checkpoint; controlled persistence rather than native SQLite.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: coldBaselineModel, + driver: async (_history, observe) => { + try { + const id = `cold-owner-after-empty-baseline` + const queryHash = hashKey([id]) + const hotStorage = createOwnershipStorage() + const hot = createPersistedOwnershipFixture(id, hotStorage, []) + await hot.collection._sync.loadSubset({}) + observe('hot-storage', storedItems(hotStorage)) + await hot.collection.cleanup() + + // This seeded durable state represents another writer's committed owner + // while the first Collection had no sync run. The cold Collection cannot + // use the first run's empty-baseline evidence. + const coldSeed = hotStorage.snapshot() + coldSeed.rows.set(shared.id, shared) + coldSeed.rowMetadata.set(shared.id, { + queryCollection: { owners: { [queryHash]: true } }, + }) + coldSeed.collectionMetadata.set(`queryCollection:gc:${queryHash}`, { + queryHash, + mode: `until-revalidated`, + }) + const coldStorage = createOwnershipStorage(coldSeed) + const coldScanRows = vi.spyOn(coldStorage.adapter, `scanRows`) + const cold = createPersistedOwnershipFixture(id, coldStorage, []) + await cold.collection._sync.loadSubset({}) + + // The empty provider result retires the cold durable owner. A design that + // skips every empty-baseline scan leaves the stored row behind. + observe('cold-baseline', { + queryCalls: cold.queryFn.mock.calls.length, + rows: rows(cold.collection), + stored: storedItems(coldStorage), + scans: coldScanRows.mock.calls.length, + }) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'row authority and retained work'), +}) + +oracleProperty({ + id: 'query-db.ownership.superseded-result', + law: { + statement: + 'rejects a superseded accepted result while publishing its successor', + authority: + 'docs/collections/query-collection.md#direct-writes-and-query-sync and existing ownership retention/result-settlement contracts.', + limits: + 'Original finite history and every retained work/cache/publication checkpoint; controlled persistence rather than native SQLite.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: supersededModel, + driver: async (_history, observe) => { + try { + const id = `superseded-retained-scan` + const queryKey = [id] as const + const queryHash = hashKey(queryKey) + const initial = { id: `initial`, category: `retained`, name: `Initial` } + const first = { id: `first`, category: `retained`, name: `First` } + const second = { id: `second`, category: `retained`, name: `Second` } + const initialScan = + createDeferred< + Array<{ key: string | number; value: Item; metadata?: unknown }> + >() + const firstRefetchScan = + createDeferred< + Array<{ key: string | number; value: Item; metadata?: unknown }> + >() + const scanPersistedRows = vi + .fn() + .mockReturnValueOnce(initialScan.promise) + .mockReturnValueOnce(firstRefetchScan.promise) + .mockResolvedValue([]) + const { collection, queryFn } = createOwnershipFixture({ + id, + results: [[initial], [first], [second]], + syncMode: `eager`, + scanPersistedRows, + setupMetadata: (metadata) => { + metadata.collection.set(`queryCollection:gc:${queryHash}`, { + queryHash, + mode: `until-revalidated`, + }) + }, + }) + const publications: Array> = [] + const subscription = collection.subscribeChanges(() => { + publications.push(itemIds(collection.toArray)) + }) + cleanups.push(() => { + subscription.unsubscribe() + return Promise.resolve() + }) + cleanups.push(() => { + initialScan.resolve([]) + firstRefetchScan.resolve([]) + return Promise.resolve() + }) + + if ( + !(await observeBoundary( + observe, + 'initial-scans', + () => scanPersistedRows.mock.calls.length, + (count) => count === 1, + )) + ) + return + observe('initial-queries', queryFn.mock.calls.length) + + const firstRefetch = collection.utils.refetch({ throwOnError: true }) + void firstRefetch.catch(() => {}) + if ( + !(await observeBoundary( + observe, + 'first-work', + () => ({ + queries: queryFn.mock.calls.length, + scans: scanPersistedRows.mock.calls.length, + }), + (work) => work.queries === 2 && work.scans === 2, + )) + ) + return + + const secondRefetch = collection.utils.refetch({ throwOnError: true }) + if ( + !(await observeBoundary( + observe, + 'successor-applied', + () => ({ + queries: queryFn.mock.calls.length, + name: collection.get(second.id)?.name, + }), + (work) => work.queries === 3 && work.name === second.name, + )) + ) + return + + await secondRefetch + observe('successor', { + publicValues: { query: collection.get(second.id)?.name }, + refetch: `resolved`, + resultOrder: [`query`], + }) + + firstRefetchScan.resolve([]) + const retiredOutcome = await settle(firstRefetch) + observe( + 'retired-error', + retiredOutcome.outcome === 'rejected' + ? { + outcome: retiredOutcome.outcome, + name: diagnosticField(retiredOutcome.error, 'name'), + } + : { outcome: retiredOutcome.outcome }, + ) + observe('retired', { + publicValues: { query: collection.get(second.id)?.name }, + refetch: `rejected`, + }) + initialScan.resolve([]) + + await observeBoundary( + observe, + 'final-rows', + () => itemIds(collection.toArray), + (ids) => ids.length === 1 && ids[0] === second.id, + ) + observe( + 'publications', + publications.map((ids) => [...ids]), + ) + } finally { + await runCleanups() + } + }, + check: supersededCheck, +}) diff --git a/packages/query-db-collection/tests/ownership-confirmation.oracle.test.ts b/packages/query-db-collection/tests/ownership-confirmation.oracle.test.ts new file mode 100644 index 0000000000..56681c988e --- /dev/null +++ b/packages/query-db-collection/tests/ownership-confirmation.oracle.test.ts @@ -0,0 +1,1078 @@ +import fc from 'fast-check' +import { createCollection } from '@tanstack/db' +import { afterEach, vi } from 'vitest' +import { + differs, + oracleProperty, + pass, + violation, +} from '../../db/tests/oracle-property.js' +import { declareOracle } from '../../db/tests/oracle-declaration.js' + +import { withOracleCleanup } from '../../db/tests/utils.js' +import { atMutationCheckpoint } from '../../db/tests/mutation-confirmation-support.js' +import { + checkMutationConfirmation, + expectedMutationConfirmation, + mutationConfirmationHistories, + mutationConfirmationInputs, + mutationConfirmationRows, +} from '../../db/tests/mutation-confirmation-oracle.js' +import { createDeferred } from '../../db/src/deferred.js' +import { queryCollectionOptions } from '../src/query.js' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { + createOwnershipStorage, + createQueryClient, + runCleanups, +} from './ownership-lifecycle-support.js' +import { + checkPostHandlerRefetch, + createRefetchCallSettlementModel, + reduceRefetchCallSettlement, +} from './ownership-model-oracle' +import type { Item } from './ownership-lifecycle-support.js' +import type { + QueryCollectionConfig, + QueryCollectionUtils, +} from '../src/query.js' +import type { + MutationConfirmationCut, + MutationConfirmationHistory, + MutationConfirmationObservation, + MutationConfirmationRow, +} from '../../db/tests/mutation-confirmation-oracle.js' +import type { + ConstructedHistory, + Observation, + Verdict, +} from '../../db/tests/oracle-property.js' +import type { + RefetchApplicationOutcome, + observeRefetchCallSettlement, +} from './ownership-model-oracle' + +afterEach(runCleanups) +type Emit = (checkpoint: string, value: unknown) => void +// Error references are compared at the production boundary. The framework +// snapshots each observation, so transporting the Error itself would lose the +// exact identity relationship that the original law checks. +function emitMutationObservation( + checkpoint: string, + cut: MutationConfirmationCut, + observation: MutationConfirmationObservation, + failure: Error, + emit: Emit, +) { + emit(checkpoint, { + cut, + observation: { ...observation, error: undefined }, + errorRelation: { + sameFailure: observation.error === failure, + absent: observation.error === undefined, + }, + }) +} +async function delayTurns(turns: number) { + for (let i = 0; i < turns; i++) await Promise.resolve() +} +async function recordBoundary( + checkpoint: string, + read: () => T, + arrived: (value: T) => boolean, + emit: Emit, +): Promise { + for (let turn = 0; turn < 200; turn++) { + if (arrived(read())) { + emit(checkpoint, read()) + return true + } + await new Promise((resolve) => setTimeout(resolve, 5)) + } + emit(checkpoint, read()) + return false +} + +/** Confirmation and handler exit are independent boundaries. The existing + * shared mutation model predicts rows and work at each held cut. A second call + * ledger predicts post-handler receipt settlement without inferring it from + * visible rows. Generated payload text changes real source and confirmed rows; + * generated microtask delays perturb the held handler window. */ +type MutationHistory = MutationConfirmationHistory & { + suffix: string + phaseDelay: number +} +type MutationPlan = { + history: MutationHistory + failure: Error + inputs: ReturnType +} +type PostHistory = { + operation: 'insert' | 'update' | 'delete' + reject: boolean + suffix: string + phaseDelay: number +} +type PostPlan = { history: PostHistory; failure: Error } +function confirmationInputs(h: MutationHistory) { + const change = (row: MutationConfirmationRow) => ({ + ...row, + name: row.name + h.suffix, + stable: row.stable + h.suffix, + }) + return { + peer: change(mutationConfirmationInputs.peer), + before: change(mutationConfirmationInputs.before), + optimistic: change(mutationConfirmationInputs.optimistic), + confirmed: change(mutationConfirmationInputs.confirmed), + } +} +const mutationFixed: Array = mutationConfirmationHistories( + 'query', +).map((h) => ({ ...h, suffix: '', phaseDelay: 0 })) +const mutationState = (h: MutationHistory) => + `${h.operation}/${h.outcome}/${h.confirmation}/${h.returned}/peer:${h.peer}` +const mutationStates = mutationFixed.map(mutationState) +const mutationObligations = new WeakMap< + MutationHistory, + ReadonlyArray +>() +const mutationArbitrary = fc + .tuple( + fc.constantFrom(...mutationFixed), + fc.string({ maxLength: 8 }), + fc.integer({ min: 0, max: 3 }), + ) + .map( + ([base, suffix, phaseDelay]) => ({ ...base, suffix, phaseDelay }), + (raw) => { + const h = raw as MutationHistory + const base = mutationFixed.find( + (v) => mutationState(v) === mutationState(h), + ) + if (!base) throw Error('Illegal confirmation history') + return [base, h.suffix, h.phaseDelay] as const + }, + ) +const mutationConstructed: Array> = + mutationFixed.map((witness) => ({ + name: mutationState(witness), + enters: [mutationState(witness)], + build(generated) { + const h = { + ...witness, + suffix: generated.suffix, + phaseDelay: generated.phaseDelay, + } + mutationObligations.set(h, [mutationState(witness)]) + return h + }, + })) +function mutationCuts( + h: MutationHistory, +): Array<{ checkpoint: string; cut: MutationConfirmationCut }> { + const cuts: Array<{ checkpoint: string; cut: MutationConfirmationCut }> = [ + { checkpoint: 'entered', cut: 'entered' }, + ] + if (h.outcome !== 'reject-before-write') { + cuts.push({ checkpoint: 'server-written', cut: 'server-written' }) + if (h.confirmation === 'refetch') + cuts.push({ + checkpoint: 'confirmation-requested', + cut: 'confirmation-requested', + }) + if (h.confirmation !== 'none') + cuts.push({ + checkpoint: 'confirmation-accepted', + cut: 'confirmation-accepted', + }) + } + cuts.push( + { checkpoint: 'settlement-waiter', cut: 'settled' }, + { checkpoint: 'settled', cut: 'settled' }, + ) + if (h.confirmation === 'none' && h.outcome !== 'reject-before-write') + cuts.push( + { checkpoint: 'late-confirmed', cut: 'late-confirmed' }, + { checkpoint: 'frozen-waiter', cut: 'settled' }, + ) + return cuts +} +function mutationModel(plan: MutationPlan) { + return { + cuts: mutationCuts(plan.history), + requiredStates: mutationObligations.get(plan.history) ?? [ + mutationState(plan.history), + ], + } +} +function checkMutation( + plan: MutationPlan, + trace: ReadonlyArray, + expected: ReturnType, +): Verdict { + const facts = trace.filter((o) => o.checkpoint !== 'reach') + for (let index = 0; index < expected.cuts.length; index++) { + const cut = expected.cuts[index]!, + fact = facts[index] + if (fact?.checkpoint !== cut.checkpoint) + return violation( + cut.checkpoint, + 'the mutation observation cut did not arrive', + 'handler confirmation', + ) + const value = fact.value as { + cut: MutationConfirmationCut + observation: MutationConfirmationObservation + errorRelation: { sameFailure: boolean; absent: boolean } + } + if (value.cut !== cut.cut) + return violation( + cut.checkpoint, + 'the observation used a different model cut', + 'handler confirmation', + ) + const expectedError = expectedMutationConfirmation( + plan.history, + cut.cut, + plan.failure, + plan.inputs, + ).error + const identity = differs( + cut.checkpoint, + 'handler confirmation', + value.errorRelation, + { + sameFailure: expectedError === plan.failure, + absent: expectedError === undefined, + }, + ) + if (identity) return identity + try { + checkMutationConfirmation( + plan.history, + cut.cut, + { + ...value.observation, + error: value.errorRelation.sameFailure ? plan.failure : undefined, + }, + plan.failure, + plan.inputs, + ) + } catch (error) { + return violation( + cut.checkpoint, + error instanceof Error ? error.message : String(error), + 'handler confirmation', + ) + } + } + const length = differs( + 'mutation-cut-count', + 'handler confirmation', + facts.length, + expected.cuts.length, + ) + if (length) return length + return checkReach(trace, expected.requiredStates) +} +function checkReach( + trace: ReadonlyArray, + required: ReadonlyArray, +): Verdict { + const reached = new Set( + trace.filter((o) => o.checkpoint === 'reach').map((o) => o.value), + ) + const missing = required.filter((state) => !reached.has(state)) + return missing.length + ? violation( + 'constructed states', + 'missing ' + missing.join(', '), + 'constructed reach', + ) + : pass +} + +const postFixed: Array = ( + ['insert', 'update', 'delete'] as const +).flatMap((operation) => + [false, true].map((reject) => ({ + operation, + reject, + suffix: '', + phaseDelay: 0, + })), +) +const postState = ( + h: PostHistory, + cut: 'application-held' | 'applied-waiter', +) => `${h.operation}/${h.reject}/${cut}` +const postStates = postFixed.flatMap((h) => [ + postState(h, 'application-held'), + postState(h, 'applied-waiter'), +]) +const postObligations = new WeakMap>() +const postArbitrary = fc + .tuple( + fc.constantFrom(...postFixed), + fc.string({ maxLength: 8 }), + fc.integer({ min: 0, max: 3 }), + ) + .map( + ([base, suffix, phaseDelay]) => ({ ...base, suffix, phaseDelay }), + (raw) => { + const h = raw as PostHistory + const base = postFixed.find( + (v) => v.operation === h.operation && v.reject === h.reject, + ) + if (!base) throw Error('Illegal post-handler history') + return [base, h.suffix, h.phaseDelay] as const + }, + ) +const postConstructed: Array> = postStates.map( + (state) => { + const witness = postFixed.find( + (h) => + postState(h, 'application-held') === state || + postState(h, 'applied-waiter') === state, + )! + return { + name: state, + enters: [state], + build(generated) { + const h = { + ...witness, + suffix: generated.suffix, + phaseDelay: generated.phaseDelay, + } + postObligations.set(h, [state]) + return h + }, + } + }, +) +const encodeRows = (values: ReadonlyArray) => + JSON.stringify( + [...values] + .map(({ id, category, name }) => ({ id, category, name })) + .sort((a, b) => a.id.localeCompare(b.id)), + ) +function postRows(h: PostHistory) { + const initial: Array = [ + { id: 'peer', category: 'peer', name: 'Peer' }, + ...(h.operation === 'insert' + ? [] + : [{ id: 'a', category: 'edited', name: 'Before' }]), + ] + const confirmed: Array = [ + { id: 'peer', category: 'peer', name: 'New peer' }, + { id: 'a', category: 'edited', name: 'New source row' }, + ] + const change = (rows: Array) => + rows.map((row) => ({ ...row, name: row.name + h.suffix })) + return { + initial: encodeRows(change(initial)), + confirmed: encodeRows(change(confirmed)), + } +} +function postModel(plan: PostPlan) { + const h = plan.history + let model = createRefetchCallSettlementModel(['query'], true) + model = reduceRefetchCallSettlement(model, { + type: 'accept-application', + queryId: 'query', + applicationId: 'later-result', + }) + model = reduceRefetchCallSettlement(model, { + type: 'settle-fetch', + queryId: 'query', + outcome: 'fulfilled', + }) + const held = model + model = reduceRefetchCallSettlement(model, { + type: 'settle-application', + applicationId: 'later-result', + outcome: 'fulfilled', + }) + return { + ...postRows(h), + held, + applied: model, + requiredStates: postObligations.get(h) ?? [ + postState(h, 'application-held'), + postState(h, 'applied-waiter'), + ], + } +} +function checkPost( + plan: PostPlan, + trace: ReadonlyArray, + expected: ReturnType, +): Verdict { + const find = (checkpoint: string) => + trace.find((o) => o.checkpoint === checkpoint) + const handlerError = find('handler-error') + if (!handlerError) + return violation( + 'handler-error', + 'the handler outcome observation is missing', + 'post-handler application', + ) + const identity = differs( + 'handler-error', + 'post-handler application', + handlerError.value, + { sameFailure: plan.history.reject, absent: !plan.history.reject }, + ) + if (identity) return identity + const first = + differs( + 'handler-state', + 'post-handler application', + find('handler-state')?.value, + plan.history.reject ? 'failed' : 'completed', + ) ?? + differs( + 'handler-rows', + 'post-handler application', + find('handler-rows')?.value, + expected.initial, + ) ?? + differs( + 'fetch-complete', + 'post-handler application', + find('fetch-complete')?.value, + { fetches: 2, fetching: 0 }, + ) + if (first) return first + for (const [checkpoint, model] of [ + ['application-held', expected.held], + ['applied-waiter', expected.applied], + ] as const) { + const fact = find(checkpoint) + if (!fact) + return violation( + checkpoint, + 'the post-handler receipt cut did not arrive', + 'post-handler application', + ) + try { + checkPostHandlerRefetch( + model, + fact.value as { + refetch: ReturnType + rows: string + waiterRows: string | undefined + }, + expected.initial, + expected.confirmed, + ) + } catch (error) { + return violation( + checkpoint, + error instanceof Error ? error.message : String(error), + 'post-handler application', + ) + } + } + return checkReach(trace, expected.requiredStates) +} + +async function observeMutation(plan: MutationPlan, emit: Emit): Promise { + const history = plan.history + try { + const { operation, confirmation, outcome, returned } = history + + const queryClient = createQueryClient() + const enterRelease = createDeferred() + const confirmationRelease = createDeferred() + const settleRelease = createDeferred() + const entered = createDeferred() + const serverWritten = createDeferred() + const confirmationAccepted = createDeferred() + const refetchRequested = createDeferred() + const refetchResponse = createDeferred() + const failure = plan.failure + const initial = mutationConfirmationRows( + operation, + `initial`, + history.peer, + plan.inputs, + ) + const confirmed = mutationConfirmationRows( + operation, + `confirmed`, + history.peer, + plan.inputs, + ) + let server = structuredClone(initial) + let handlerWrites = 0 + let confirmationCalls = 0 + let confirmationStatus: MutationConfirmationObservation[`confirmationStatus`] = `not-requested` + let settlement: MutationConfirmationObservation[`settlement`] = `pending` + let error: unknown + let holdRefetch = confirmation === `refetch` + const queryFn = vi.fn(async () => { + if (confirmationCalls > 0 && holdRefetch) { + refetchRequested.resolve() + await refetchResponse.promise + } + return structuredClone(server) + }) + let releaseCollection: (() => Promise) | undefined + let verdict: Promise | undefined + await withOracleCleanup(async () => { + const handler = async ({ + collection: target, + }: Pick< + Parameters< + NonNullable< + QueryCollectionConfig[`onInsert`] + > + >[0], + `collection` + >) => { + entered.resolve() + await enterRelease.promise + if (outcome === `reject-before-write`) throw failure + server = structuredClone(confirmed) + handlerWrites++ + serverWritten.resolve() + await confirmationRelease.promise + if (confirmation !== `none`) { + confirmationCalls++ + confirmationStatus = `pending` + } + if (confirmation === `refetch`) await target.utils.refetch() + if (confirmation === `write`) { + if (operation === `delete`) await target.utils.writeDelete(1) + else + await target.utils.writeUpsert({ + ...plan.inputs.confirmed, + }) + } + if (confirmation !== `none`) confirmationStatus = `accepted` + confirmationAccepted.resolve() + await settleRelease.promise + if (outcome === `reject-after-write`) throw failure + return returned === `legacy-disable` + ? { refetch: false } + : returned === `legacy-enable` + ? { refetch: true } + : undefined + } + const collection = createCollection( + queryCollectionOptions({ + queryKey: [`explicit-handler-refresh`, history], + queryClient, + queryFn, + getKey: (row) => row.id, + // JS callers may return coordination objects that TS now rejects. + onInsert: handler as unknown as NonNullable< + QueryCollectionConfig[`onInsert`] + >, + onUpdate: handler as unknown as NonNullable< + QueryCollectionConfig[`onUpdate`] + >, + onDelete: handler as unknown as NonNullable< + QueryCollectionConfig[`onDelete`] + >, + }), + ) + releaseCollection = () => collection.cleanup() + await atMutationCheckpoint(collection.preload(), `initial Query preload`) + const transaction = + operation === `insert` + ? collection.insert({ ...plan.inputs.optimistic }) + : operation === `delete` + ? collection.delete(1) + : collection.update(1, (draft) => { + draft.name = plan.inputs.optimistic.name + }) + const observe = (): MutationConfirmationObservation => ({ + rows: collection.toArray + .map(({ id, name, stable }) => ({ id, name, stable })) + .sort((a, b) => a.id - b.id), + handlerWrites, + confirmationCalls, + fetches: queryFn.mock.calls.length, + providerRequests: 0, + confirmationStatus, + state: transaction.state, + settlement, + error, + }) + verdict = transaction.when(`settled`).then( + () => { + settlement = `fulfilled` + return observe() + }, + (reason: unknown) => { + settlement = `rejected` + error = reason + return observe() + }, + ) + await atMutationCheckpoint(entered.promise, `Query handler entry`) + emitMutationObservation('entered', `entered`, observe(), failure, emit) + + await delayTurns(history.phaseDelay) + enterRelease.resolve() + if (outcome !== `reject-before-write`) { + await atMutationCheckpoint(serverWritten.promise, `Query server write`) + emitMutationObservation( + 'server-written', + `server-written`, + observe(), + failure, + emit, + ) + + confirmationRelease.resolve() + if (confirmation === `refetch`) { + await atMutationCheckpoint( + refetchRequested.promise, + `held Query refetch response`, + ) + emitMutationObservation( + 'confirmation-requested', + `confirmation-requested`, + observe(), + failure, + emit, + ) + + holdRefetch = false + refetchResponse.resolve() + } + await atMutationCheckpoint( + confirmationAccepted.promise, + `Query confirmation acceptance`, + ) + if (confirmation !== `none`) { + emitMutationObservation( + 'confirmation-accepted', + `confirmation-accepted`, + observe(), + failure, + emit, + ) + } + settleRelease.resolve() + } + const atWaiter = await atMutationCheckpoint( + verdict, + `Query handler settlement`, + ) + emitMutationObservation( + 'settlement-waiter', + `settled`, + atWaiter, + failure, + emit, + ) + + emitMutationObservation('settled', `settled`, observe(), failure, emit) + + if (confirmation === `none` && outcome !== `reject-before-write`) { + // Backend success alone supplied no source confirmation. A later + // explicit reader may refetch; the frozen settlement cut stays old. + await atMutationCheckpoint( + collection.utils.refetch(), + `later explicit Query refetch`, + ) + emitMutationObservation( + 'late-confirmed', + `late-confirmed`, + observe(), + failure, + emit, + ) + + emitMutationObservation( + 'frozen-waiter', + `settled`, + atWaiter, + failure, + emit, + ) + } + + emit('reach', mutationState(history)) + }, [ + () => { + enterRelease.resolve() + confirmationRelease.resolve() + settleRelease.resolve() + refetchResponse.resolve() + }, + () => + verdict && atMutationCheckpoint(verdict, `Query cleanup settlement`), + () => releaseCollection?.(), + () => queryClient.clear(), + ]) + } finally { + await runCleanups() + } +} +async function observePostHandler(plan: PostPlan, emit: Emit): Promise { + const history = plan.history + const { operation, reject } = history + try { + const initial: Array = [ + { id: `peer`, category: `peer`, name: `Peer` }, + ...(operation === `insert` + ? [] + : [{ id: `a`, category: `edited`, name: `Before` }]), + ] + const authoritative: Array = [ + { id: `peer`, category: `peer`, name: `New peer` }, + { id: `a`, category: `edited`, name: `New source row` }, + ] + const encode = (values: ReadonlyArray) => + JSON.stringify( + [...values] + .map(({ id, category, name }) => ({ id, category, name })) + .sort((left, right) => left.id.localeCompare(right.id)), + ) + initial.forEach((row) => { + row.name += history.suffix + }) + authoritative.forEach((row) => { + row.name += history.suffix + }) + let server = structuredClone(initial) + const handlerFailure = plan.failure + const handler = async () => { + await Promise.resolve() + if (reject) throw handlerFailure + } + const storage = createOwnershipStorage() + const entered = createDeferred() + const release = createDeferred() + let hold = false + const apply = storage.adapter.applyCommittedTx.bind(storage.adapter) + storage.adapter.applyCommittedTx = async (...args) => { + if (hold && args[1].mutations.length > 0) { + hold = false + entered.resolve() + await release.promise + } + return apply(...args) + } + const queryClient = createQueryClient() + const queryFn = vi.fn(() => Promise.resolve(structuredClone(server))) + let releaseCollection: (() => Promise) | undefined + let external: Promise | undefined + await withOracleCleanup(async () => { + const collection = createCollection( + persistedCollectionOptions< + Item, + string | number, + never, + QueryCollectionUtils + >({ + ...queryCollectionOptions({ + id: `post-handler-${operation}-${reject}`, + queryKey: [`post-handler`, operation, reject], + queryClient, + queryFn, + getKey: (row) => row.id, + onInsert: handler, + onUpdate: handler, + onDelete: handler, + }), + persistence: { adapter: storage.adapter }, + }), + ) + releaseCollection = () => collection.cleanup() + await atMutationCheckpoint( + collection.preload(), + `post-handler initial preload`, + ) + const transaction = + operation === `insert` + ? collection.insert({ + id: `a`, + category: `edited`, + name: `Optimistic`, + }) + : operation === `delete` + ? collection.delete(`a`) + : collection.update(`a`, (draft) => { + draft.name = `Optimistic` + }) + const firstError = await transaction.when(`settled`).then( + () => undefined, + (error: unknown) => error, + ) + emit('handler-error', { + sameFailure: firstError === handlerFailure, + absent: firstError === undefined, + }) + emit('handler-state', transaction.state) + emit('handler-rows', encode(collection.toArray)) + await delayTurns(history.phaseDelay) + hold = true + server = structuredClone(authoritative) + let outcome: RefetchApplicationOutcome = `pending` + let resultOrder: Array | undefined + let waiterRows: string | undefined + const observe = () => ({ + rows: encode(collection.toArray), + waiterRows, + refetch: { + refetch: outcome, + ...(outcome === `resolved` ? { resultOrder } : {}), + }, + }) + + external = collection.utils.refetch({ throwOnError: true }).then( + (results) => { + outcome = `resolved` + resultOrder = results.map(() => `query`) + waiterRows = encode(collection.toArray) + }, + (error: unknown) => { + outcome = `rejected` + throw error + }, + ) + void external.catch(() => {}) + await atMutationCheckpoint( + entered.promise, + `post-handler durable application held`, + ) + if ( + !(await recordBoundary( + 'fetch-complete', + () => ({ + fetches: queryFn.mock.calls.length, + fetching: queryClient.isFetching(), + }), + (v) => v.fetches === 2 && v.fetching === 0, + emit, + )) + ) + return + // Query's flags clear before the utility's queued Promise chain + // finishes. Keep the receipt held through a task boundary so every + // already-queued microtask can process that completed fetch. The + // leaked-count transform is the calibration for this timing premise. + await new Promise((resolve) => setTimeout(resolve, 0)) + + emit('application-held', observe()) + emit('reach', postState(history, 'application-held')) + release.resolve() + await atMutationCheckpoint( + external, + `post-handler external refetch settlement`, + ) + + emit('applied-waiter', observe()) + emit('reach', postState(history, 'applied-waiter')) + }, [ + () => release.resolve(), + () => + external && + atMutationCheckpoint(external, `post-handler cleanup refetch`), + () => releaseCollection?.(), + () => queryClient.clear(), + ]) + } finally { + await runCleanups() + } +} + +/** Every original history remains a fixed replay. Builders preserve genuinely + * generated row text and held-window timing while entering each old tuple/cut. + * The independent check enforces the construction ledger on all fresh outputs, + * including generated and shrunk constructions that the pinned API omits from + * its own identity association. Missing-state controls calibrate every builder. */ +oracleProperty({ + id: 'query-db.ownership.mutation-confirmation', + law: { + statement: + 'Handler settlement starts no implicit request; accepted confirmation decides source rows independently of handler fulfillment or rejection.', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior; optimistic-history acceptance and settlement law; first-RC explicit confirmation decision.', + limits: + 'One handler, edited row and optional untouched peer; legal no/refetch/write confirmation and discarded legacy returns. No provider framing or concurrent handlers.', + owner: 'Query DB ownership lifecycle', + }, + histories: { + arbitrary: mutationArbitrary, + runs: 40, + fixedSeeds: [2367], + states: mutationStates, + constructed: mutationConstructed, + fixed: mutationFixed.map((history) => ({ + name: + 'refreshes only when the handler requests it: ' + + JSON.stringify(history), + history, + })), + }, + resolve: (history) => ({ + history, + failure: new Error('handler ' + history.outcome), + inputs: confirmationInputs(history), + }), + model: mutationModel, + driver: observeMutation, + check: checkMutation, + calibrations: [ + { + name: 'rejects a different handler error object', + law: 'handler confirmation', + corrupt: (trace: ReadonlyArray, plan: MutationPlan) => + plan.history.outcome === 'fulfilled' + ? undefined + : trace.map((o) => + o.checkpoint === 'settlement-waiter' + ? { + ...o, + value: { + ...(o.value as Record), + errorRelation: { sameFailure: false, absent: false }, + }, + } + : o, + ), + }, + ...mutationStates.map((state) => ({ + name: 'omits constructed ' + state, + law: 'constructed reach', + corrupt: (trace: ReadonlyArray, plan: MutationPlan) => + mutationObligations.get(plan.history)?.includes(state) + ? trace.filter((o) => o.checkpoint !== 'reach' || o.value !== state) + : undefined, + })), + { + name: 'omits reach from a fresh generated construction', + law: 'constructed reach', + driver: async (plan, emit) => { + const builder = mutationConstructed.find((c) => + c.enters.includes(mutationState(plan.history)), + )! + const history = builder.build({ + ...plan.history, + phaseDelay: (plan.history.phaseDelay + 1) % 4, + }) + await observeMutation( + { + history, + failure: plan.failure, + inputs: confirmationInputs(history), + }, + (cut, value) => { + if (cut !== 'reach') emit(cut, value) + }, + ) + }, + }, + ], +}) +oracleProperty({ + id: 'query-db.ownership.post-handler', + law: { + statement: + 'Handler exit restores the normal public-refetch application barrier after either fulfillment or rejection; the later waiter observes the complete confirmed snapshot.', + authority: + 'docs/collections/query-collection.md#explicit-refetch-inside-a-handler and core applied-receipt law.', + limits: + 'One CRUD handler and one later local persisted refetch, with durable application held after the Query fetch; no native SQLite fault or concurrent handler.', + owner: 'Query DB ownership lifecycle', + }, + histories: { + arbitrary: postArbitrary, + runs: 24, + fixedSeeds: [2367], + states: postStates, + constructed: postConstructed, + fixed: postFixed.map((history) => ({ + name: `waits for later application after handler ${history.operation} rejection:${history.reject}`, + history, + })), + }, + resolve: (history) => ({ history, failure: new Error('handler rejected') }), + model: postModel, + driver: observePostHandler, + check: checkPost, + calibrations: [ + { + name: 'rejects a different post-handler error object', + law: 'post-handler application', + corrupt: (trace: ReadonlyArray, plan: PostPlan) => + !plan.history.reject + ? undefined + : trace.map((o) => + o.checkpoint === 'handler-error' + ? { ...o, value: { sameFailure: false, absent: false } } + : o, + ), + }, + ...postStates.map((state) => ({ + name: 'omits constructed ' + state, + law: 'constructed reach', + corrupt: (trace: ReadonlyArray, plan: PostPlan) => + postObligations.get(plan.history)?.includes(state) + ? trace.filter((o) => o.checkpoint !== 'reach' || o.value !== state) + : undefined, + })), + { + name: 'omits reach from a fresh generated construction', + law: 'constructed reach', + driver: async (plan, emit) => { + const builder = postConstructed.find((c) => + c.enters.includes(postState(plan.history, 'application-held')), + )! + const history = builder.build({ + ...plan.history, + phaseDelay: (plan.history.phaseDelay + 1) % 4, + }) + await observePostHandler( + { history, failure: plan.failure }, + (cut, value) => { + if (cut !== 'reach') emit(cut, value) + }, + ) + }, + }, + ...(['early-receipt', 'partial-rows', 'stale-waiter'] as const).map( + (fault) => ({ + name: 'rejects post-handler wrong observation: ' + fault, + law: 'post-handler application', + corrupt: (trace: ReadonlyArray, plan: PostPlan) => + trace.map((o) => { + const target = + fault === 'stale-waiter' ? 'applied-waiter' : 'application-held' + if (o.checkpoint !== target) return o + const raw = o.value as { + refetch: unknown + rows: string + waiterRows: string | undefined + } + return { + ...o, + value: { + ...raw, + ...(fault === 'early-receipt' + ? { refetch: { refetch: 'resolved', resultOrder: ['query'] } } + : fault === 'partial-rows' + ? { rows: 'partial' } + : { waiterRows: postRows(plan.history).initial }), + }, + } + }), + }), + ), + ], +}) + +declareOracle({ + owns: ['src/query.ts#queryCollectionOptions'], + model: [mutationModel, postModel], + check: [checkMutation, checkPost], + grammar: [mutationArbitrary, postArbitrary], +}) diff --git a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts index 52dc5e4866..363bcf1221 100644 --- a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts +++ b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts @@ -1,990 +1,97 @@ -import { hashKey, isCancelledError } from '@tanstack/query-core' +/** + * # Which observer lease owns a Query row? + * + * A row stays public while any active committed observer lease needs it. + * Reacquiring a demand starts a new lease that a delayed old release cannot + * retire. Publication and hydration must not copy released leases back. + * Direct delete ends the fetched lifetime; a new direct insert has no inferred + * subset owner. Physical fetch starts and accepted direct writes order values + * per key; releasing a newer owner cannot revert a continuously owned value. + * Authority: Query guide, Direct Writes and Query Sync; Query cache staleness + * and refetch rules; the original ownership oracle's documented lease laws. + * + * Five independent models retain only authored claims, causal positions or + * successful lease results. The complete finite grammars preserve all 101 + * prior histories without reducing a generated-run budget. Real QueryClient, + * Collection, metadata and dehydrate/hydrate paths record the original cuts. + * Native SQLite and arbitrary transport retries are separate receiving paths. + */ +import { hashKey } from '@tanstack/query-core' import { DbClient, IR, collectionOptions, createCollection, - createOptimisticAction, createTransaction, eq, getLoadSubsetDemandKey, } from '@tanstack/db' -import { afterAll, afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, vi } from 'vitest' import { declareOracle } from '../../db/tests/oracle-declaration.js' -import { oracleRoute } from '../../db/tests/oracle-config.js' -import { createOracleReach } from '../../db/tests/oracle-reach' -import { withOracleCleanup } from '../../db/tests/utils' -import { atMutationCheckpoint } from '../../db/tests/mutation-confirmation-support' import { - checkMutationConfirmation, - mutationConfirmationHistories, - mutationConfirmationInputs, - mutationConfirmationRows, -} from '../../db/tests/mutation-confirmation-oracle' + differs, + oracleProperty, + pass, + violation, +} from '../../db/tests/oracle-property.js' + import { createDeferred } from '../../db/src/deferred.js' import { compileSingleRowExpression, toBooleanPredicate, } from '../../db/src/query/compiler/evaluators.js' - -import { InvalidQueryResultError } from '../src/errors.js' import { queryCollectionOptions } from '../src/query.js' -import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' -import { mergeDirectWritesOverFetch } from './direct-write-precedence-oracle' import { categorySubset, cleanups, createOwnershipFixture, - createOwnershipStorage, - createPersistedOwnershipFixture, createQueryClient, - detailOnly, - itemIds, - listOnly, persistedOwners, - reportedError, rows, runCleanups, shared, - storedItems, -} from './ownership-lifecycle-support.js' -import type { - MutationConfirmationObservation, - MutationConfirmationRow, -} from '../../db/tests/mutation-confirmation-oracle' -import type { LoadSubsetOptions, Transaction } from '@tanstack/db' -import type { QueryClient, QueryFunctionContext } from '@tanstack/query-core' - -import type { - QueryCollectionConfig, - QueryCollectionUtils, -} from '../src/query.js' -import type { - Item, - MetadataRecorder, - OwnershipFixture, } from './ownership-lifecycle-support.js' - -/** - * # Who owns a Query-backed row, and when may it disappear? - * - * Query cache entries acquire rows for one or more exact subset demands. A row - * remains public while any committed owner needs it. Provisional ownership - * begins during result application, becomes durable only after publication and - * persistence, and retires when superseded, canceled, cleaned up, or released. - * Explicit mutation refetches add a second authority path but do not bypass - * those rules. Completing a mutation handler starts no Query request and does - * not change synced rows. The first-RC migration requires handlers to refetch - * or write confirmed data explicitly to retain their optimistic changes. - * An accepted direct write changes its Collection without starting a Query - * request. In eager mode, compatible Collections sharing a physical Query - * project representable cache patches made by either writer. In on-demand - * mode, a direct write leaves a Query owned by another Collection untouched; - * a later peer cannot publish another writer's patched cache as a server - * result. A later physical fetch may replace the direct row, while a disabled - * on-demand peer waits for explicit refetch. These claims cover one - * QueryClient; hydration into another client loses local-patch provenance. - * - * The reference view is an ownership graph: query scope and demand nodes point - * to row keys. Sync-run lifetime fences, direct-write key positions, and - * defer-result barriers govern competing results. Publication and persistence - * are separate commit boundaries. Tests use real QueryClient - * observers, cache events, collection metadata, persisted scans, and live - * queries. They compare source rows, derived rows, cache rows, metadata writes, - * exact request lifetimes, and bounded refetch work at each boundary. - * - * A public refetch has a related result-settlement law. Every successful Query - * result reaches exactly one explicit terminal outcome: its rows apply, its - * application rejects, or its work is cancelled or retired. A deferred result - * is not terminal. Its caller waits for the replacement result to apply. - * - * The result-settlement model below is intentionally smaller than production. - * `applicable` combines adapter validation and the ability to materialize the - * result; it does not classify arrays, select wrappers, Query states, or sync - * transactions. The action grammar covers multiple public refetch operations, - * immediate results, valid empty results, invalid shapes, eager recovery, - * overlapping defer generations, replacement results, cleanup retirement, and - * same-result subscriber reentry with a controlled nested applied receipt. - * The production driver crosses eager and on-demand ownership. It observes - * public promise settlement and public rows at the settlement checkpoint. - * The model does not track Query fetch counters or observer scheduling. - * - * The file is large because it crosses the real Query cache boundary, not - * because it duplicates Query internals. Each history names one ownership edge - * or generation race; shared fixtures provide the graph and observations. - * Managed SQLite rotation retires every old Query result, acquisition, and - * retention scan. Active demands reacquire from a post-rotation fetch; a late - * refetch cancels instead of returning unapplied old data. The real SQLite - * histories below compare public rows and caller settlement at both sides of - * the rotation and at the later stale-work release cut. - * - * Known omissions: this result-settlement refinement does not model accepted- - * result generations or the diff-free application signal requested by #1828. - * Transport failure and cache replacement remain owned by the existing Query - * lifecycle histories in this file. - * Direct write receipts have a separate boundary: a Query Collection write - * settles when its sync commit applies. The controlled persisted-owner history - * below holds that commit after the initial fetch, then checks the returned - * promise and stored row before and after release. It does not model native - * SQLite failure or overlapping on-demand explicit refetches. - * Direct writes belong to the current sync run. Cleanup start invalidates that - * run, so later writes must fail without changing storage. Restart admits - * writes through a new run. The fixed history below checks each boundary. - */ - -type ResultSettlementOperationState = - | { phase: `waiting-for-result` } - | { phase: `waiting-for-refresh`; barrier: string } - | { - phase: `terminal` - outcome: `applied` | `rejected` | `cancelled` - rowCount?: 0 | 1 - } - -type ResultSettlementModelState = ReadonlyMap< - string, - ResultSettlementOperationState -> - -/** - * `query-succeeded` and `refresh-succeeded` compress the result lifecycle. - * With an applicable result and no `deferOn`, each action combines Query fetch - * fulfillment, completed Collection application and its applied settlement, - * and the terminal public-refetch checkpoint. - * A `deferOn` action instead records the skipped result and its replacement wait; - * a nondeferred `invalid-shape` result records application rejection. The model - * omits intermediate application states, which the separate obligation models - * below retain. - */ -type ResultSettlementModelAction = - | { type: `start-refetch`; operation: string } - | { - type: `query-succeeded` - operation: string - result: `applicable` | `invalid-shape` - rowCount?: 0 | 1 - deferOn?: string - } - | { - type: `refresh-succeeded` - barrier: string - result: `applicable` | `invalid-shape` - rowCount?: 0 | 1 - deferOn?: string - } - | { type: `retire` } - -type PublicRefetchObservation = - | { settled: false } - | { settled: true; outcome: `fulfilled` | `rejected`; rowCount?: number } - -function advanceResultSettlementModel( - state: ResultSettlementModelState, - action: ResultSettlementModelAction, -): ResultSettlementModelState { - const next = new Map(state) - - if (action.type === `start-refetch`) { - if (next.has(action.operation)) { - throw new Error(`A public refetch operation must be unique`) - } - next.set(action.operation, { phase: `waiting-for-result` }) - return next - } - - if (action.type === `retire`) { - next.forEach((operation, id) => { - if (operation.phase !== `terminal`) { - next.set(id, { phase: `terminal`, outcome: `cancelled` }) - } - }) - return next - } - - if (action.type === `refresh-succeeded`) { - const waiting = [...next].filter( - ([, operation]) => - operation.phase === `waiting-for-refresh` && - operation.barrier === action.barrier, - ) - if (waiting.length === 0) { - throw new Error(`A replacement result requires its defer generation`) - } - waiting.forEach(([id]) => { - next.set( - id, - action.deferOn - ? { phase: `waiting-for-refresh`, barrier: action.deferOn } - : action.result === `applicable` - ? { - phase: `terminal`, - outcome: `applied`, - rowCount: action.rowCount, - } - : { phase: `terminal`, outcome: `rejected` }, - ) - }) - return next - } - - const operation = next.get(action.operation) - if (operation?.phase !== `waiting-for-result`) { - throw new Error(`A Query result requires one waiting public refetch`) - } - next.set( - action.operation, - action.deferOn - ? { phase: `waiting-for-refresh`, barrier: action.deferOn } - : action.result === `applicable` - ? { - phase: `terminal`, - outcome: `applied`, - rowCount: action.rowCount, - } - : { phase: `terminal`, outcome: `rejected` }, - ) - return next -} - -function startResultSettlementModel( - operation: string, -): ResultSettlementModelState { - return advanceResultSettlementModel(new Map(), { - type: `start-refetch`, - operation, - }) -} - -function expectPublicRefetchObservation( - state: ResultSettlementModelState, - operationId: string, - observation: PublicRefetchObservation, -): void { - const operation = state.get(operationId) - if (!operation) throw new Error(`Unknown public refetch operation`) - - if (operation.phase !== `terminal`) { - expect(observation).toEqual({ settled: false }) - return +import type { Observation, Verdict } from '../../db/tests/oracle-property.js' +import type { Item, MetadataRecorder } from './ownership-lifecycle-support.js' +import type { LoadSubsetOptions } from '@tanstack/db' +import type { QueryFunctionContext } from '@tanstack/query-core' + +afterEach(runCleanups) +type RecordFact = (checkpoint: string, value: unknown) => void + +/** Wait for a production boundary without computing an expected row. If the + * boundary does not arrive, record the last fact and return; the property's + * check identifies that cut rather than losing the mismatch to a setup error. */ +async function boundary(test: () => boolean): Promise { + for (let turn = 0; turn < 200; turn++) { + if (test()) return true + await new Promise((resolve) => setTimeout(resolve, 5)) } - - expect(observation).toEqual( - operation.outcome === `applied` - ? { - settled: true, - outcome: `fulfilled`, - ...(operation.rowCount === undefined - ? {} - : { rowCount: operation.rowCount }), - } - : { settled: true, outcome: `rejected` }, - ) -} - -/** - * `collection.utils.refetch()` is normally an application barrier. The public - * contract is `docs/collections/query-collection.md#controlling-refetch-behavior`: - * when no user mutation is persisting and no mutation handler is active, - * successful fulfillment waits for every result accepted for Collection - * application, including an application with no row diff. Rejection is - * fail-fast and may leave independent obligations pending. While publication - * is blocked by a persisting mutation or active handler, every call uses the - * Query fetch boundary, including an overlapping external call. - * - * The handler parameter and every matching mutation alias preserve the real - * external Collection identity. This keeps strict equality and identity-keyed - * registries valid inside shared persistence handlers. Handler activity keeps - * refetch at the Query fetch boundary without replacing those public objects. - * The same acyclic fetch boundary applies when a manual transaction, - * `createOptimisticAction`, or a direct handler awaits a captured Collection: - * those documented forms still cannot wait on publication blocked by their - * own transaction. The phase law applies in both start orders. A refresh that - * begins first must change to the fetch boundary if a mutation starts before - * its result application can publish. - * - * Model mapping: - * - `callId` and `resultId` are model-only identities for one public call and - * its ordered Query results; they are not production generation counters. - * - `accept-result` means the observer result has an exact application promise. - * - `fulfill-application` and `reject-application` mean that promise settled. - * - `retire-application` means cancellation or supersession won before the - * application no-cancel point and therefore rejects that exact obligation. - * - `publicValues` represents Collection rows at the public checkpoint. - * - `RefetchCallSettlementModel` splits each tracked Query fetch from the zero - * or more Collection applications it causes. Same-key background work is a - * separate source even when it occurs while that fetch is active. - * - `HandlerRefetchBoundaryModel` excludes the causally queued Collection - * application from the handler call while retaining its later error record. - * - `HandlerIdentityObservation` is a model-only projection of strict - * transaction, mutation, and Collection identity plus identity-keyed - * registry results. Its bounded grammar crosses insert, update, and delete - * through handler parameter and mutation-alias access during explicit refetch. - * Historical operation labels are replay coordinates only, not model state. - * - `MutationRefreshLivenessModel` is a wait-for graph. Its mutation, fetch, - * and application nodes map to the user transaction, Query promise, and - * Collection publication boundary. Invocation labels test both owned and - * overlapping external calls but do not affect the phase-based law. - * - `QueryCacheWorkModel` treats Query ownership as the only edge authorizing - * Collection cache-listener work. The driver subtracts Query Core's own - * microtask baseline before comparing that bounded diagnostic. - * - `FetchRecordLifecycleModel` gives a terminally cancelled fetch no live - * causal record. The driver observes retained state as extra cleanup work at - * the next fetch without using production record classification. - * - * Results skipped before application because refresh is deferred, or because a - * manual-write snapshot is unchanged, are outside this accepted-result model. - * Invalid query shapes remain the input-validation contract in `query.test.ts`. - * A successful tracked result is otherwise admitted even when its on-demand - * Query has no listeners. A Query removed after the call snapshot retains an - * `undefined` result slot rather than turning lifecycle churn into a TypeError. - * - * State minimality: result order is observable in the returned array; pending - * result identity and causal source control call settlement; accepted values - * control publication; `throwOnError` distinguishes rejection from suppressed - * failure; separate calls retain the outcome of a superseded caller. Handler - * Collection identity is observed through the parameter, matching mutation - * aliases, and identity-keyed registries. Direct Collection handlers own one - * Collection, so unrelated multi-Collection mutation aliases are outside this - * driver. - * - * Embedded ORC-012 review record for this extension: ORC-001 through 006 and - * ORC-008 through 010 apply and are evidenced by this prose, independent - * reducers, bounded grammar with an exhaustiveness assertion, real drivers, - * observation helpers, wrong-result controls, state explanation, vocabulary - * mapping, and aggregate cleanup. ORC-007 does not apply because this bounded - * grammar is not a generated campaign. ORC-011 applies to the shared deadlock - * claim: the wait-for-graph formulation is structurally separate from the - * production drivers and from the earlier result/application ledger. This - * executable comment records every ORC-001 through ORC-011 outcome; no - * verdict-critical evidence lives only in the PR description. - */ -type RefetchApplicationOutcome = `pending` | `resolved` | `rejected` - -type ExplicitRefetchCallModel = { - resultOrder: Array - pendingResultIds: Set - acceptedValues: Map - throwOnError: boolean - outcome: RefetchApplicationOutcome -} - -type ExplicitRefetchApplicationModel = { - calls: Map - publicValues: Map -} - -type ExplicitRefetchApplicationAction = - | { - type: `start-call` - callId: number - resultIds: Array - throwOnError: boolean - } - | { - type: `accept-result` - callId: number - resultId: string - value: string - } - | { type: `fulfill-application`; callId: number; resultId: string } - | { type: `reject-application`; callId: number; resultId: string } - | { type: `retire-application`; callId: number; resultId: string } - -type ExplicitRefetchObservation = { - publicValues: Record - refetch: RefetchApplicationOutcome - resultOrder?: Array + return test() } -function reduceExplicitRefetchApplication( - model: ExplicitRefetchApplicationModel, - action: ExplicitRefetchApplicationAction, -): ExplicitRefetchApplicationModel { - if (action.type === `start-call`) { - if (model.calls.has(action.callId)) { - throw new Error(`Refetch call ${action.callId} already exists`) - } - if (new Set(action.resultIds).size !== action.resultIds.length) { - throw new Error( - `A refetch call cannot contain duplicate result identities`, +/** Domain checks retain every observation cut in the old driver. No expected + * value is recorded by a production callback: models build the owed facts + * from authored history data, and this check compares the ordered trace. */ +function checkCuts( + actual: ReadonlyArray, + expected: ReadonlyArray, + law: string, +): Verdict { + for (let i = 0; i < expected.length; i++) { + const wanted = expected[i]! + const got = actual[i] + if (!got) + return violation( + wanted.checkpoint, + 'the promised observation cut was not reached', + law, ) - } - const calls = new Map(model.calls) - calls.set(action.callId, { - resultOrder: [...action.resultIds], - pendingResultIds: new Set(action.resultIds), - acceptedValues: new Map(), - throwOnError: action.throwOnError, - outcome: action.resultIds.length === 0 ? `resolved` : `pending`, - }) - return { ...model, calls } - } - - const call = model.calls.get(action.callId) - if (!call) throw new Error(`Unknown refetch call ${action.callId}`) - if (!call.resultOrder.includes(action.resultId)) { - throw new Error(`Unknown result ${action.resultId}`) - } - if (!call.pendingResultIds.has(action.resultId)) { - throw new Error(`Result ${action.resultId} already settled`) - } - - if (action.type === `accept-result`) { - if (call.acceptedValues.has(action.resultId)) { - throw new Error(`Result ${action.resultId} already accepted`) - } - const calls = new Map(model.calls) - calls.set(action.callId, { - ...call, - acceptedValues: new Map(call.acceptedValues).set( - action.resultId, - action.value, - ), - }) - return { ...model, calls } - } - - const acceptedValue = call.acceptedValues.get(action.resultId) - if (acceptedValue === undefined) { - throw new Error(`Cannot settle a result before it is accepted`) - } - - const pendingResultIds = new Set(call.pendingResultIds) - pendingResultIds.delete(action.resultId) - const calls = new Map(model.calls) - if (action.type === `fulfill-application`) { - const publicValues = new Map(model.publicValues).set( - action.resultId, - acceptedValue, - ) - calls.set(action.callId, { - ...call, - pendingResultIds, - outcome: - call.outcome === `rejected` - ? `rejected` - : pendingResultIds.size === 0 - ? `resolved` - : `pending`, - }) - return { calls, publicValues } - } - - calls.set(action.callId, { - ...call, - pendingResultIds, - outcome: call.throwOnError - ? `rejected` - : pendingResultIds.size === 0 - ? `resolved` - : `pending`, - }) - return { ...model, calls } -} - -function observeExplicitRefetchApplication( - model: ExplicitRefetchApplicationModel, - callId: number, -): ExplicitRefetchObservation { - const call = model.calls.get(callId) - if (!call) throw new Error(`Unknown refetch call ${callId}`) - return { - publicValues: Object.fromEntries( - call.resultOrder.map((resultId) => [ - resultId, - model.publicValues.get(resultId), - ]), - ), - refetch: call.outcome, - ...(call.outcome === `resolved` - ? { resultOrder: call.resultOrder } - : undefined), - } -} - -function expectExplicitRefetchObservation( - actual: ExplicitRefetchObservation, - expected: ExplicitRefetchObservation, -): void { - expect(actual).toEqual(expected) -} - -/** - * This model-only call ledger separates Query fetch settlement from each - * causally owned Collection application. Same-key background applications stay - * observable to the model but do not become call obligations. A successful - * call waits for every owned fetch and application obligation. A throwing call - * rejects at its first owned failure; other obligations may remain pending. - * This is the `Promise.all` aggregate promised by the public utility, not an - * all-settled barrier. - */ -type RefetchCallSettlementModel = { - queryOrder: Array - fetches: Map - applications: Map< - string, - { - queryId: string - outcome: `pending` | `fulfilled` | `rejected` - source: `call` | `background` - } - > - throwOnError: boolean -} - -type RefetchCallSettlementAction = - | { - type: `accept-application` - queryId: string - applicationId: string - source?: `call` | `background` - } - | { - type: `settle-fetch` - queryId: string - outcome: `fulfilled` | `rejected` - } - | { type: `remove-query`; queryId: string } - | { - type: `settle-application` - applicationId: string - outcome: `fulfilled` | `rejected` - } - -function createRefetchCallSettlementModel( - queryOrder: Array, - throwOnError: boolean, -): RefetchCallSettlementModel { - if (new Set(queryOrder).size !== queryOrder.length) { - throw new Error(`A refetch call cannot contain duplicate Query identities`) - } - return { - queryOrder: [...queryOrder], - fetches: new Map(queryOrder.map((queryId) => [queryId, `pending`])), - applications: new Map(), - throwOnError, - } -} - -function reduceRefetchCallSettlement( - model: RefetchCallSettlementModel, - action: RefetchCallSettlementAction, -): RefetchCallSettlementModel { - if (action.type === `accept-application`) { - if (model.fetches.get(action.queryId) !== `pending`) { - throw new Error(`Applications belong to an active Query fetch`) - } - if (model.applications.has(action.applicationId)) { - throw new Error(`Application ${action.applicationId} already exists`) - } - return { - ...model, - applications: new Map(model.applications).set(action.applicationId, { - queryId: action.queryId, - outcome: `pending`, - source: action.source ?? `call`, - }), - } - } - - if (action.type === `remove-query`) { - if (model.fetches.get(action.queryId) !== `pending`) { - throw new Error(`Query ${action.queryId} is not pending removal`) - } - return { - ...model, - fetches: new Map(model.fetches).set(action.queryId, `removed`), - } - } - - if (action.type === `settle-fetch`) { - if (model.fetches.get(action.queryId) !== `pending`) { - throw new Error(`Query fetch ${action.queryId} already settled`) - } - return { - ...model, - fetches: new Map(model.fetches).set(action.queryId, action.outcome), - } - } - - const application = model.applications.get(action.applicationId) - if (!application) { - throw new Error(`Unknown application ${action.applicationId}`) - } - if (application.outcome !== `pending`) { - throw new Error(`Application ${action.applicationId} already settled`) - } - return { - ...model, - applications: new Map(model.applications).set(action.applicationId, { - ...application, - outcome: action.outcome, - }), - } -} - -function observeRefetchCallSettlement(model: RefetchCallSettlementModel): { - refetch: RefetchApplicationOutcome - resultOrder?: Array -} { - const fetchOutcomes = [...model.fetches.values()] - const applicationOutcomes = [...model.applications.values()] - .filter(({ source }) => source === `call`) - .map(({ outcome }) => outcome) - if ( - model.throwOnError && - [...fetchOutcomes, ...applicationOutcomes].includes(`rejected`) - ) { - return { refetch: `rejected` } - } - if ( - fetchOutcomes.includes(`pending`) || - applicationOutcomes.includes(`pending`) - ) { - return { refetch: `pending` } - } - return { - refetch: `resolved`, - resultOrder: model.queryOrder.map((queryId) => - model.fetches.get(queryId) === `removed` ? undefined : queryId, - ), - } -} - -/** - * A handler-phase refetch has one Query fetch obligation. Its accepted - * Collection application remains outside the handler boundary because that - * application is queued behind the invoking mutation transaction. A later - * application failure is recorded by Collection error utilities; it cannot - * retroactively change the already-settled handler call. - */ -type HandlerRefetchBoundaryModel = { - fetch: `pending` | `fulfilled` | `rejected` - application: `unaccepted` | `pending` | `fulfilled` | `rejected` - throwOnError: boolean -} - -type HandlerRefetchBoundaryAction = - | { type: `accept-application` } - | { type: `settle-fetch`; outcome: `fulfilled` | `rejected` } - | { type: `settle-application`; outcome: `fulfilled` | `rejected` } - -function reduceHandlerRefetchBoundary( - model: HandlerRefetchBoundaryModel, - action: HandlerRefetchBoundaryAction, -): HandlerRefetchBoundaryModel { - if (action.type === `accept-application`) { - if (model.application !== `unaccepted`) { - throw new Error(`Handler application already accepted`) - } - return { ...model, application: `pending` } - } - if (action.type === `settle-fetch`) { - if (model.fetch !== `pending`) { - throw new Error(`Handler Query fetch already settled`) - } - return { ...model, fetch: action.outcome } - } - if (model.application !== `pending`) { - throw new Error(`Handler application is not pending`) - } - return { ...model, application: action.outcome } -} - -function observeHandlerRefetchBoundary(model: HandlerRefetchBoundaryModel): { - refetch: RefetchApplicationOutcome - applicationErrorRecorded: boolean -} { - return { - refetch: - model.fetch === `pending` - ? `pending` - : model.fetch === `rejected` && model.throwOnError - ? `rejected` - : `resolved`, - applicationErrorRecorded: model.application === `rejected`, - } -} - -/** - * A refresh started while a mutation is persisting cannot wait for publication - * that the transaction prevents. This law comes from the documented - * `createOptimisticAction` protocol in `packages/db/src/optimistic-action.ts` - * and `docs/guides/mutations.md`: a mutation function may await Query - * `refetch()` before it returns, while its optimistic transaction remains - * active until that function returns. - * - * This reference model is a wait-for graph, not a copy of Query Collection - * branches. `mutation` is the user transaction, `fetch` is the Query boundary, - * and `application` is publication into Collection rows. During that phase the - * refresh depends on `fetch`, regardless of which Collection reference invoked - * it. When the mutation owns the refresh, an `application` dependency would - * create the forbidden cycle mutation -> refresh -> application -> mutation; - * for an external caller it would add an unrelated persistence delay. - */ -/** - * Before the RC, these matrices exercised refetch and the removed clearError - * utility. Both paths now explicitly refetch. Keep the former operation labels - * only as replay coordinates: existing traces, fixture IDs, and all 12 runs of - * each matrix survive. The labels add no semantic history or model distinction. - * To select the former clearError rows, use the test-name pattern - * `legacy replay:.*clearError`; Vitest can quote interpolated string values. - */ -type LegacyRefreshReplayCoordinate = `refetch` | `clearError` -const legacyRefreshReplayCoordinates = [`refetch`, `clearError`] as const - -type LegacyRefreshReplay = { - legacyOperation: LegacyRefreshReplayCoordinate -} - -type MutationRefreshInvocation = - | `manual-transaction` - | `optimistic-action` - | `captured-handler-collection` - | `external-handler-overlap` - | `external-persisted-application-overlap` - -type MutationRefreshSchedule = - | `mutation-first` - | `refresh-first-during-fetch` - | `refresh-first-during-application` - -type MutationRefreshHistory = { - invocation: MutationRefreshInvocation - schedule: MutationRefreshSchedule -} - -const mutationRefreshInvocations = [ - `manual-transaction`, - `optimistic-action`, - `captured-handler-collection`, - `external-handler-overlap`, - `external-persisted-application-overlap`, -] as const -const mutationRefreshHistoryGrammar: ReadonlyArray< - MutationRefreshHistory & LegacyRefreshReplay -> = [ - ...mutationRefreshInvocations - .filter( - (invocation) => - invocation !== `external-handler-overlap` && - invocation !== `external-persisted-application-overlap`, - ) - .flatMap((invocation) => - legacyRefreshReplayCoordinates.map((legacyOperation) => ({ - invocation, - legacyOperation, - schedule: `mutation-first` as const, - })), - ), - ...legacyRefreshReplayCoordinates.flatMap((legacyOperation) => - ([`mutation-first`, `refresh-first-during-fetch`] as const).map( - (schedule) => ({ - invocation: `external-handler-overlap` as const, - legacyOperation, - schedule, - }), - ), - ), - ...legacyRefreshReplayCoordinates.map((legacyOperation) => ({ - invocation: `external-persisted-application-overlap` as const, - legacyOperation, - schedule: `refresh-first-during-application` as const, - })), -] - -type MutationRefreshLivenessModel = { - fetch: `pending` | `fulfilled` | `rejected` - application: `unaccepted` | `pending` | `fulfilled` | `rejected` - boundary: `fetch` | `application` - throwOnError: boolean -} - -type MutationRefreshLivenessAction = - | { type: `accept-application` } - | { type: `settle-fetch`; outcome: `fulfilled` | `rejected` } - | { type: `settle-application`; outcome: `fulfilled` | `rejected` } - -function reduceMutationRefreshLiveness( - model: MutationRefreshLivenessModel, - action: MutationRefreshLivenessAction, -): MutationRefreshLivenessModel { - if (action.type === `accept-application`) { - if (model.application !== `unaccepted`) { - throw new Error(`Mutation refresh application already accepted`) - } - return { ...model, application: `pending` } - } - if (action.type === `settle-fetch`) { - if (model.fetch !== `pending`) { - throw new Error(`Mutation refresh fetch already settled`) - } - return { ...model, fetch: action.outcome } - } - if (model.application !== `pending`) { - throw new Error(`Mutation refresh application is not pending`) - } - return { ...model, application: action.outcome } -} - -function observeMutationRefreshLiveness(model: MutationRefreshLivenessModel): { - refresh: RefetchApplicationOutcome - application: `unaccepted` | RefetchApplicationOutcome -} { - const refresh = - model.fetch === `pending` - ? `pending` - : model.fetch === `rejected` && model.throwOnError - ? `rejected` - : model.boundary === `application` && model.application === `pending` - ? `pending` - : model.boundary === `application` && - model.application === `rejected` && - model.throwOnError - ? `rejected` - : `resolved` - return { - refresh, - application: - model.application === `unaccepted` - ? `unaccepted` - : model.application === `pending` - ? `pending` - : model.application === `fulfilled` - ? `resolved` - : `rejected`, - } -} - -/** - * Query-cache observation is an ownership boundary. A Collection may schedule - * bookkeeping for a Query it owns, but a foreign Query has no modeled edge to - * that Collection and therefore contributes no Collection microtasks. The - * production observation subtracts Query Core's own baseline work, leaving - * only work introduced by registered Query Collections. - */ -type QueryCacheWorkModel = { - collectionCount: number - queryOwnership: `owned` | `foreign` -} - -function observeQueryCacheWork(model: QueryCacheWorkModel): { - collectionMicrotasks: number -} { - return { - collectionMicrotasks: - model.queryOwnership === `owned` ? model.collectionCount : 0, - } -} - -type FetchRecordLifecycleModel = { - state: `active` | `cancelled` -} - -function observeFetchRecordSuccessorWork(model: FetchRecordLifecycleModel): { - extraCleanupMicrotasks: number -} { - return { - extraCleanupMicrotasks: model.state === `active` ? 1 : 0, - } -} - -type MutationHandlerKind = `insert` | `update` | `delete` -type HandlerCollectionAccessPath = `parameter` | `mutation-alias` - -type HandlerCollectionHistory = { - handlerKind: MutationHandlerKind - accessPath: HandlerCollectionAccessPath -} - -type HandlerIdentityObservation = HandlerCollectionHistory & { - matchingAliasCount: number - transactionIsExternal: boolean - allMutationsPreserveIdentity: boolean - parameterIsExternal: boolean - allAliasesMatchParameter: boolean - anyAliasMatchesExternal: boolean - externalMapMatchesAnyAlias: boolean - parameterMapMatchesAllAliases: boolean - externalWeakMapMatchesAnyAlias: boolean - parameterWeakMapMatchesAllAliases: boolean -} - -const mutationHandlerKinds = [`insert`, `update`, `delete`] as const -const handlerCollectionAccessPaths = [`parameter`, `mutation-alias`] as const -const handlerCollectionHistoryGrammar: ReadonlyArray< - HandlerCollectionHistory & LegacyRefreshReplay -> = mutationHandlerKinds.flatMap((handlerKind) => - handlerCollectionAccessPaths.flatMap((accessPath) => - legacyRefreshReplayCoordinates.map((legacyOperation) => ({ - handlerKind, - accessPath, - legacyOperation, - })), - ), -) - -/** - * A direct mutation handler receives the real Collection. Every matching - * mutation alias preserves that identity. Strict equality and registries keyed - * by the external or parameter reference therefore select the same mutations. - * The model records only this public equality relation. - */ -function observeHandlerIdentity( - history: HandlerCollectionHistory, - matchingAliasCount: number, -): HandlerIdentityObservation { - if (matchingAliasCount < 1) { - throw new Error(`A direct handler requires a matching mutation alias`) - } - return { - handlerKind: history.handlerKind, - accessPath: history.accessPath, - matchingAliasCount, - transactionIsExternal: true, - allMutationsPreserveIdentity: true, - parameterIsExternal: true, - allAliasesMatchParameter: true, - anyAliasMatchesExternal: true, - externalMapMatchesAnyAlias: true, - parameterMapMatchesAllAliases: true, - externalWeakMapMatchesAnyAlias: true, - parameterWeakMapMatchesAllAliases: true, + const difference = differs(wanted.checkpoint, law, got, wanted) + if (difference) return difference } + return differs('trace-length', law, actual.length, expected.length) ?? pass } - -function expectHandlerIdentityObservation( - actual: HandlerIdentityObservation, - expected: HandlerIdentityObservation, -): void { - expect(actual).toEqual(expected) -} - -/** - * ## A reacquired observer lease owns the current Query result - * - * Law: reacquiring a released demand creates a new observer lease. Its - * current result is the Query's latest successful data, so it owns that - * data's rows from reacquisition until its own release. An older acquisition - * release that publishes later retires only the older lease. If the new - * lease starts a refetch, a successful refetch replaces the lease's result. - * A failed refetch keeps the last successful data, so it keeps those rows. - * `staleTime` decides whether a refetch starts. It cannot decide whether the - * cached result is owned. - * - * Authority: the reacquisition law in the history above, and the TanStack - * Query rules that a stale Query refetches when an observer mounts and that - * a failed refetch keeps the previous `data`. - * - * Limits: one reacquired on-demand subset whose result has one or two - * rows, zero to two peer subsets that own the first row, no persisted - * metadata, and one unrelated optimistic transaction that holds - * publication. A release commit that - * rejects and the direct-write path have their own histories below. Eager - * mode cannot reach the premise: it has no subset release, and ending an - * eager sync run removes its Query from the cache, so a restart fetches - * instead of replaying cached data. - */ const solo: Item = { id: `solo`, category: `detail`, name: `Solo` } type ReacquisitionHistory = { @@ -1186,4084 +293,40 @@ const publishedOwnerHistories: Array = ( ), ) -/** - * An empty persisted baseline is established by its first scan in a sync - * run. With no durable writer or cache rotation, later retained Query - * reacquisitions have no row ownership to reconcile and need no further - * full-store scan. - * A new sync run has no such evidence: a durable owner may have appeared - * while this Collection was absent. This is a bounded work proposal beside - * the existing row-ownership law, not a claim about arbitrary peer writes. - * - * The independent model retains only whether the current sync run scanned - * its empty baseline. `restart` removes that evidence. The fixed histories - * distinguish retained reacquisition from a cold run. They drive the real - * Query observer and persistence scan seam. They compare scan counts and - * public rows after each applied result. - */ -const expectedEmptyBaselineScans = ( - history: ReadonlyArray<`empty-result` | `restart`>, -): number => { - let scans = 0 - let emptyBaselineKnown = false - for (const action of history) { - if (action === `restart`) { - emptyBaselineKnown = false - } else if (!emptyBaselineKnown) { - scans++ - emptyBaselineKnown = true - } - } - return scans -} - -/** - * Law: a direct row keeps its public value and persisted owners through every - * legal delete, reinsert, refetch, and subset release, as the lifetime reference - * in the test below states. One call runs one history of that grammar. - */ -async function runDirectRowLifetimeHistory({ - ownerCount, - withMetadata, - history, -}: { - ownerCount: 1 | 2 - withMetadata: boolean - history: - | `update` - | `delete` - | `delete-insert` - | `delete-upsert` - | `delete-insert-refetch` -}): Promise { - const id = `direct-row-lifetime-${ownerCount}-${withMetadata}-${history}` - const replacement = { ...shared, name: `Reinserted` } - const serverReplacement = { ...shared, name: `Server result` } - const initialResults = Array.from({ length: ownerCount }, () => [shared]) - const laterResults = Array.from({ length: ownerCount }, () => [ - serverReplacement, - ]) - const metadata: MetadataRecorder = { rows: new Map(), writes: [] } - const { collection, queryFn } = createOwnershipFixture({ - id, - results: [...initialResults, ...laterResults], - metadataRecorder: withMetadata ? metadata : undefined, - }) - const demands = [categorySubset(`detail`), categorySubset(`list`)].slice( - 0, - ownerCount, - ) - const demandHashes = demands.map((demand) => - hashKey([id, getLoadSubsetDemandKey(demand)]), - ) - const claims = new Set() - let expected: Item | undefined - let insertedDirectly = false - const observe = () => { - expect(collection.get(shared.id)?.name).toBe(expected?.name) - expect(rows(collection)).toEqual(expected ? [shared.id] : []) - if (withMetadata) - expect( - persistedOwners(collection._state.syncedMetadata, shared.id), - ).toEqual([...claims].map((index) => demandHashes[index]!).sort()) - } - for (const [index, demand] of demands.entries()) { - await collection._sync.loadSubset(demand) - claims.add(index) - expected = shared - insertedDirectly = false - observe() - } - if (history === `update`) { - await collection.utils.writeUpdate({ id: shared.id, name: `Updated` }) - expected = { ...shared, name: `Updated` } - observe() - } else { - await collection.utils.writeDelete(shared.id) - expected = undefined - claims.clear() - insertedDirectly = false - observe() - if (history !== `delete`) { - if (history === `delete-upsert`) - await collection.utils.writeUpsert(replacement) - else await collection.utils.writeInsert(replacement) - expected = replacement - insertedDirectly = true - observe() - } - } - if (history === `delete-insert-refetch`) { - await collection.utils.refetch() - demands.forEach((_, index) => claims.add(index)) - expected = serverReplacement - insertedDirectly = false - observe() - } - for (const [index, demand] of demands.entries()) { - collection._sync.unloadSubset(demand) - claims.delete(index) - if (claims.size === 0 && !insertedDirectly) expected = undefined - observe() - } - expect(queryFn).toHaveBeenCalledTimes( - ownerCount * (history === `delete-insert-refetch` ? 2 : 1), - ) -} - -/** - * Law: a direct write and a physical fetch are ordered by authored position; the - * later of the two supplies the shared key. One call runs one order history of - * the grammar below against the reference it builds. - */ -async function runDirectWriteOrderHistory({ - order, - laterWrite, - cacheEdit, - finalRefetch, -}: { - order: `write-first` | `fetch-first` - laterWrite: `none` | `same-key` | `other-key` - cacheEdit: boolean - finalRefetch: boolean -}): Promise { - type Row = { id: string; name: string } - const queryClient = createQueryClient() - const queryKey = [`shared-post-write-cached-before-acquisition`] - let requestNumber = 0 - const queryFn = vi.fn(() => - Promise.resolve([ - { - id: `x`, - name: ++requestNumber === 1 ? `Server 1` : `Server 2`, - }, - ]), - ) - const makeCollection = (id: string) => - createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey, - queryFn, - getKey: (row) => row.id, - syncMode: `on-demand`, - staleTime: Infinity, - startSync: true, - }), - ) - const a = makeCollection(`cached-newer-a`) - const b = makeCollection(`cached-newer-b`) - cleanups.push(async () => { - await Promise.all([a.cleanup(), b.cleanup()]) - queryClient.clear() - }) - const reference: { - position: number - directX?: { value: string; position: number } - remoteX?: { value: string; position: number } - cacheStillFetched: boolean - } = { position: 0, cacheStillFetched: false } - const writeX = async (name: string) => { - reference.directX = { value: name, position: ++reference.position } - if (a.has(`x`)) await a.utils.writeUpdate({ id: `x`, name }) - else await a.utils.writeInsert({ id: `x`, name }) - } - const fetchB = async (first: boolean) => { - const started = ++reference.position - if (first) await b._sync.loadSubset({}) - else await b.utils.refetch({ throwOnError: true }) - reference.remoteX = { - value: first ? `Server 1` : `Server 2`, - position: started, - } - reference.cacheStillFetched = true - } - if (order === `write-first`) await writeX(`Written`) - await fetchB(true) - if (order === `fetch-first`) await writeX(`Written`) - if (laterWrite === `same-key`) await writeX(`Later write`) - if (laterWrite === `other-key`) { - reference.position++ - await a.utils.writeInsert({ id: `y`, name: `Other write` }) - } - if (cacheEdit) { - queryClient.setQueryData(queryKey, [{ id: `x`, name: `Local cache edit` }]) - reference.cacheStillFetched = false - } - if (finalRefetch) await fetchB(false) - const callsBeforeAcquisition = queryFn.mock.calls.length - await a._sync.loadSubset({}) - const revalidatedCacheEdit = cacheEdit && !finalRefetch - expect(queryFn).toHaveBeenCalledTimes( - callsBeforeAcquisition + Number(revalidatedCacheEdit), - ) - if (revalidatedCacheEdit) { - reference.remoteX = { - value: `Server 2`, - position: ++reference.position, - } - reference.cacheStillFetched = true - } - const { directX, remoteX, cacheStillFetched } = reference - if (!directX || !remoteX) throw new Error(`Illegal authority history`) - const remoteMayReplace = - cacheStillFetched && remoteX.position > directX.position - expect(a.get(`x`)?.name).toBe( - remoteMayReplace ? remoteX.value : directX.value, - ) - // The complete unbounded response omits y, so it retires that - // ownerless direct row; without such a response the row survives. - expect(a.get(`y`)?.name).toBe( - laterWrite === `other-key` && !finalRefetch && !revalidatedCacheEdit - ? `Other write` - : undefined, - ) - a._sync.unloadSubset({}) - expect(a.get(`x`)?.name).toBe(remoteMayReplace ? undefined : directX.value) -} - -/** - * Law: a physical fetch takes its position when it starts, so the newest started - * fetch supplies a shared row, and releasing a newer owner does not revert it. One - * call runs one history of the grammar below and compares public values. - */ -async function runOverlappingSubsetHistory({ - existingOwner, - settleNewerFirst, - directWrite, - sameValue, -}: { - existingOwner: boolean - settleNewerFirst: boolean - directWrite: `none` | `between` | `after` - sameValue: boolean -}): Promise { - const id = `overlapping-fetch-value-${existingOwner}-${settleNewerFirst}-${directWrite}-${sameValue}` - const older = createDeferred>() - const newer = createDeferred>() - const initial = { ...shared, name: `Initial` } - const olderRow = { ...shared, name: `Older` } - const newerRow = { ...shared, name: sameValue ? `Initial` : `Newer` } - const manual = { ...shared, name: `Manual` } - const { collection, queryClient, queryFn } = createOwnershipFixture({ - id, - results: existingOwner - ? [[initial], older.promise, newer.promise] - : [older.promise, newer.promise], - }) - cleanups.push(async () => { - older.resolve([olderRow]) - newer.resolve([newerRow]) - }) - const olderSubset = { where: eq(new IR.PropRef([`category`]), `shared`) } - const newerSubset = { where: eq(new IR.PropRef([`id`]), `shared`) } - expect(getLoadSubsetDemandKey(olderSubset)).not.toBe( - getLoadSubsetDemandKey(newerSubset), - ) - // The provider's authored rows must be legal for their real scopes. - // This checks fixture admission only; the reference below still judges - // public values independently of the production expression compiler. - expect( - toBooleanPredicate(compileSingleRowExpression(olderSubset.where)(initial)), - ).toBe(true) - expect( - toBooleanPredicate(compileSingleRowExpression(olderSubset.where)(olderRow)), - ).toBe(true) - expect( - toBooleanPredicate(compileSingleRowExpression(newerSubset.where)(newerRow)), - ).toBe(true) - - // Model: each accepted operation may replace the value only when its - // position exceeds the position of the accepted value already held. - // Claiming a subset and replacing a value are separate operations. - const reference: { - position: number - acceptedAt: number - value: string | undefined - owners: Set<`older` | `newer`> - } = { - position: 0, - acceptedAt: 0, - value: undefined, - owners: new Set(), - } - const start = () => ++reference.position - const acceptFetch = ( - owner: `older` | `newer`, - started: number, - value: string, - ) => { - reference.owners.add(owner) - if (started > reference.acceptedAt) { - reference.value = value - reference.acceptedAt = started - } - } - const acceptWrite = () => { - reference.value = manual.name - reference.acceptedAt = ++reference.position - } - const expectPublicRow = () => { - expect(collection.get(shared.id)?.name).toBe( - reference.owners.size > 0 ? reference.value : undefined, - ) - expect(collection.has(shared.id)).toBe(reference.owners.size > 0) - } - - if (existingOwner) { - const initialStart = start() - await collection._sync.loadSubset(olderSubset) - acceptFetch(`older`, initialStart, initial.name) - expectPublicRow() - } - const olderStart = start() - const olderLoad = collection._sync.loadSubset( - existingOwner ? { ...olderSubset, refetch: true } : olderSubset, - ) - void Promise.resolve(olderLoad).catch(() => undefined) - await vi.waitFor(() => - expect(queryFn).toHaveBeenCalledTimes(existingOwner ? 2 : 1), - ) - - if (directWrite === `between`) { - await collection.utils.writeUpdate(manual) - acceptWrite() - expectPublicRow() - } - const newerStart = start() - const newerLoad = collection._sync.loadSubset(newerSubset) - void Promise.resolve(newerLoad).catch(() => undefined) - await vi.waitFor(() => - expect(queryFn).toHaveBeenCalledTimes(existingOwner ? 3 : 2), - ) - expect(queryClient.getQueryCache().findAll({ queryKey: [id] })).toHaveLength( - 2, - ) - if (directWrite === `after`) { - await collection.utils.writeUpdate(manual) - acceptWrite() - expectPublicRow() - } - - const settle = async (owner: `older` | `newer`) => { - if (owner === `older`) { - older.resolve([olderRow]) - await olderLoad - acceptFetch(owner, olderStart, olderRow.name) - } else { - newer.resolve([newerRow]) - await newerLoad - acceptFetch(owner, newerStart, newerRow.name) - } - expectPublicRow() - } - if (settleNewerFirst) { - await settle(`newer`) - await settle(`older`) - } else { - await settle(`older`) - await settle(`newer`) - } - - const release = (owner: `older` | `newer`) => { - const subset = owner === `older` ? olderSubset : newerSubset - collection._sync.unloadSubset(subset) - if (owner === `older` && existingOwner) - collection._sync.unloadSubset(subset) - reference.owners.delete(owner) - expectPublicRow() - } - if (existingOwner) { - release(`older`) - release(`newer`) - } else { - release(`newer`) - release(`older`) - } - - // Reacquisition starts a new row-ownership lifetime. Only the acquired - // Query's retained cache is projected; an earlier overlap does not - // permanently rewrite a different Query entry. This legal neighbor - // distinguishes continuous-owner precedence from global precedence. - if (!existingOwner && settleNewerFirst && directWrite === `none`) { - reference.owners.add(`older`) - reference.value = olderRow.name - await collection._sync.loadSubset(olderSubset) - expectPublicRow() - expect(queryFn).toHaveBeenCalledTimes(2) - collection._sync.unloadSubset(olderSubset) - reference.owners.delete(`older`) - expectPublicRow() - - reference.owners.add(`newer`) - reference.value = newerRow.name - await collection._sync.loadSubset(newerSubset) - expectPublicRow() - expect(queryFn).toHaveBeenCalledTimes(2) - } -} - -/** - * Law: a fetch that started before a direct write keeps the server's rows for - * every key that write did not touch; a write recorded after the fetch began - * wins for its key. This reference overlays the writes on the fetched rows in - * order. It is the expected visible rows of the merge-cut histories below. - */ -// A pending application obligation can include persistence after publication. -// While its receipt is held, allow either complete source snapshot; never infer -// receipt settlement merely from visible rows. Once it fulfills, the waiter -// itself must read the confirmed snapshot. This composes the existing call -// ledger with Collection commit coherence without copying persistence machinery. -function checkPostHandlerRefetch( - model: RefetchCallSettlementModel, - observed: { - refetch: ReturnType - rows: string - waiterRows: string | undefined - }, - initial: string, - confirmed: string, -): void { - const expected = observeRefetchCallSettlement(model) - expect(observed.refetch).toEqual(expected) - if (expected.refetch === `pending`) { - expect([initial, confirmed]).toContain(observed.rows) - expect(observed.waiterRows).toBeUndefined() - } else { - expect(observed.rows).toBe(confirmed) - expect(observed.waiterRows).toBe(confirmed) - } -} - -// What this oracle owns: the query collection's mutation confirmation, explicit -// refetch application, fetch-record ownership and reacquisition (queryCollectionOptions -// and the manual-sync write path it reaches). -declareOracle({ - owns: [`src/query.ts#queryCollectionOptions`], - check: [ - checkMutationConfirmation, - checkPostHandlerRefetch, - expectPublicRefetchObservation, - runDirectRowLifetimeHistory, - runDirectWriteOrderHistory, - runOverlappingSubsetHistory, - ], - model: [ - advanceResultSettlementModel, - reduceExplicitRefetchApplication, - reduceRefetchCallSettlement, - reduceHandlerRefetchBoundary, - reduceMutationRefreshLiveness, - observeHandlerRefetchBoundary, - observeQueryCacheWork, - observeFetchRecordSuccessorWork, - modelReacquisition, - modelPublishedOwners, - expectedEmptyBaselineScans, - mergeDirectWritesOverFetch, - ], - grammar: [mutationRefreshHistoryGrammar, handlerCollectionHistoryGrammar], -}) - -describe(`query collection ownership lifecycle`, () => { - afterEach(async () => { - await runCleanups() - }) - - // The shared independent model owns handler outcome versus confirmation. - // This real QueryClient driver controls the server edit, explicit acceptance, - // and handler settlement separately. No focus/poll/invalidation trigger runs. - // Rejections after a committed server edit must not undo accepted source data. - describe(`mutation outcome and confirmation`, () => { - const mutationHistories = mutationConfirmationHistories(`query`) - const mutationReachStates = mutationHistories.map( - (h) => - `${h.operation}/${h.outcome}/${h.confirmation}/${h.returned}/peer:${h.peer}`, - ) - const mutationReach = createOracleReach( - `query.mutation-confirmation`, - mutationReachStates, - ) - afterAll(() => { - mutationReach.assertReached() - console.info( - `query.mutation-confirmation reach`, - Object.fromEntries(mutationReach.counts()), - ) - }) - it.each(mutationHistories)( - `refreshes only when the handler requests it: %j`, - async (history) => { - oracleRoute(`case`) - mutationReach.beginHistory() - const { operation, confirmation, outcome, returned } = history - const localReach = createOracleReach(`query.mutation-cuts`, [ - `entered`, - `settled`, - `settlement-waiter`, - ...(outcome === `reject-before-write` ? [] : [`server-written`]), - ...(confirmation === `none` ? [] : [`confirmation-accepted`]), - ...(confirmation === `refetch` ? [`confirmation-requested`] : []), - ...(confirmation === `none` && outcome !== `reject-before-write` - ? [`late-confirmed`] - : []), - ]) - localReach.beginHistory() - const queryClient = createQueryClient() - const enterRelease = createDeferred() - const confirmationRelease = createDeferred() - const settleRelease = createDeferred() - const entered = createDeferred() - const serverWritten = createDeferred() - const confirmationAccepted = createDeferred() - const refetchRequested = createDeferred() - const refetchResponse = createDeferred() - const failure = new Error(`handler ${outcome}`) - const initial = mutationConfirmationRows( - operation, - `initial`, - history.peer, - ) - const confirmed = mutationConfirmationRows( - operation, - `confirmed`, - history.peer, - ) - let server = structuredClone(initial) - let handlerWrites = 0 - let confirmationCalls = 0 - let confirmationStatus: MutationConfirmationObservation[`confirmationStatus`] = `not-requested` - let settlement: MutationConfirmationObservation[`settlement`] = `pending` - let error: unknown - let holdRefetch = confirmation === `refetch` - const queryFn = vi.fn(async () => { - if (confirmationCalls > 0 && holdRefetch) { - refetchRequested.resolve() - await refetchResponse.promise - } - return structuredClone(server) - }) - let releaseCollection: (() => Promise) | undefined - let verdict: Promise | undefined - await withOracleCleanup(async () => { - const handler = async ({ - collection: target, - }: Pick< - Parameters< - NonNullable< - QueryCollectionConfig[`onInsert`] - > - >[0], - `collection` - >) => { - entered.resolve() - await enterRelease.promise - if (outcome === `reject-before-write`) throw failure - server = structuredClone(confirmed) - handlerWrites++ - serverWritten.resolve() - await confirmationRelease.promise - if (confirmation !== `none`) { - confirmationCalls++ - confirmationStatus = `pending` - } - if (confirmation === `refetch`) await target.utils.refetch() - if (confirmation === `write`) { - if (operation === `delete`) await target.utils.writeDelete(1) - else - await target.utils.writeUpsert({ - ...mutationConfirmationInputs.confirmed, - }) - } - if (confirmation !== `none`) confirmationStatus = `accepted` - confirmationAccepted.resolve() - await settleRelease.promise - if (outcome === `reject-after-write`) throw failure - return returned === `legacy-disable` - ? { refetch: false } - : returned === `legacy-enable` - ? { refetch: true } - : undefined - } - const collection = createCollection( - queryCollectionOptions({ - queryKey: [`explicit-handler-refresh`, history], - queryClient, - queryFn, - getKey: (row) => row.id, - // JS callers may return coordination objects that TS now rejects. - onInsert: handler as unknown as NonNullable< - QueryCollectionConfig[`onInsert`] - >, - onUpdate: handler as unknown as NonNullable< - QueryCollectionConfig[`onUpdate`] - >, - onDelete: handler as unknown as NonNullable< - QueryCollectionConfig[`onDelete`] - >, - }), - ) - releaseCollection = () => collection.cleanup() - await atMutationCheckpoint( - collection.preload(), - `initial Query preload`, - ) - const transaction = - operation === `insert` - ? collection.insert({ ...mutationConfirmationInputs.optimistic }) - : operation === `delete` - ? collection.delete(1) - : collection.update(1, (draft) => { - draft.name = `optimistic` - }) - const observe = (): MutationConfirmationObservation => ({ - rows: collection.toArray - .map(({ id, name, stable }) => ({ id, name, stable })) - .sort((a, b) => a.id - b.id), - handlerWrites, - confirmationCalls, - fetches: queryFn.mock.calls.length, - providerRequests: 0, - confirmationStatus, - state: transaction.state, - settlement, - error, - }) - verdict = transaction.when(`settled`).then( - () => { - settlement = `fulfilled` - return observe() - }, - (reason: unknown) => { - settlement = `rejected` - error = reason - return observe() - }, - ) - await atMutationCheckpoint(entered.promise, `Query handler entry`) - checkMutationConfirmation(history, `entered`, observe(), failure) - localReach.reach(`entered`) - enterRelease.resolve() - if (outcome !== `reject-before-write`) { - await atMutationCheckpoint( - serverWritten.promise, - `Query server write`, - ) - checkMutationConfirmation( - history, - `server-written`, - observe(), - failure, - ) - localReach.reach(`server-written`) - confirmationRelease.resolve() - if (confirmation === `refetch`) { - await atMutationCheckpoint( - refetchRequested.promise, - `held Query refetch response`, - ) - checkMutationConfirmation( - history, - `confirmation-requested`, - observe(), - failure, - ) - localReach.reach(`confirmation-requested`) - holdRefetch = false - refetchResponse.resolve() - } - await atMutationCheckpoint( - confirmationAccepted.promise, - `Query confirmation acceptance`, - ) - if (confirmation !== `none`) { - checkMutationConfirmation( - history, - `confirmation-accepted`, - observe(), - failure, - ) - localReach.reach(`confirmation-accepted`) - } - settleRelease.resolve() - } - const atWaiter = await atMutationCheckpoint( - verdict, - `Query handler settlement`, - ) - checkMutationConfirmation(history, `settled`, atWaiter, failure) - localReach.reach(`settlement-waiter`) - checkMutationConfirmation(history, `settled`, observe(), failure) - localReach.reach(`settled`) - if (confirmation === `none` && outcome !== `reject-before-write`) { - // Backend success alone supplied no source confirmation. A later - // explicit reader may refetch; the frozen settlement cut stays old. - await atMutationCheckpoint( - collection.utils.refetch(), - `later explicit Query refetch`, - ) - checkMutationConfirmation( - history, - `late-confirmed`, - observe(), - failure, - ) - localReach.reach(`late-confirmed`) - checkMutationConfirmation(history, `settled`, atWaiter, failure) - } - localReach.assertReached() - mutationReach.reach( - `${operation}/${outcome}/${confirmation}/${returned}/peer:${history.peer}`, - ) - }, [ - () => { - enterRelease.resolve() - confirmationRelease.resolve() - settleRelease.resolve() - refetchResponse.resolve() - }, - () => - verdict && - atMutationCheckpoint(verdict, `Query cleanup settlement`), - () => releaseCollection?.(), - () => queryClient.clear(), - ]) - }, - ) - }) - - /** - * Handler exit restores the ordinary public-refetch boundary. A later call - * waits for its commit receipt, even if an earlier CRUD handler - * rejected. The existing call-obligation model owns that judgment. A real - * persisted wrapper holds the later durable commit with no optimistic work - * left; this distinguishes a leaked handler count from a legitimate mutation - * publication block. Ordinary source values are serialized without dropping - * fields; ordering is normalized because Collection order is unspecified. - */ - describe(`refetch application after handler exit`, () => { - const histories = ([`insert`, `update`, `delete`] as const).flatMap( - (operation) => [false, true].map((reject) => ({ operation, reject })), - ) - const reach = createOracleReach( - `query.post-handler-application`, - histories.flatMap((h) => [ - `${h.operation}/${h.reject}/application-held`, - `${h.operation}/${h.reject}/applied-waiter`, - ]), - ) - afterAll(() => { - reach.assertReached() - console.info( - `query.post-handler-application reach`, - Object.fromEntries(reach.counts()), - ) - }) - it.each([`early-receipt`, `partial-rows`, `stale-waiter`] as const)( - `rejects post-handler wrong observation: %s`, - (fault) => { - oracleRoute(`calibration`) - let model = createRefetchCallSettlementModel([`query`], true) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `query`, - applicationId: `later-result`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId: `query`, - outcome: `fulfilled`, - }) - if (fault === `stale-waiter`) - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `later-result`, - outcome: `fulfilled`, - }) - const actual = { - refetch: observeRefetchCallSettlement(model), - rows: `confirmed`, - waiterRows: fault === `stale-waiter` ? `confirmed` : undefined, - } - checkPostHandlerRefetch(model, actual, `initial`, `confirmed`) - if (fault === `early-receipt`) - actual.refetch = { refetch: `resolved`, resultOrder: [`query`] } - if (fault === `partial-rows`) actual.rows = `partial` - if (fault === `stale-waiter`) actual.waiterRows = `initial` - expect(() => - checkPostHandlerRefetch(model, actual, `initial`, `confirmed`), - ).toThrow() - }, - ) - it.each(histories)( - `waits for later application after handler $operation rejection:$reject`, - async ({ operation, reject }) => { - oracleRoute(`case`) - reach.beginHistory() - const initial: Array = [ - { id: `peer`, category: `peer`, name: `Peer` }, - ...(operation === `insert` - ? [] - : [{ id: `a`, category: `edited`, name: `Before` }]), - ] - const authoritative: Array = [ - { id: `peer`, category: `peer`, name: `New peer` }, - { id: `a`, category: `edited`, name: `New source row` }, - ] - const encode = (values: ReadonlyArray) => - JSON.stringify( - [...values] - .map(({ id, category, name }) => ({ id, category, name })) - .sort((left, right) => left.id.localeCompare(right.id)), - ) - let server = structuredClone(initial) - const handlerFailure = new Error(`handler rejected`) - const handler = async () => { - await Promise.resolve() - if (reject) throw handlerFailure - } - const storage = createOwnershipStorage() - const entered = createDeferred() - const release = createDeferred() - let hold = false - const apply = storage.adapter.applyCommittedTx.bind(storage.adapter) - storage.adapter.applyCommittedTx = async (...args) => { - if (hold && args[1].mutations.length > 0) { - hold = false - entered.resolve() - await release.promise - } - return apply(...args) - } - const queryClient = createQueryClient() - const queryFn = vi.fn(() => Promise.resolve(structuredClone(server))) - let releaseCollection: (() => Promise) | undefined - let external: Promise | undefined - await withOracleCleanup(async () => { - const collection = createCollection( - persistedCollectionOptions< - Item, - string | number, - never, - QueryCollectionUtils - >({ - ...queryCollectionOptions({ - id: `post-handler-${operation}-${reject}`, - queryKey: [`post-handler`, operation, reject], - queryClient, - queryFn, - getKey: (row) => row.id, - onInsert: handler, - onUpdate: handler, - onDelete: handler, - }), - persistence: { adapter: storage.adapter }, - }), - ) - releaseCollection = () => collection.cleanup() - await atMutationCheckpoint( - collection.preload(), - `post-handler initial preload`, - ) - const transaction = - operation === `insert` - ? collection.insert({ - id: `a`, - category: `edited`, - name: `Optimistic`, - }) - : operation === `delete` - ? collection.delete(`a`) - : collection.update(`a`, (draft) => { - draft.name = `Optimistic` - }) - const firstError = await transaction.when(`settled`).then( - () => undefined, - (error: unknown) => error, - ) - expect(firstError).toBe(reject ? handlerFailure : undefined) - expect(transaction.state).toBe(reject ? `failed` : `completed`) - expect(encode(collection.toArray)).toBe(encode(initial)) - hold = true - server = structuredClone(authoritative) - let outcome: RefetchApplicationOutcome = `pending` - let resultOrder: Array | undefined - let waiterRows: string | undefined - const observe = () => ({ - rows: encode(collection.toArray), - waiterRows, - refetch: { - refetch: outcome, - ...(outcome === `resolved` ? { resultOrder } : {}), - }, - }) - let model = createRefetchCallSettlementModel([`query`], true) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `query`, - applicationId: `later-result`, - }) - external = collection.utils.refetch({ throwOnError: true }).then( - (results) => { - outcome = `resolved` - resultOrder = results.map(() => `query`) - waiterRows = encode(collection.toArray) - }, - (error: unknown) => { - outcome = `rejected` - throw error - }, - ) - void external.catch(() => {}) - await atMutationCheckpoint( - entered.promise, - `post-handler durable application held`, - ) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(2) - expect(queryClient.isFetching()).toBe(0) - }) - // Query's flags clear before the utility's queued Promise chain - // finishes. Keep the receipt held through a task boundary so every - // already-queued microtask can process that completed fetch. The - // leaked-count transform is the calibration for this timing premise. - await new Promise((resolve) => setTimeout(resolve, 0)) - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId: `query`, - outcome: `fulfilled`, - }) - checkPostHandlerRefetch( - model, - observe(), - encode(initial), - encode(authoritative), - ) - reach.reach(`${operation}/${reject}/application-held`) - release.resolve() - await atMutationCheckpoint( - external, - `post-handler external refetch settlement`, - ) - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `later-result`, - outcome: `fulfilled`, - }) - checkPostHandlerRefetch( - model, - observe(), - encode(initial), - encode(authoritative), - ) - reach.reach(`${operation}/${reject}/applied-waiter`) - }, [ - () => release.resolve(), - () => - external && - atMutationCheckpoint(external, `post-handler cleanup refetch`), - () => releaseCollection?.(), - () => queryClient.clear(), - ]) - }, - ) - }) - - it(`keeps an accepted empty-diff result pending until application`, () => { - oracleRoute(`case`) - const initial: ExplicitRefetchApplicationModel = { - calls: new Map(), - publicValues: new Map([[`query`, `same`]]), - } - const started = reduceExplicitRefetchApplication(initial, { - type: `start-call`, - callId: 1, - resultIds: [`query`], - throwOnError: true, - }) - const accepted = reduceExplicitRefetchApplication(started, { - type: `accept-result`, - callId: 1, - resultId: `query`, - value: `same`, - }) - - expectExplicitRefetchObservation( - observeExplicitRefetchApplication(accepted, 1), - { - publicValues: { query: `same` }, - refetch: `pending`, - }, - ) - const fulfilled = reduceExplicitRefetchApplication(accepted, { - type: `fulfill-application`, - callId: 1, - resultId: `query`, - }) - expectExplicitRefetchObservation( - observeExplicitRefetchApplication(fulfilled, 1), - { - publicValues: { query: `same` }, - refetch: `resolved`, - resultOrder: [`query`], - }, - ) - }) - - it(`retains separate outcomes for a retired caller and its successor`, () => { - oracleRoute(`case`) - let model: ExplicitRefetchApplicationModel = { - calls: new Map(), - publicValues: new Map([[`query`, `initial`]]), - } - model = reduceExplicitRefetchApplication(model, { - type: `start-call`, - callId: 1, - resultIds: [`query`], - throwOnError: true, - }) - model = reduceExplicitRefetchApplication(model, { - type: `accept-result`, - callId: 1, - resultId: `query`, - value: `first`, - }) - model = reduceExplicitRefetchApplication(model, { - type: `start-call`, - callId: 2, - resultIds: [`query`], - throwOnError: true, - }) - model = reduceExplicitRefetchApplication(model, { - type: `accept-result`, - callId: 2, - resultId: `query`, - value: `second`, - }) - model = reduceExplicitRefetchApplication(model, { - type: `retire-application`, - callId: 1, - resultId: `query`, - }) - - expectExplicitRefetchObservation( - observeExplicitRefetchApplication(model, 1), - { - publicValues: { query: `initial` }, - refetch: `rejected`, - }, - ) - expectExplicitRefetchObservation( - observeExplicitRefetchApplication(model, 2), - { - publicValues: { query: `initial` }, - refetch: `pending`, - }, - ) - - model = reduceExplicitRefetchApplication(model, { - type: `fulfill-application`, - callId: 2, - resultId: `query`, - }) - expectExplicitRefetchObservation( - observeExplicitRefetchApplication(model, 2), - { - publicValues: { query: `second` }, - refetch: `resolved`, - resultOrder: [`query`], - }, - ) - }) - - it(`rejects a fetch-boundary-only wrong-result control`, () => { - oracleRoute(`calibration`) - let model: ExplicitRefetchApplicationModel = { - calls: new Map(), - publicValues: new Map([[`query`, `Optimistic`]]), - } - model = reduceExplicitRefetchApplication(model, { - type: `start-call`, - callId: 1, - resultIds: [`query`], - throwOnError: true, - }) - model = reduceExplicitRefetchApplication(model, { - type: `accept-result`, - callId: 1, - resultId: `query`, - value: `Authoritative`, - }) - const expected = observeExplicitRefetchApplication(model, 1) - const fetchBoundaryOnly: ExplicitRefetchObservation = { - publicValues: { query: `Optimistic` }, - refetch: `pending`, - resultOrder: [`query`], - } - fetchBoundaryOnly.refetch = `resolved` - - expect(() => - expectExplicitRefetchObservation(fetchBoundaryOnly, expected), - ).toThrow() - expect(() => - expectExplicitRefetchObservation(expected, expected), - ).not.toThrow() - }) - - it(`rejects illegal explicit-refetch model histories`, () => { - oracleRoute(`calibration`) - const started = reduceExplicitRefetchApplication( - { calls: new Map(), publicValues: new Map() }, - { - type: `start-call`, - callId: 1, - resultIds: [`query`], - throwOnError: true, - }, - ) - const accepted = reduceExplicitRefetchApplication(started, { - type: `accept-result`, - callId: 1, - resultId: `query`, - value: `accepted`, - }) - - expect(() => - reduceExplicitRefetchApplication(started, { - type: `start-call`, - callId: 1, - resultIds: [`replacement`], - throwOnError: true, - }), - ).toThrow(`already exists`) - expect(() => - reduceExplicitRefetchApplication(accepted, { - type: `accept-result`, - callId: 1, - resultId: `query`, - value: `replacement`, - }), - ).toThrow(`already accepted`) - expect(() => - reduceExplicitRefetchApplication(accepted, { - type: `fulfill-application`, - callId: 1, - resultId: `unknown`, - }), - ).toThrow(`Unknown result`) - const fulfilled = reduceExplicitRefetchApplication(accepted, { - type: `fulfill-application`, - callId: 1, - resultId: `query`, - }) - expect(() => - reduceExplicitRefetchApplication(fulfilled, { - type: `fulfill-application`, - callId: 1, - resultId: `query`, - }), - ).toThrow(`already settled`) - }) - - it(`keeps a suppressed fetch failure pending for its accepted application`, () => { - oracleRoute(`case`) - let model = createRefetchCallSettlementModel([`query`], false) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `query`, - applicationId: `cached-result`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId: `query`, - outcome: `rejected`, - }) - expect(observeRefetchCallSettlement(model)).toEqual({ - refetch: `pending`, - }) - - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `cached-result`, - outcome: `fulfilled`, - }) - expect(observeRefetchCallSettlement(model)).toEqual({ - refetch: `resolved`, - resultOrder: [`query`], - }) - }) - - it(`excludes unrelated background applications from a refetch call`, () => { - oracleRoute(`case`) - let model = createRefetchCallSettlementModel([`query`], true) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `query`, - applicationId: `own-result`, - }) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `query`, - applicationId: `background-result`, - source: `background`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `own-result`, - outcome: `fulfilled`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId: `query`, - outcome: `fulfilled`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `background-result`, - outcome: `rejected`, - }) - - expect(observeRefetchCallSettlement(model)).toEqual({ - refetch: `resolved`, - resultOrder: [`query`], - }) - }) - - it(`preserves the result slot of a Query removed during refetch startup`, () => { - oracleRoute(`case`) - let model = createRefetchCallSettlementModel([`first`, `second`], true) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `first`, - applicationId: `first-result`, - }) - model = reduceRefetchCallSettlement(model, { - type: `remove-query`, - queryId: `second`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId: `first`, - outcome: `fulfilled`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `first-result`, - outcome: `fulfilled`, - }) - - expect(observeRefetchCallSettlement(model)).toEqual({ - refetch: `resolved`, - resultOrder: [`first`, undefined], - }) - }) - - it(`rejects fail-fast while an independent application remains pending`, () => { - oracleRoute(`case`) - let model = createRefetchCallSettlementModel([`detail`, `list`], true) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `detail`, - applicationId: `detail-result`, - }) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `list`, - applicationId: `list-result`, - }) - for (const queryId of [`detail`, `list`]) { - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId, - outcome: `fulfilled`, - }) - } - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `detail-result`, - outcome: `rejected`, - }) - - expect(observeRefetchCallSettlement(model)).toEqual({ - refetch: `rejected`, - }) - expect(model.applications.get(`list-result`)?.outcome).toBe(`pending`) - }) - - it(`rejects a handler call that resolves before its Query fetch`, () => { - oracleRoute(`calibration`) - const model: HandlerRefetchBoundaryModel = { - fetch: `pending`, - application: `unaccepted`, - throwOnError: true, - } - const expected = observeHandlerRefetchBoundary(model) - const returnsAfterInvocation = { - ...expected, - refetch: `resolved` as const, - } - - expect(() => expect(returnsAfterInvocation).toEqual(expected)).toThrow() - expect(expected).toEqual({ - refetch: `pending`, - applicationErrorRecorded: false, - }) - }) - - it(`preserves every direct handler identity legacy replay coordinate`, () => { - oracleRoute(`case`) - const historyKeys = handlerCollectionHistoryGrammar.map( - ({ handlerKind, accessPath, legacyOperation }) => - `${handlerKind}:${accessPath}:${legacyOperation}`, - ) - expect(historyKeys).toEqual([ - `insert:parameter:refetch`, - `insert:parameter:clearError`, - `insert:mutation-alias:refetch`, - `insert:mutation-alias:clearError`, - `update:parameter:refetch`, - `update:parameter:clearError`, - `update:mutation-alias:refetch`, - `update:mutation-alias:clearError`, - `delete:parameter:refetch`, - `delete:parameter:clearError`, - `delete:mutation-alias:refetch`, - `delete:mutation-alias:clearError`, - ]) - expect(new Set(historyKeys).size).toBe(historyKeys.length) - // Six semantic histories each keep their two historical replay runs. - expect( - new Set( - handlerCollectionHistoryGrammar.map( - ({ handlerKind, accessPath }) => `${handlerKind}:${accessPath}`, - ), - ).size, - ).toBe(6) - }) - - it(`rejects handler identity wrong-result controls`, () => { - oracleRoute(`calibration`) - const history = handlerCollectionHistoryGrammar[0]! - const expected = observeHandlerIdentity(history, 2) - const mutants: Array = [ - { ...expected, transactionIsExternal: false }, - { ...expected, allMutationsPreserveIdentity: false }, - { ...expected, parameterIsExternal: false }, - { ...expected, allAliasesMatchParameter: false }, - { ...expected, anyAliasMatchesExternal: false }, - { ...expected, externalMapMatchesAnyAlias: false }, - { ...expected, parameterMapMatchesAllAliases: false }, - { ...expected, externalWeakMapMatchesAnyAlias: false }, - { ...expected, parameterWeakMapMatchesAllAliases: false }, - ] - - for (const mutant of mutants) { - expect(() => expectHandlerIdentityObservation(mutant, expected)).toThrow() - } - expect(() => - expectHandlerIdentityObservation(expected, expected), - ).not.toThrow() - expect(() => observeHandlerIdentity(history, 0)).toThrow( - `requires a matching mutation alias`, - ) - }) - - it(`resolves a refetch with no tracked Query results`, async () => { - oracleRoute(`case`) - const { collection, queryFn } = createOwnershipFixture({ - id: `explicit-refetch-empty-aggregate`, - results: [], - syncMode: `on-demand`, - }) - const model = reduceExplicitRefetchApplication( - { calls: new Map(), publicValues: new Map() }, - { - type: `start-call`, - callId: 1, - resultIds: [], - throwOnError: true, - }, - ) - - const results = await collection.utils.refetch({ throwOnError: true }) - - expect(queryFn).not.toHaveBeenCalled() - expectExplicitRefetchObservation( - { publicValues: {}, refetch: `resolved`, resultOrder: [] }, - observeExplicitRefetchApplication(model, 1), - ) - expect(results).toEqual([]) - }) - - it(`applies a successful on-demand refetch without Query listeners`, async () => { - oracleRoute(`case`) - const before = { ...shared, name: `Before` } - const after = { ...shared, name: `After` } - const { collection } = createOwnershipFixture({ - id: `explicit-refetch-unsubscribed-on-demand`, - results: [[before], [after]], - syncMode: `on-demand`, - }) - await collection._sync.loadSubset({}) - const subscription = collection.subscribeChanges(() => {}) - subscription.unsubscribe() - - let model: ExplicitRefetchApplicationModel = { - calls: new Map(), - publicValues: new Map([[`query`, before.name]]), - } - model = reduceExplicitRefetchApplication(model, { - type: `start-call`, - callId: 1, - resultIds: [`query`], - throwOnError: true, - }) - - const results = await collection.utils.refetch({ throwOnError: true }) - model = reduceExplicitRefetchApplication(model, { - type: `accept-result`, - callId: 1, - resultId: `query`, - value: after.name, - }) - model = reduceExplicitRefetchApplication(model, { - type: `fulfill-application`, - callId: 1, - resultId: `query`, - }) - - expectExplicitRefetchObservation( - { - publicValues: { query: collection.get(shared.id)?.name }, - refetch: `resolved`, - resultOrder: [`query`], - }, - observeExplicitRefetchApplication(model, 1), - ) - expect((results[0]?.data as Array)[0]?.name).toBe(after.name) - }) - - it(`skips a snapshotted Query removed while an earlier refetch starts`, async () => { - oracleRoute(`case`) - const queryClient = createQueryClient() - const first = { limit: 1 } - const second = { limit: 2 } - let unloadSecond = () => {} - let call = 0 - const queryFn = vi.fn(() => { - call++ - if (call === 3) unloadSecond() - return Promise.resolve([ - { id: String(call), category: `query`, name: `Call ${call}` }, - ]) - }) - const collection = createCollection( - queryCollectionOptions({ - id: `refetch-removes-later-query`, - queryClient, - queryKey: [`refetch-removes-later-query`], - queryFn, - getKey: (item) => item.id, - syncMode: `on-demand`, - startSync: true, - }), - ) - unloadSecond = () => collection._sync.unloadSubset(second) - cleanups.push(async () => { - await collection.cleanup() - queryClient.clear() - }) - await collection._sync.loadSubset(first) - await collection._sync.loadSubset(second) - - let model = createRefetchCallSettlementModel([`first`, `second`], true) - const results = await collection.utils.refetch({ throwOnError: true }) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `first`, - applicationId: `first-result`, - }) - model = reduceRefetchCallSettlement(model, { - type: `remove-query`, - queryId: `second`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId: `first`, - outcome: `fulfilled`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `first-result`, - outcome: `fulfilled`, - }) - - expect({ - refetch: `resolved`, - resultOrder: results.map((result, index) => - result === undefined ? undefined : index === 0 ? `first` : `second`, - ), - }).toEqual(observeRefetchCallSettlement(model)) - }) - - it(`settles a suppressed fetch failure at the fetch boundary while a mutation persists`, async () => { - oracleRoute(`case`) - const initial = { ...shared, name: `Initial` } - const fetchError = new Error(`Explicit refetch fetch failed`) - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockRejectedValueOnce(fetchError) - const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) - const collection = createCollection( - queryCollectionOptions({ - id: `explicit-refetch-suppressed-fetch-failure`, - queryClient, - queryKey: [`explicit-refetch-suppressed-fetch-failure`], - queryFn, - getKey: (item) => item.id, - startSync: true, - }), - ) - const persistence = createDeferred() - const transaction = createTransaction({ - mutationFn: () => persistence.promise, - }) - cleanups.push(async () => { - persistence.resolve() - await transaction.when(`settled`).catch(() => undefined) - consoleError.mockRestore() - await collection.cleanup() - queryClient.clear() - }) - await collection.stateWhenReady() - transaction.mutate(() => { - collection.update(shared.id, (draft) => { - draft.name = `Optimistic` - }) - }) - - let model: MutationRefreshLivenessModel = { - fetch: `pending`, - application: `unaccepted`, - boundary: `fetch`, - throwOnError: false, - } - let actualOutcome: RefetchApplicationOutcome = `pending` - const refetch = collection.utils.refetch({ throwOnError: false }).then( - (result) => { - actualOutcome = `resolved` - return result - }, - (error: unknown) => { - actualOutcome = `rejected` - throw error - }, - ) - void refetch.catch(() => undefined) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(2) - expect(collection.utils.isFetching).toBe(false) - }) - model = reduceMutationRefreshLiveness(model, { - type: `accept-application`, - }) - model = reduceMutationRefreshLiveness(model, { - type: `settle-fetch`, - outcome: `rejected`, - }) - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - expect({ refresh: actualOutcome, application: `pending` }).toEqual( - observeMutationRefreshLiveness(model), - ) - expect(collection.get(shared.id)?.name).toBe(`Optimistic`) - - persistence.resolve() - await transaction.when(`settled`) - const results = await refetch - model = reduceMutationRefreshLiveness(model, { - type: `settle-application`, - outcome: `fulfilled`, - }) - expect({ refresh: actualOutcome, application: `resolved` }).toEqual( - observeMutationRefreshLiveness(model), - ) - expect(results).toHaveLength(1) - expect(results[0]?.isError).toBe(true) - }) - - it(`waits for every Query result and preserves tracked-key order`, async () => { - oracleRoute(`case`) - const firstRefresh = createDeferred>() - const secondRefresh = createDeferred>() - const refreshedDetail = { ...detailOnly, name: `Detail refreshed` } - const refreshedList = { ...listOnly, name: `List refreshed` } - const { collection, queryFn } = createOwnershipFixture({ - id: `explicit-refetch-aggregate-order`, - results: [ - [detailOnly], - [listOnly], - firstRefresh.promise, - secondRefresh.promise, - ], - }) - cleanups.push(() => { - firstRefresh.resolve([refreshedDetail]) - secondRefresh.resolve([refreshedList]) - return Promise.resolve() - }) - const detail = { where: eq(new IR.PropRef([`category`]), `detail`) } - const list = { where: eq(new IR.PropRef([`category`]), `list`) } - await collection._sync.loadSubset(detail) - await collection._sync.loadSubset(list) - - let model: ExplicitRefetchApplicationModel = { - calls: new Map(), - publicValues: new Map([ - [`detail`, detailOnly.name], - [`list`, listOnly.name], - ]), - } - model = reduceExplicitRefetchApplication(model, { - type: `start-call`, - callId: 1, - resultIds: [`detail`, `list`], - throwOnError: true, - }) - let refetchOutcome: RefetchApplicationOutcome = `pending` - const refetch = collection.utils.refetch({ throwOnError: true }).then( - (results) => { - refetchOutcome = `resolved` - return results - }, - (error: unknown) => { - refetchOutcome = `rejected` - throw error - }, - ) - void refetch.catch(() => {}) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(4)) - - secondRefresh.resolve([refreshedList]) - await vi.waitFor(() => - expect(collection.get(listOnly.id)?.name).toBe(refreshedList.name), - ) - model = reduceExplicitRefetchApplication(model, { - type: `accept-result`, - callId: 1, - resultId: `list`, - value: refreshedList.name, - }) - model = reduceExplicitRefetchApplication(model, { - type: `fulfill-application`, - callId: 1, - resultId: `list`, - }) - expectExplicitRefetchObservation( - { - publicValues: { - detail: collection.get(detailOnly.id)?.name, - list: collection.get(listOnly.id)?.name, - }, - refetch: refetchOutcome, - }, - observeExplicitRefetchApplication(model, 1), - ) - - firstRefresh.resolve([refreshedDetail]) - const results = await refetch - model = reduceExplicitRefetchApplication(model, { - type: `accept-result`, - callId: 1, - resultId: `detail`, - value: refreshedDetail.name, - }) - model = reduceExplicitRefetchApplication(model, { - type: `fulfill-application`, - callId: 1, - resultId: `detail`, - }) - expectExplicitRefetchObservation( - { - publicValues: { - detail: collection.get(detailOnly.id)?.name, - list: collection.get(listOnly.id)?.name, - }, - refetch: refetchOutcome, - resultOrder: results.map((result) => { - if (!result || !Array.isArray(result.data) || !result.data[0]) { - throw new Error(`Expected one applied row per Query result`) - } - return (result.data[0] as Item).category - }), - }, - observeExplicitRefetchApplication(model, 1), - ) - }) - - it(`does not adopt a background application from the same Query`, async () => { - oracleRoute(`case`) - const id = `explicit-refetch-excludes-background-application` - const queryKey = [id] as const - const initial = { ...shared, name: `Initial` } - const ownResult = { ...shared, name: `Own result` } - const background = { ...shared, name: `Background` } - const successor = { ...shared, name: `Successor` } - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockResolvedValueOnce([ownResult]) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey, - queryFn, - getKey: (item) => item.id, - startSync: true, - }), - ) - const persistence = createDeferred() - let transaction: ReturnType | undefined - let armed = false - let backgroundInjected = false - let nameBeforeBackground: string | undefined - const unsubscribeCache = queryClient.getQueryCache().subscribe((event) => { - if ( - armed && - event.type === `updated` && - event.action.type === `success` && - !event.action.manual - ) { - armed = false - nameBeforeBackground = collection.get(shared.id)?.name - transaction = createTransaction({ - mutationFn: () => persistence.promise, - }) - transaction.mutate(() => { - collection.update(shared.id, (draft) => { - draft.name = `Optimistic` - }) - }) - queryClient.setQueryData(queryKey, [background]) - backgroundInjected = true - } - }) - cleanups.push(async () => { - unsubscribeCache() - persistence.resolve() - await transaction?.when(`settled`).catch(() => undefined) - await collection.cleanup() - queryClient.clear() - }) - await collection.stateWhenReady() - - let model = createRefetchCallSettlementModel([`query`], true) - armed = true - let actualOutcome: RefetchApplicationOutcome = `pending` - const refetch = collection.utils.refetch({ throwOnError: true }).then( - (results) => { - actualOutcome = `resolved` - return results - }, - (error: unknown) => { - actualOutcome = `rejected` - throw error - }, - ) - void refetch.catch(() => undefined) - await vi.waitFor(() => expect(backgroundInjected).toBe(true)) - expect(nameBeforeBackground).toBe(ownResult.name) - - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `query`, - applicationId: `own-result`, - }) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `query`, - applicationId: `background-result`, - source: `background`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `own-result`, - outcome: `fulfilled`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId: `query`, - outcome: `fulfilled`, - }) - - queryClient.setQueryData(queryKey, [successor]) - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `background-result`, - outcome: `rejected`, - }) - persistence.resolve() - await transaction?.when(`settled`) - const results = await refetch - - expect(actualOutcome).toBe(observeRefetchCallSettlement(model).refetch) - expect(results).toHaveLength(1) - expect(collection.get(shared.id)?.name).toBe(successor.name) - }) - - it(`rejects before an independent accepted application settles`, async () => { - oracleRoute(`case`) - const id = `explicit-refetch-fail-fast` - const detail = { where: eq(new IR.PropRef([`category`]), `detail`) } - const list = { where: eq(new IR.PropRef([`category`]), `list`) } - const listKey = [id, getLoadSubsetDemandKey(list)] - const invalidDetail = { ...detailOnly, name: `Invalid detail` } - const refreshedList = { ...listOnly, name: `Refreshed list` } - const applicationError = new Error(`Detail application failed`) - const pendingListScan = - createDeferred< - Array<{ key: string | number; value: Item; metadata?: unknown }> - >() - const scanPersistedRows = vi.fn(() => pendingListScan.promise) - const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) - const { collection } = createOwnershipFixture({ - id, - results: [[detailOnly], [listOnly], [invalidDetail], [refreshedList]], - getKey: (item) => { - if (item.name === invalidDetail.name) throw applicationError - return item.id - }, - scanPersistedRows, - setupMetadata: (metadata) => { - const queryHash = hashKey(listKey) - metadata.collection.set(`queryCollection:gc:${queryHash}`, { - queryHash, - mode: `until-revalidated`, - }) - }, - }) - cleanups.push(() => { - pendingListScan.resolve([]) - consoleError.mockRestore() - return Promise.resolve() - }) - await collection._sync.loadSubset(detail) - const initialListLoad = Promise.resolve(collection._sync.loadSubset(list)) - void initialListLoad.catch(() => undefined) - await vi.waitFor(() => expect(scanPersistedRows).toHaveBeenCalledOnce()) - - let model = createRefetchCallSettlementModel([`detail`, `list`], true) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `detail`, - applicationId: `detail-result`, - }) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `list`, - applicationId: `list-result`, - }) - for (const queryId of [`detail`, `list`]) { - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId, - outcome: `fulfilled`, - }) - } - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `detail-result`, - outcome: `rejected`, - }) - - const refetch = collection.utils.refetch({ throwOnError: true }) - void refetch.catch(() => undefined) - await vi.waitFor(() => expect(scanPersistedRows).toHaveBeenCalledTimes(2)) - await expect(refetch).rejects.toBe(applicationError) - expect(observeRefetchCallSettlement(model)).toEqual({ - refetch: `rejected`, - }) - expect(model.applications.get(`list-result`)?.outcome).toBe(`pending`) - - pendingListScan.resolve([]) - await initialListLoad.catch(() => undefined) - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `list-result`, - outcome: `fulfilled`, - }) - await vi.waitFor(() => { - expect(collection.get(listOnly.id)?.name).toBe(refreshedList.name) - }) - expect(observeRefetchCallSettlement(model)).toEqual({ - refetch: `rejected`, - }) - }) - - it.each([ - { - caseName: `changed`, - id: `explicit-refetch-changed-application`, - initialName: `Initial`, - authoritativeName: `Authoritative`, - }, - { - caseName: `empty-diff`, - id: `explicit-refetch-empty-diff-application`, - initialName: `Same`, - authoritativeName: `Same`, - }, - ])( - `settles a $caseName accepted result at fetch and applies it after mutation`, - async ({ id, initialName, authoritativeName }) => { - oracleRoute(`case`) - const initial = { ...shared, name: initialName } - const authoritative = { ...shared, name: authoritativeName } - const { collection, queryClient, queryFn } = createOwnershipFixture({ - id, - results: [[initial], [authoritative]], - syncMode: `eager`, - }) - await collection.stateWhenReady() - - const persistence = createDeferred() - const transaction = createTransaction({ - mutationFn: () => persistence.promise, - }) - transaction.mutate(() => { - collection.update(shared.id, (draft) => { - draft.name = `Optimistic` - }) - }) - cleanups.push(async () => { - persistence.resolve() - await transaction.when(`settled`) - }) - - let model: MutationRefreshLivenessModel = { - fetch: `pending`, - application: `unaccepted`, - boundary: `fetch`, - throwOnError: true, - } - let refetchOutcome: RefetchApplicationOutcome = `pending` - const refetch = collection.utils.refetch({ throwOnError: true }).then( - (result) => { - refetchOutcome = `resolved` - return result - }, - (error: unknown) => { - refetchOutcome = `rejected` - throw error - }, - ) - void refetch.catch(() => {}) - - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(2) - expect( - queryClient.getQueryCache().find({ queryKey: [id], exact: true }) - ?.state.dataUpdateCount, - ).toBe(2) - expect(collection.utils.isFetching).toBe(false) - }) - model = reduceMutationRefreshLiveness(model, { - type: `accept-application`, - }) - model = reduceMutationRefreshLiveness(model, { - type: `settle-fetch`, - outcome: `fulfilled`, - }) - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - - expect({ - refresh: refetchOutcome, - application: - collection.get(shared.id)?.name === authoritative.name - ? `resolved` - : `pending`, - }).toEqual(observeMutationRefreshLiveness(model)) - - persistence.resolve() - await transaction.when(`settled`) - await refetch - model = reduceMutationRefreshLiveness(model, { - type: `settle-application`, - outcome: `fulfilled`, - }) - expect({ refresh: refetchOutcome, application: `resolved` }).toEqual( - observeMutationRefreshLiveness(model), - ) - expect(collection.get(shared.id)?.name).toBe(authoritative.name) - }, - ) - - it(`settles an eager refetch at fetch while another collection refetches`, async () => { - oracleRoute(`case`) - const queryClient = createQueryClient() - const concurrentResult = createDeferred>() - const explicitResult = createDeferred>() - const persistence = createDeferred() - const initial = { ...shared, name: `Initial` } - const authoritative = { ...shared, name: `Authoritative` } - let call = 0 - const queryFn = vi.fn(() => { - call++ - if (call === 1) return Promise.resolve([initial]) - if (call === 2) return concurrentResult.promise - return explicitResult.promise - }) - const eager = createCollection( - queryCollectionOptions({ - id: `authority-gated-eager-refetch`, - queryClient, - queryKey: [`shared-refetch-authority`], - queryFn, - getKey: (item) => item.id, - syncMode: `eager`, - startSync: true, - }), - ) - const onDemand = createCollection( - queryCollectionOptions({ - id: `authority-gated-on-demand-writer`, - queryClient, - queryKey: [`shared-refetch-authority`], - queryFn, - getKey: (item) => item.id, - syncMode: `on-demand`, - startSync: true, - }), - ) - const transaction = createTransaction({ - mutationFn: () => persistence.promise, - }) - let transactionStarted = false - cleanups.push(async () => { - concurrentResult.resolve([authoritative]) - explicitResult.resolve([authoritative]) - persistence.resolve() - if (transactionStarted) { - await transaction.when(`settled`).catch(() => undefined) - } - await Promise.allSettled([eager.cleanup(), onDemand.cleanup()]) - queryClient.clear() - }) - await eager.stateWhenReady() - await onDemand._sync.loadSubset({}) - - transactionStarted = true - transaction.mutate(() => { - eager.update(shared.id, (draft) => { - draft.name = `Optimistic` - }) - }) - onDemand.utils.writeUpdate({ ...shared, name: `Manual` }) - void onDemand.utils.refetch({ throwOnError: true }).catch(() => {}) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - - let model: MutationRefreshLivenessModel = { - fetch: `pending`, - application: `unaccepted`, - boundary: `fetch`, - throwOnError: true, - } - let refetchOutcome: RefetchApplicationOutcome = `pending` - const refetch = eager.utils.refetch({ throwOnError: true }).then( - (results) => { - refetchOutcome = `resolved` - return results - }, - (error: unknown) => { - refetchOutcome = `rejected` - throw error - }, - ) - void refetch.catch(() => undefined) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(3)) - - explicitResult.resolve([authoritative]) - await vi.waitFor(() => expect(eager.utils.isFetching).toBe(false)) - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - model = reduceMutationRefreshLiveness(model, { - type: `accept-application`, - }) - model = reduceMutationRefreshLiveness(model, { - type: `settle-fetch`, - outcome: `fulfilled`, - }) - expect({ - refresh: refetchOutcome, - application: - eager.get(shared.id)?.name === authoritative.name - ? `resolved` - : `pending`, - }).toEqual(observeMutationRefreshLiveness(model)) - - persistence.resolve() - await transaction.when(`settled`) - const results = await refetch - model = reduceMutationRefreshLiveness(model, { - type: `settle-application`, - outcome: `fulfilled`, - }) - expect({ refresh: refetchOutcome, application: `resolved` }).toEqual( - observeMutationRefreshLiveness(model), - ) - expect(results.map(() => `query`)).toEqual([`query`]) - expect(eager.get(shared.id)?.name).toBe(authoritative.name) - }) - - it.each( - mutationRefreshHistoryGrammar.filter( - ({ invocation }) => invocation === `external-handler-overlap`, - ), - )( - `settles an external refetch at fetch with $schedule overlap (legacy replay: $legacyOperation)`, - async ({ legacyOperation, schedule }) => { - oracleRoute(`case`) - const initial = { ...shared, name: `Initial` } - const authoritative = { ...shared, name: `Authoritative` } - const refreshResult = createDeferred>() - const handlerStarted = createDeferred() - const releaseHandler = createDeferred() - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockReturnValueOnce(refreshResult.promise) - const collection = createCollection( - queryCollectionOptions({ - id: `external-${legacyOperation}-${schedule}-overlaps-handler`, - queryClient, - queryKey: [ - `external-${legacyOperation}-${schedule}-overlaps-handler`, - ], - queryFn, - getKey: (item) => item.id, - startSync: true, - onUpdate: async () => { - handlerStarted.resolve() - await releaseHandler.promise - }, - }), - ) - cleanups.push(async () => { - refreshResult.resolve([authoritative]) - releaseHandler.resolve() - await collection.cleanup() - queryClient.clear() - }) - await collection.stateWhenReady() - - let model: MutationRefreshLivenessModel = { - fetch: `pending`, - application: `unaccepted`, - boundary: `fetch`, - throwOnError: true, - } - let refetchOutcome: RefetchApplicationOutcome = `pending` - const startRefresh = () => { - const refresh = collection.utils.refetch({ throwOnError: true }).then( - (result) => { - refetchOutcome = `resolved` - return result - }, - (error: unknown) => { - refetchOutcome = `rejected` - throw error - }, - ) - void refresh.catch(() => {}) - return refresh - } - const startMutation = () => - collection.update(shared.id, (draft) => { - draft.name = `Optimistic` - }) - - let mutation: ReturnType - let refresh: ReturnType - if (schedule === `mutation-first`) { - mutation = startMutation() - await handlerStarted.promise - refresh = startRefresh() - } else { - refresh = startRefresh() - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - mutation = startMutation() - await handlerStarted.promise - } - refreshResult.resolve([authoritative]) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(2) - expect(collection.utils.isFetching).toBe(false) - }) - model = reduceMutationRefreshLiveness(model, { - type: `accept-application`, - }) - model = reduceMutationRefreshLiveness(model, { - type: `settle-fetch`, - outcome: `fulfilled`, - }) - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - - expect({ - refresh: refetchOutcome, - application: - collection.get(shared.id)?.name === authoritative.name - ? `resolved` - : `pending`, - }).toEqual(observeMutationRefreshLiveness(model)) - - releaseHandler.resolve() - await mutation.when(`settled`) - await refresh - model = reduceMutationRefreshLiveness(model, { - type: `settle-application`, - outcome: `fulfilled`, - }) - expect({ refresh: refetchOutcome, application: `resolved` }).toEqual( - observeMutationRefreshLiveness(model), - ) - expect(collection.get(shared.id)?.name).toBe(authoritative.name) - }, - ) - - it.each( - mutationRefreshHistoryGrammar.filter( - ({ invocation }) => - invocation === `external-persisted-application-overlap`, - ), - )( - `settles an external refetch when mutation starts during persisted application preparation (legacy replay: $legacyOperation)`, - async ({ legacyOperation }) => { - oracleRoute(`case`) - const id = `external-${legacyOperation}-persisted-application-overlap` - const queryHash = hashKey([id]) - const initial = { ...shared, name: `Initial` } - const authoritative = { ...shared, name: `Authoritative` } - const optimistic = { - id: `optimistic`, - category: `mutation`, - name: `Optimistic`, - } - const initialScan = - createDeferred< - Array<{ key: string | number; value: Item; metadata?: unknown }> - >() - const applicationScan = - createDeferred< - Array<{ key: string | number; value: Item; metadata?: unknown }> - >() - const refreshResult = createDeferred>() - const persistence = createDeferred() - const scanPersistedRows = vi - .fn() - .mockReturnValueOnce(initialScan.promise) - .mockReturnValueOnce(applicationScan.promise) - const { collection, queryFn } = createOwnershipFixture({ - id, - syncMode: `eager`, - results: [[initial], refreshResult.promise], - scanPersistedRows, - setupMetadata: (metadata) => - metadata.collection.set(`queryCollection:gc:${queryHash}`, { - queryHash, - mode: `until-revalidated`, - }), - }) - cleanups.push(() => { - initialScan.resolve([]) - applicationScan.resolve([]) - refreshResult.resolve([authoritative]) - persistence.resolve() - return Promise.resolve() - }) - - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledOnce() - expect(scanPersistedRows).toHaveBeenCalledOnce() - }) - - let model: MutationRefreshLivenessModel = { - fetch: `pending`, - application: `unaccepted`, - boundary: `fetch`, - throwOnError: true, - } - let refetchOutcome: RefetchApplicationOutcome = `pending` - const refresh = collection.utils.refetch({ throwOnError: true }).then( - (result) => { - refetchOutcome = `resolved` - return result - }, - (error: unknown) => { - refetchOutcome = `rejected` - throw error - }, - ) - void refresh.catch(() => undefined) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - refreshResult.resolve([authoritative]) - await vi.waitFor(() => { - expect(collection.utils.isFetching).toBe(false) - expect(scanPersistedRows).toHaveBeenCalledTimes(2) - }) - - const transaction = createTransaction({ - mutationFn: () => persistence.promise, - }) - transaction.mutate(() => collection.insert(optimistic)) - void transaction.when(`settled`).catch(() => undefined) - applicationScan.resolve([]) - model = reduceMutationRefreshLiveness(model, { - type: `accept-application`, - }) - model = reduceMutationRefreshLiveness(model, { - type: `settle-fetch`, - outcome: `fulfilled`, - }) - await vi.waitFor(() => - expect(refetchOutcome).toBe( - observeMutationRefreshLiveness(model).refresh, - ), - ) - expect(collection.get(shared.id)?.name).not.toBe(authoritative.name) - - persistence.resolve() - await transaction.when(`settled`) - await refresh - model = reduceMutationRefreshLiveness(model, { - type: `settle-application`, - outcome: `fulfilled`, - }) - await vi.waitFor(() => - expect(collection.get(shared.id)?.name).toBe(authoritative.name), - ) - expect({ refresh: refetchOutcome, application: `resolved` }).toEqual( - observeMutationRefreshLiveness(model), - ) - }, - ) - - it(`rejects a call-entry-only boundary mutant for refresh-first overlap`, () => { - oracleRoute(`calibration`) - const history = mutationRefreshHistoryGrammar.find( - ({ invocation, legacyOperation, schedule }) => - invocation === `external-handler-overlap` && - legacyOperation === `refetch` && - schedule === `refresh-first-during-fetch`, - ) - expect(history).toBeDefined() - let model: MutationRefreshLivenessModel = { - fetch: `pending`, - application: `unaccepted`, - boundary: `fetch`, - throwOnError: true, - } - model = reduceMutationRefreshLiveness(model, { - type: `accept-application`, - }) - model = reduceMutationRefreshLiveness(model, { - type: `settle-fetch`, - outcome: `fulfilled`, - }) - - const expected = observeMutationRefreshLiveness(model) - const callEntryOnlyMutant = observeMutationRefreshLiveness({ - ...model, - boundary: `application`, - }) - - expect(expected).toEqual({ refresh: `resolved`, application: `pending` }) - expect(callEntryOnlyMutant).not.toEqual(expected) - }) - - it(`preserves every mutation refresh legacy replay coordinate`, () => { - oracleRoute(`case`) - expect(mutationRefreshHistoryGrammar).toHaveLength(12) - expect( - new Set( - mutationRefreshHistoryGrammar.map( - ({ invocation, legacyOperation, schedule }) => - `${invocation}:${legacyOperation}:${schedule}`, - ), - ).size, - ).toBe(mutationRefreshHistoryGrammar.length) - expect( - new Set( - mutationRefreshHistoryGrammar.map( - ({ invocation, schedule }) => `${invocation}:${schedule}`, - ), - ).size, - ).toBe(6) - }) - - it.each( - mutationRefreshHistoryGrammar.filter( - ({ invocation }) => - invocation === `manual-transaction` || - invocation === `optimistic-action`, - ), - )( - `settles a $invocation refetch at the fetch boundary (legacy replay: $legacyOperation)`, - async ({ invocation, legacyOperation }) => { - oracleRoute(`case`) - const id = `mutation-refresh-${invocation}-${legacyOperation}` - const initial = { ...shared, name: `Initial` } - const authoritative = { ...shared, name: `Authoritative` } - const refreshResult = createDeferred>() - const releaseMutation = createDeferred() - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockReturnValueOnce(refreshResult.promise) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - startSync: true, - }), - ) - let refreshOutcome: RefetchApplicationOutcome = `pending` - const mutationFn = async (): Promise => { - const refresh = collection.utils.refetch({ throwOnError: true }) - await refresh.then( - () => { - refreshOutcome = `resolved` - }, - (error: unknown) => { - refreshOutcome = `rejected` - throw error - }, - ) - await releaseMutation.promise - } - const transactionReference: { - current?: Transaction> - } = {} - cleanups.push(async () => { - refreshResult.resolve([authoritative]) - if (transactionReference.current) { - // An immediate direct write drains the accepted sync result and - // breaks the deliberately reproduced cycle during RED cleanup. - collection.utils.writeUpdate(authoritative) - releaseMutation.resolve() - await transactionReference.current.when(`settled`) - } - await collection.cleanup() - queryClient.clear() - }) - await collection.stateWhenReady() - - let transaction: Transaction> - if (invocation === `manual-transaction`) { - transaction = createTransaction>({ - mutationFn, - }) - transaction.mutate(() => { - collection.update(shared.id, (draft) => { - draft.name = `Optimistic` - }) - }) - } else { - const action = createOptimisticAction({ - onMutate: () => { - collection.update(shared.id, (draft) => { - draft.name = `Optimistic` - }) - }, - mutationFn: async () => mutationFn(), - }) - transaction = action(`refresh`) - } - transactionReference.current = transaction - void transaction.when(`settled`).catch(() => undefined) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - - let model: MutationRefreshLivenessModel = { - fetch: `pending`, - application: `unaccepted`, - boundary: `fetch`, - throwOnError: true, - } - refreshResult.resolve([authoritative]) - await vi.waitFor(() => expect(collection.utils.isFetching).toBe(false)) - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - model = reduceMutationRefreshLiveness(model, { - type: `accept-application`, - }) - model = reduceMutationRefreshLiveness(model, { - type: `settle-fetch`, - outcome: `fulfilled`, - }) - - expect({ - refresh: refreshOutcome, - application: - collection.get(shared.id)?.name === authoritative.name - ? `resolved` - : `pending`, - }).toEqual(observeMutationRefreshLiveness(model)) - }, - ) - - it.each( - mutationRefreshHistoryGrammar.filter( - ({ invocation }) => invocation === `captured-handler-collection`, - ), - )( - `settles a captured handler Collection refetch at the fetch boundary (legacy replay: $legacyOperation)`, - async ({ legacyOperation }) => { - oracleRoute(`case`) - const id = `captured-handler-refresh-${legacyOperation}` - const initial = { ...shared, name: `Initial` } - const authoritative = { ...shared, name: `Authoritative` } - const refreshResult = createDeferred>() - const releaseHandler = createDeferred() - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockReturnValueOnce(refreshResult.promise) - let refreshOutcome: RefetchApplicationOutcome = `pending` - const collectionReference: { current?: OwnershipFixture[`collection`] } = - {} - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - startSync: true, - onUpdate: async () => { - const capturedCollection = collectionReference.current - if (!capturedCollection) { - throw new Error(`The captured Collection is not available`) - } - const refresh = capturedCollection.utils.refetch({ - throwOnError: true, - }) - await refresh.then( - () => { - refreshOutcome = `resolved` - }, - (error: unknown) => { - refreshOutcome = `rejected` - throw error - }, - ) - await releaseHandler.promise - }, - }), - ) - collectionReference.current = collection - const transactionReference: { - current?: ReturnType - } = {} - cleanups.push(async () => { - refreshResult.resolve([authoritative]) - if (transactionReference.current) { - collection.utils.writeUpdate(authoritative) - releaseHandler.resolve() - await transactionReference.current.when(`settled`) - } - await collection.cleanup() - queryClient.clear() - }) - await collection.stateWhenReady() - - const transaction = collection.update(shared.id, (draft) => { - draft.name = `Optimistic` - }) - transactionReference.current = transaction - void transaction.when(`settled`).catch(() => undefined) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - - let model: MutationRefreshLivenessModel = { - fetch: `pending`, - application: `unaccepted`, - boundary: `fetch`, - throwOnError: true, - } - refreshResult.resolve([authoritative]) - await vi.waitFor(() => expect(collection.utils.isFetching).toBe(false)) - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - model = reduceMutationRefreshLiveness(model, { - type: `accept-application`, - }) - model = reduceMutationRefreshLiveness(model, { - type: `settle-fetch`, - outcome: `fulfilled`, - }) - - expect({ - refresh: refreshOutcome, - application: - collection.get(shared.id)?.name === authoritative.name - ? `resolved` - : `pending`, - }).toEqual(observeMutationRefreshLiveness(model)) - }, - ) - - it(`does not schedule Collection work for a foreign Query`, async () => { - oracleRoute(`case`) - const queryClient = createQueryClient() - const baselineClient = createQueryClient() - const first = createCollection( - queryCollectionOptions({ - id: `foreign-query-work-first`, - queryClient, - queryKey: [`foreign-query-work-first`], - queryFn: () => Promise.resolve([]), - getKey: (item) => item.id, - startSync: true, - }), - ) - const second = createCollection( - queryCollectionOptions({ - id: `foreign-query-work-second`, - queryClient, - queryKey: [`foreign-query-work-second`], - queryFn: () => Promise.resolve([]), - getKey: (item) => item.id, - startSync: true, - }), - ) - await Promise.all([first.stateWhenReady(), second.stateWhenReady()]) - - const nativeQueueMicrotask = globalThis.queueMicrotask - let scheduledMicrotasks = 0 - const queueMicrotaskSpy = vi - .spyOn(globalThis, `queueMicrotask`) - .mockImplementation((callback) => { - scheduledMicrotasks++ - nativeQueueMicrotask(callback) - }) - cleanups.push(async () => { - queueMicrotaskSpy.mockRestore() - await Promise.all([first.cleanup(), second.cleanup()]) - queryClient.clear() - baselineClient.clear() - }) - - const measureFetchMicrotasks = async ( - client: QueryClient, - queryKey: ReadonlyArray, - ): Promise => { - scheduledMicrotasks = 0 - await client.fetchQuery({ queryKey, queryFn: () => Promise.resolve([]) }) - for (let turn = 0; turn < 5; turn++) await Promise.resolve() - return scheduledMicrotasks - } - const baseline = await measureFetchMicrotasks(baselineClient, [ - `baseline-foreign-query`, - ]) - const withCollections = await measureFetchMicrotasks(queryClient, [ - `foreign-query`, - ]) - const model = observeQueryCacheWork({ - collectionCount: 2, - queryOwnership: `foreign`, - }) - const ownedQueryMutant = observeQueryCacheWork({ - collectionCount: 2, - queryOwnership: `owned`, - }) - - expect({ collectionMicrotasks: withCollections - baseline }).toEqual(model) - expect(ownedQueryMutant).not.toEqual(model) - }) - - it(`does not defer reverted fetch record retirement to a successor fetch`, async () => { - oracleRoute(`case`) - const measureSuccessorFetchMicrotasks = async ( - cancelFirst: boolean, - ): Promise => { - const id = `cancelled-fetch-record-${cancelFirst}` - const initial = { ...shared, name: `Initial` } - const authoritative = { ...shared, name: `Authoritative` } - const cancelledResult = createDeferred>() - const queryClient = createQueryClient() - const queryFn = vi.fn<() => Promise>>() - queryFn.mockResolvedValueOnce([initial]) - if (cancelFirst) queryFn.mockReturnValueOnce(cancelledResult.promise) - queryFn.mockResolvedValue([authoritative]) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - syncMode: `eager`, - startSync: true, - }), - ) - - try { - await collection.stateWhenReady() - if (cancelFirst) { - const cancelledRefetch = collection.utils.refetch({ - throwOnError: true, - }) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - await queryClient.cancelQueries({ queryKey: [id], exact: true }) - await cancelledRefetch - for (let turn = 0; turn < 5; turn++) await Promise.resolve() - } - - const nativeQueueMicrotask = globalThis.queueMicrotask - let scheduledMicrotasks = 0 - const queueMicrotaskSpy = vi - .spyOn(globalThis, `queueMicrotask`) - .mockImplementation((callback) => { - scheduledMicrotasks++ - nativeQueueMicrotask(callback) - }) - try { - await collection.utils.refetch({ throwOnError: true }) - for (let turn = 0; turn < 5; turn++) await Promise.resolve() - return scheduledMicrotasks - } finally { - queueMicrotaskSpy.mockRestore() - } - } finally { - cancelledResult.resolve([authoritative]) - await collection.cleanup() - queryClient.clear() - } - } - - const baseline = await measureSuccessorFetchMicrotasks(false) - const afterCancellation = await measureSuccessorFetchMicrotasks(true) - const expected = observeFetchRecordSuccessorWork({ state: `cancelled` }) - const retainedRecordMutant = observeFetchRecordSuccessorWork({ - state: `active`, - }) - - expect({ - extraCleanupMicrotasks: afterCancellation - baseline, - }).toEqual(expected) - expect(retainedRecordMutant).not.toEqual(expected) - }) - - it.each(handlerCollectionHistoryGrammar)( - `preserves $handlerKind handler identity through $accessPath refetch (legacy replay: $legacyOperation)`, - async (history) => { - oracleRoute(`case`) - const { handlerKind, accessPath, legacyOperation } = history - const initial = [ - { id: `first`, category: `shared`, name: `First` }, - { id: `second`, category: `shared`, name: `Second` }, - ] - const inserted = [ - { id: `inserted-first`, category: `shared`, name: `Client first` }, - { id: `inserted-second`, category: `shared`, name: `Client second` }, - ] - const authoritative = - handlerKind === `insert` - ? [ - ...initial, - { ...inserted[0]!, name: `Server inserted first` }, - { ...inserted[1]!, name: `Server inserted second` }, - ] - : handlerKind === `update` - ? [ - { ...initial[0]!, name: `Server updated first` }, - { ...initial[1]!, name: `Server updated second` }, - ] - : [] - const refresh = createDeferred>() - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce(initial) - .mockReturnValueOnce(refresh.promise) - const externalCollectionReference: { current?: object } = {} - const inspectIdentity = createDeferred() - const externalTransactionReference: { current?: object } = {} - let identityObservation: HandlerIdentityObservation | undefined - let handlerOutcome: RefetchApplicationOutcome = `pending` - const handleMutation = async ( - actualHandlerKind: MutationHandlerKind, - handlerCollection: { - utils: Pick< - QueryCollectionUtils, - `refetch` - > - }, - transaction: { - mutations: ReadonlyArray<{ collection: unknown }> - }, - ): Promise => { - await inspectIdentity.promise - const externalCollection = externalCollectionReference.current - if (!externalCollection) { - throw new Error(`The external Collection is not available`) - } - const externalTransaction = externalTransactionReference.current - if (!externalTransaction) { - throw new Error(`The external transaction is not available`) - } - const externalMutations = (externalTransaction as typeof transaction) - .mutations - const aliases = transaction.mutations.map( - ({ collection: mutationCollection }) => mutationCollection as object, - ) - const externalMap = new Map([ - [externalCollection, `external`], - ]) - const parameterMap = new Map([ - [handlerCollection, `parameter`], - ]) - const externalWeakMap = new WeakMap([ - [externalCollection, `external`], - ]) - const parameterWeakMap = new WeakMap([ - [handlerCollection, `parameter`], - ]) - identityObservation = { - handlerKind: actualHandlerKind, - accessPath, - matchingAliasCount: aliases.length, - transactionIsExternal: transaction === externalTransaction, - allMutationsPreserveIdentity: transaction.mutations.every( - (mutation, index) => mutation === externalMutations[index], - ), - parameterIsExternal: handlerCollection === externalCollection, - allAliasesMatchParameter: aliases.every( - (alias) => alias === handlerCollection, - ), - anyAliasMatchesExternal: aliases.some( - (alias) => alias === externalCollection, - ), - externalMapMatchesAnyAlias: aliases.some((alias) => - externalMap.has(alias), - ), - parameterMapMatchesAllAliases: aliases.every((alias) => - parameterMap.has(alias), - ), - externalWeakMapMatchesAnyAlias: aliases.some((alias) => - externalWeakMap.has(alias), - ), - parameterWeakMapMatchesAllAliases: aliases.every((alias) => - parameterWeakMap.has(alias), - ), - } - const refetchCollection = - accessPath === `parameter` - ? handlerCollection - : (aliases[0] as typeof handlerCollection) - const boundary = refetchCollection.utils.refetch({ throwOnError: true }) - await boundary.then( - () => { - handlerOutcome = `resolved` - }, - (error: unknown) => { - handlerOutcome = `rejected` - throw error - }, - ) - } - const collection = createCollection( - queryCollectionOptions({ - id: `handler-scope-${handlerKind}-${accessPath}-${legacyOperation}`, - queryClient, - queryKey: [`handler-scope`, handlerKind, accessPath, legacyOperation], - queryFn, - getKey: (item) => item.id, - startSync: true, - onInsert: ({ transaction, collection: handlerCollection }) => - handleMutation(`insert`, handlerCollection, transaction), - onUpdate: ({ transaction, collection: handlerCollection }) => - handleMutation(`update`, handlerCollection, transaction), - onDelete: ({ transaction, collection: handlerCollection }) => - handleMutation(`delete`, handlerCollection, transaction), - }), - ) - externalCollectionReference.current = collection - cleanups.push(async () => { - inspectIdentity.resolve() - refresh.resolve(authoritative) - await collection.cleanup() - queryClient.clear() - }) - await collection.stateWhenReady() - - let model: HandlerRefetchBoundaryModel = { - fetch: `pending`, - application: `unaccepted`, - throwOnError: true, - } - const mutation = - handlerKind === `insert` - ? collection.insert(inserted) - : handlerKind === `update` - ? collection.update( - initial.map(({ id }) => id), - (drafts) => { - drafts[0]!.name = `Client updated first` - drafts[1]!.name = `Client updated second` - }, - ) - : collection.delete(initial.map(({ id }) => id)) - externalTransactionReference.current = mutation - inspectIdentity.resolve() - void mutation.when(`settled`).catch(() => undefined) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - expect(handlerOutcome).toBe(observeHandlerRefetchBoundary(model).refetch) - if (!identityObservation) { - throw new Error( - `The mutation handler did not record Collection identity`, - ) - } - expectHandlerIdentityObservation( - identityObservation, - observeHandlerIdentity(history, 2), - ) - - refresh.resolve(authoritative) - model = reduceHandlerRefetchBoundary(model, { - type: `accept-application`, - }) - model = reduceHandlerRefetchBoundary(model, { - type: `settle-fetch`, - outcome: `fulfilled`, - }) - await vi.waitFor(() => { - expect(handlerOutcome).toBe( - observeHandlerRefetchBoundary(model).refetch, - ) - }) - - await mutation.when(`settled`) - model = reduceHandlerRefetchBoundary(model, { - type: `settle-application`, - outcome: `fulfilled`, - }) - expect(observeHandlerRefetchBoundary(model)).toEqual({ - refetch: `resolved`, - applicationErrorRecorded: false, - }) - expect([...collection.values()].map(({ name }) => name).sort()).toEqual( - authoritative.map(({ name }) => name).sort(), - ) - }, - ) - - it.each([ - { throwOnError: true, expectedOutcome: `rejected` as const }, - { throwOnError: false, expectedOutcome: `resolved` as const }, - ])( - `settles an application failure with throwOnError=$throwOnError`, - async ({ throwOnError, expectedOutcome }) => { - oracleRoute(`case`) - const initial = { ...shared, name: `Initial` } - const invalid = { ...shared, name: `Invalid` } - const applicationError = new Error(`Explicit refetch application failed`) - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockResolvedValueOnce([invalid]) - const consoleError = vi - .spyOn(console, `error`) - .mockImplementation(() => {}) - const collection = createCollection( - queryCollectionOptions({ - id: `explicit-refetch-application-failure-${throwOnError}`, - queryClient, - queryKey: [`explicit-refetch-application-failure`, throwOnError], - queryFn, - getKey: (item) => { - if (item.name === invalid.name) throw applicationError - return item.id - }, - startSync: true, - }), - ) - cleanups.push(async () => { - consoleError.mockRestore() - await collection.cleanup() - queryClient.clear() - }) - await collection.stateWhenReady() - - let model: ExplicitRefetchApplicationModel = { - calls: new Map(), - publicValues: new Map([[`query`, initial.name]]), - } - model = reduceExplicitRefetchApplication(model, { - type: `start-call`, - callId: 1, - resultIds: [`query`], - throwOnError, - }) - model = reduceExplicitRefetchApplication(model, { - type: `accept-result`, - callId: 1, - resultId: `query`, - value: invalid.name, - }) - model = reduceExplicitRefetchApplication(model, { - type: `reject-application`, - callId: 1, - resultId: `query`, - }) - - let actualOutcome: RefetchApplicationOutcome - const refetch = collection.utils.refetch({ throwOnError }) - if (throwOnError) { - await expect(refetch).rejects.toBe(applicationError) - actualOutcome = `rejected` - } else { - await expect(refetch).resolves.toHaveLength(1) - actualOutcome = `resolved` - } - expect(actualOutcome).toBe(expectedOutcome) - expectExplicitRefetchObservation( - { - publicValues: { query: collection.get(shared.id)?.name }, - refetch: actualOutcome, - ...(actualOutcome === `resolved` - ? { resultOrder: [`query`] } - : undefined), - }, - observeExplicitRefetchApplication(model, 1), - ) - }, - ) - - it(`records a handler-phase application failure after fetch settlement`, async () => { - oracleRoute(`case`) - const initial = { ...shared, name: `Initial` } - const invalid = { ...shared, name: `Invalid` } - const applicationError = new Error(`Handler application failed`) - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockResolvedValueOnce([invalid]) - const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) - let handlerOutcome: RefetchApplicationOutcome = `pending` - const collection = createCollection( - queryCollectionOptions({ - id: `handler-refetch-application-failure`, - queryClient, - queryKey: [`handler-refetch-application-failure`], - queryFn, - getKey: (item) => { - if (item.name === invalid.name) throw applicationError - return item.id - }, - startSync: true, - onUpdate: async ({ collection: handlerCollection }) => { - await handlerCollection.utils.refetch({ throwOnError: true }).then( - () => { - handlerOutcome = `resolved` - }, - (error: unknown) => { - handlerOutcome = `rejected` - throw error - }, - ) - }, - }), - ) - cleanups.push(async () => { - consoleError.mockRestore() - await collection.cleanup() - queryClient.clear() - }) - await collection.stateWhenReady() - - let model: HandlerRefetchBoundaryModel = { - fetch: `pending`, - application: `unaccepted`, - throwOnError: true, - } - const mutation = collection.update(shared.id, (draft) => { - draft.name = `Optimistic` - }) - await mutation.when(`settled`) - model = reduceHandlerRefetchBoundary(model, { - type: `accept-application`, - }) - model = reduceHandlerRefetchBoundary(model, { - type: `settle-fetch`, - outcome: `fulfilled`, - }) - model = reduceHandlerRefetchBoundary(model, { - type: `settle-application`, - outcome: `rejected`, - }) - await vi.waitFor(() => { - expect(reportedError(collection)).toBe(applicationError) - }) - - expect({ - refetch: handlerOutcome, - applicationErrorRecorded: reportedError(collection) === applicationError, - }).toEqual(observeHandlerRefetchBoundary(model)) - }) - - it(`rejects silent fulfillment in the public result-settlement checker`, () => { - oracleRoute(`calibration`) - const deferredOperation = `deferred` - const deferred = advanceResultSettlementModel( - startResultSettlementModel(deferredOperation), - { - type: `query-succeeded`, - operation: deferredOperation, - result: `applicable`, - rowCount: 1, - deferOn: `barrier-a`, - }, - ) - const invalidOperation = `invalid` - const invalid = advanceResultSettlementModel( - startResultSettlementModel(invalidOperation), - { - type: `query-succeeded`, - operation: invalidOperation, - result: `invalid-shape`, - }, - ) - const recoveredOperation = `recovered` - const withRecoveredOperation = advanceResultSettlementModel(invalid, { - type: `start-refetch`, - operation: recoveredOperation, - }) - const recovered = advanceResultSettlementModel(withRecoveredOperation, { - type: `query-succeeded`, - operation: recoveredOperation, - result: `applicable`, - rowCount: 1, - }) - const rebound = advanceResultSettlementModel(deferred, { - type: `refresh-succeeded`, - barrier: `barrier-a`, - result: `applicable`, - deferOn: `barrier-b`, - }) - expect(() => - expectPublicRefetchObservation(deferred, deferredOperation, { - settled: true, - outcome: `fulfilled`, - rowCount: 1, - }), - ).toThrow() - expect(() => - expectPublicRefetchObservation(invalid, invalidOperation, { - settled: true, - outcome: `fulfilled`, - }), - ).toThrow() - expect(() => - expectPublicRefetchObservation(recovered, recoveredOperation, { - settled: true, - outcome: `rejected`, - }), - ).toThrow() - expect(() => - expectPublicRefetchObservation(rebound, deferredOperation, { - settled: true, - outcome: `fulfilled`, - rowCount: 1, - }), - ).toThrow() - }) - - it(`fulfills a public refetch after applying a valid empty result`, async () => { - oracleRoute(`case`) - const id = `empty-result-settlement` - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([shared]) - .mockResolvedValueOnce([]) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - startSync: true, - }), - ) - cleanups.push(async () => { - await collection.cleanup() - queryClient.clear() - }) - - await collection.stateWhenReady() - const refetch = collection.utils.refetch({ throwOnError: true }) - await refetch - - const operation = `empty-result` - const expected = advanceResultSettlementModel( - startResultSettlementModel(operation), - { - type: `query-succeeded`, - operation, - result: `applicable`, - rowCount: 0, - }, - ) - expectPublicRefetchObservation(expected, operation, { - settled: true, - outcome: `fulfilled`, - rowCount: collection.size, - }) - }) - - it.each([`fulfilled`, `rejected`] as const)( - `keeps a same-result reentrant $receiptOutcome receipt attached to its refetch`, - async (receiptOutcome) => { - oracleRoute(`case`) - const id = `same-result-reentrant-application` - const initial = { ...shared, name: `Initial` } - const updated = { ...shared, name: `Updated` } - const applicationFailure = new Error(`Reentrant application failed`) - const reentrantApplication = createDeferred() - const reentrantApplicationStarted = createDeferred() - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockResolvedValueOnce([updated]) - const baseOptions = queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - startSync: true, - }) - const originalSync = baseOptions.sync - let commitDepth = 0 - let holdReentrantApplication = false - let reentrantCommitCount = 0 - const collection = createCollection({ - ...baseOptions, - sync: { - sync: (params: Parameters[0]) => - originalSync.sync({ - ...params, - commit: (signal) => { - commitDepth++ - try { - const applied = params.commit(signal) - if (holdReentrantApplication && commitDepth > 1) { - reentrantCommitCount++ - reentrantApplicationStarted.resolve(undefined) - return Promise.resolve( - applied === true ? undefined : applied, - ).then(() => reentrantApplication.promise) - } - return applied - } finally { - commitDepth-- - } - }, - }), - }, - }) - const consoleError = - receiptOutcome === `rejected` - ? vi.spyOn(console, `error`).mockImplementation(() => {}) - : undefined - let subscription = collection.subscribeChanges(() => {}, { - includeInitialState: false, - }) - cleanups.push(async () => { - consoleError?.mockRestore() - reentrantApplication.resolve(undefined) - subscription.unsubscribe() - await collection.cleanup() - queryClient.clear() - }) - - await collection.stateWhenReady() - let reentered = false - const publications: Array< - Array<{ type: string; name: string; previousName?: string }> - > = [] - subscription.unsubscribe() - subscription = collection.subscribeChanges( - (batch) => { - if (batch.length > 0) { - publications.push( - batch.map((change) => ({ - type: change.type, - name: change.value.name, - ...(change.type === `update` - ? { previousName: change.previousValue?.name } - : {}), - })), - ) - } - if (reentered || collection.get(shared.id)?.name !== updated.name) - return - reentered = true - // Dropping the last subscriber and immediately adding another makes - // Query DB apply this observer's exact current result again while the - // outer synchronous commit is still delivering its change. - subscription.unsubscribe() - subscription = collection.subscribeChanges(() => {}, { - includeInitialState: false, - }) - }, - { includeInitialState: false }, - ) - holdReentrantApplication = true - let model = createRefetchCallSettlementModel([`query`], true) - model = reduceRefetchCallSettlement(model, { - type: `accept-application`, - queryId: `query`, - applicationId: `reentrant-application`, - }) - model = reduceRefetchCallSettlement(model, { - type: `settle-fetch`, - queryId: `query`, - outcome: `fulfilled`, - }) - let refetchOutcome: RefetchApplicationOutcome = `pending` - const refetch = collection.utils.refetch({ throwOnError: true }).then( - (result) => { - refetchOutcome = `resolved` - return result - }, - (error: unknown) => { - refetchOutcome = `rejected` - throw error - }, - ) - void refetch.catch(() => undefined) - - await reentrantApplicationStarted.promise - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - expect(reentered).toBe(true) - expect(reentrantCommitCount).toBe(1) - expect(queryFn).toHaveBeenCalledTimes(2) - expect(refetchOutcome).toBe(observeRefetchCallSettlement(model).refetch) - expect(publications).toEqual([ - [{ type: `update`, name: `Updated`, previousName: `Initial` }], - ]) - - if (receiptOutcome === `fulfilled`) { - reentrantApplication.resolve(undefined) - await expect(refetch).resolves.toHaveLength(1) - } else { - reentrantApplication.reject(applicationFailure) - await expect(refetch).rejects.toBe(applicationFailure) - } - model = reduceRefetchCallSettlement(model, { - type: `settle-application`, - applicationId: `reentrant-application`, - outcome: receiptOutcome, - }) - expect(refetchOutcome).toBe(observeRefetchCallSettlement(model).refetch) - expect(collection.get(shared.id)?.name).toBe(updated.name) - expect(publications).toEqual([ - [{ type: `update`, name: `Updated`, previousName: `Initial` }], - ]) - }, - ) - - it.each([ - { - invalidShape: `non-array`, - throwOnError: false, - invalidResult: 42, - diagnostic: `Got: number`, - }, - { - invalidShape: `non-array`, - throwOnError: true, - invalidResult: 42, - diagnostic: `Got: number`, - }, - { - invalidShape: `null array member`, - throwOnError: false, - invalidResult: [null], - diagnostic: `Invalid item at index 0: null`, - }, - { - invalidShape: `null array member`, - throwOnError: true, - invalidResult: [null], - diagnostic: `Invalid item at index 0: null`, - }, - ] as const)( - `rejects a public refetch whose successful Query result has an invalid $invalidShape shape, throwOnError=$throwOnError`, - async ({ throwOnError, invalidResult, diagnostic }) => { - oracleRoute(`case`) - const id = `invalid-result-settlement` - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([shared]) - .mockResolvedValueOnce(invalidResult as unknown as Array) - const consoleError = vi - .spyOn(console, `error`) - .mockImplementation(() => {}) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - startSync: true, - }), - ) - cleanups.push(async () => { - consoleError.mockRestore() - await collection.cleanup() - queryClient.clear() - }) - - await collection.stateWhenReady() - let rejection: unknown - const refetch = collection.utils.refetch({ throwOnError }).then( - () => ({ settled: true, outcome: `fulfilled` as const }), - (error: unknown) => { - rejection = error - return { settled: true, outcome: `rejected` as const } - }, - ) - - const operation = `invalid-result` - const expected = advanceResultSettlementModel( - startResultSettlementModel(operation), - { - type: `query-succeeded`, - operation, - result: `invalid-shape`, - }, - ) - expectPublicRefetchObservation(expected, operation, await refetch) - expect(rejection).toBeInstanceOf(InvalidQueryResultError) - expect(rejection).toEqual( - expect.objectContaining({ - message: expect.stringContaining(diagnostic), - }), - ) - expect(rows(collection)).toEqual([shared.id]) - expect(consoleError).toHaveBeenCalledTimes(1) - expect(consoleError).toHaveBeenCalledWith( - expect.stringContaining(`[QueryCollection] Error applying query`), - expect.objectContaining({ - name: `InvalidQueryResultError`, - message: expect.stringContaining( - `@tanstack/query-db-collection: queryFn must return an array of objects`, - ), - }), - ) - }, - ) - - it(`keeps a deferred public refetch pending until its replacement applies`, async () => { - oracleRoute(`case`) - const id = `deferred-result-settlement` - const barrier = createDeferred() - const initial = { ...shared, name: `Initial` } - const skipped = { ...shared, name: `Skipped` } - const replacement = { ...shared, name: `Replacement` } - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockResolvedValueOnce([skipped]) - .mockResolvedValueOnce([replacement]) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - startSync: true, - }), - ) - cleanups.push(async () => { - collection.deferDataRefresh = null - barrier.resolve() - await collection.cleanup() - queryClient.clear() - }) - - await collection.stateWhenReady() - collection.deferDataRefresh = barrier.promise - let observation: PublicRefetchObservation = { settled: false } - const refetch = collection.utils.refetch({ throwOnError: true }).then( - () => { - observation = { - settled: true, - outcome: `fulfilled`, - rowCount: collection.size, - } - }, - () => { - observation = { settled: true, outcome: `rejected` } - }, - ) - - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - - const operation = `deferred-result` - let expected = advanceResultSettlementModel( - startResultSettlementModel(operation), - { - type: `query-succeeded`, - operation, - result: `applicable`, - rowCount: 1, - deferOn: `barrier-a`, - }, - ) - expectPublicRefetchObservation(expected, operation, observation) - expect(collection.get(shared.id)?.name).toBe(initial.name) - - collection.deferDataRefresh = null - barrier.resolve() - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(3)) - await refetch - - expected = advanceResultSettlementModel(expected, { - type: `refresh-succeeded`, - barrier: `barrier-a`, - result: `applicable`, - rowCount: 1, - }) - expectPublicRefetchObservation(expected, operation, observation) - expect(collection.get(shared.id)?.name).toBe(replacement.name) - }) - - it(`cancels a deferred public refetch when cleanup retires it`, async () => { - oracleRoute(`case`) - const id = `deferred-result-cleanup` - const barrier = createDeferred() - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([shared]) - .mockResolvedValueOnce([{ ...shared, name: `Skipped` }]) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - startSync: true, - }), - ) - cleanups.push(async () => { - collection.deferDataRefresh = null - barrier.resolve() - await collection.cleanup() - queryClient.clear() - }) - - await collection.stateWhenReady() - collection.deferDataRefresh = barrier.promise - const refetch = collection.utils.refetch({ throwOnError: true }).then( - () => ({ observation: { settled: true, outcome: `fulfilled` as const } }), - (error: unknown) => ({ - observation: { settled: true, outcome: `rejected` as const }, - error, - }), - ) - - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - const operation = `deferred-cleanup` - let expected = advanceResultSettlementModel( - startResultSettlementModel(operation), - { - type: `query-succeeded`, - operation, - result: `applicable`, - rowCount: 1, - deferOn: `barrier-a`, - }, - ) - await collection.cleanup() - expected = advanceResultSettlementModel(expected, { type: `retire` }) - - const result = await refetch - expectPublicRefetchObservation(expected, operation, result.observation) - expect(isCancelledError(`error` in result ? result.error : undefined)).toBe( - true, - ) - }) - - it(`does not reuse an invalid result rejection for a later valid refetch`, async () => { - oracleRoute(`case`) - const id = `recovered-result-settlement` - const queryHash = hashKey([id]) - const persistedScan = - createDeferred< - Array<{ key: string | number; value: Item; metadata?: unknown }> - >() - const scanPersistedRows = vi.fn(() => persistedScan.promise) - const invalidResult = 42 as unknown as Array - const recovered = { ...shared, name: `Recovered` } - const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) - const { collection, queryFn } = createOwnershipFixture({ - id, - results: [invalidResult, [recovered]], - syncMode: `on-demand`, - scanPersistedRows, - setupMetadata: (metadata) => { - metadata.collection.set(`queryCollection:gc:${queryHash}`, { - queryHash, - mode: `until-revalidated`, - }) - }, - }) - cleanups.push(() => { - consoleError.mockRestore() - return Promise.resolve() - }) - - let expected = startResultSettlementModel(`invalid`) - try { - const initialLoad = collection._sync.loadSubset({}) - await expect( - Promise.resolve(initialLoad === true ? undefined : initialLoad), - ).rejects.toBeInstanceOf(InvalidQueryResultError) - expected = advanceResultSettlementModel(expected, { - type: `query-succeeded`, - operation: `invalid`, - result: `invalid-shape`, - }) - expected = advanceResultSettlementModel(expected, { - type: `start-refetch`, - operation: `recovered`, - }) - - const refetch = collection.utils.refetch({ throwOnError: true }).then( - () => ({ settled: true, outcome: `fulfilled` as const }), - () => ({ settled: true, outcome: `rejected` as const }), - ) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(2) - expect(scanPersistedRows).toHaveBeenCalledOnce() - }) - persistedScan.resolve([]) - expected = advanceResultSettlementModel(expected, { - type: `query-succeeded`, - operation: `recovered`, - result: `applicable`, - }) - - expectPublicRefetchObservation(expected, `recovered`, await refetch) - await vi.waitFor(() => { - expect(collection.get(shared.id)?.name).toBe(recovered.name) - }) - } finally { - persistedScan.resolve([]) - } - }) - - it(`does not reuse an eager initial rejection when a refetch applies a valid result`, async () => { - oracleRoute(`case`) - const id = `eager-recovered-result-settlement` - const invalidResult = 42 as unknown as Array - const recovered = { ...shared, name: `Recovered` } - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce(invalidResult) - .mockResolvedValueOnce([recovered]) - const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - syncMode: `eager`, - startSync: true, - }), - ) - cleanups.push(async () => { - consoleError.mockRestore() - await collection.cleanup() - queryClient.clear() - }) - - let expected = startResultSettlementModel(`invalid`) - await expect(collection.stateWhenReady()).rejects.toBeInstanceOf( - InvalidQueryResultError, - ) - expected = advanceResultSettlementModel(expected, { - type: `query-succeeded`, - operation: `invalid`, - result: `invalid-shape`, - }) - expect(collection.status).toBe(`error`) - expect(reportedError(collection)).toBeDefined() - - expected = advanceResultSettlementModel(expected, { - type: `start-refetch`, - operation: `recovered`, - }) - let rejection: unknown - const recovery = collection.utils.refetch({ throwOnError: true }).then( - () => ({ - settled: true, - outcome: `fulfilled` as const, - rowCount: collection.size, - }), - (error: unknown) => { - rejection = error - return { settled: true, outcome: `rejected` as const } - }, - ) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - expected = advanceResultSettlementModel(expected, { - type: `query-succeeded`, - operation: `recovered`, - result: `applicable`, - rowCount: 1, - }) - - expectPublicRefetchObservation(expected, `recovered`, await recovery) - expect(rejection).toBeUndefined() - // An initial error is final for its sync run. The refetch applies its - // rows, but the Collection keeps its initial error until a restart. - expect(collection.status).toBe(`error`) - expect(reportedError(collection)).toBeDefined() - expect(collection.get(shared.id)?.name).toBe(recovered.name) - }) - - it(`rejects an invalid retained revalidation before its persisted baseline loads`, async () => { - oracleRoute(`case`) - const id = `retained-invalid-result-settlement` - const queryHash = hashKey([id]) - const persistedScan = - createDeferred< - Array<{ key: string | number; value: Item; metadata?: unknown }> - >() - const scanPersistedRows = vi.fn(() => persistedScan.promise) - const invalidResult = 42 as unknown as Array - const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) - const { collection, queryFn } = createOwnershipFixture({ - id, - results: [[shared], invalidResult], - syncMode: `on-demand`, - scanPersistedRows, - setupMetadata: (metadata) => { - metadata.collection.set(`queryCollection:gc:${queryHash}`, { - queryHash, - mode: `until-revalidated`, - }) - }, - }) - cleanups.push(() => { - consoleError.mockRestore() - return Promise.resolve() - }) - - const initialLoad = collection._sync.loadSubset({}) - const initialLoadPromise = Promise.resolve( - initialLoad === true ? undefined : initialLoad, - ) - void initialLoadPromise.catch(() => undefined) - +/** The old complete finite grammar is retained: 42 reacquisitions and eight + * dehydrate/hydrate histories. Neither family had a generated-run budget or + * literal reach table to retire. Publication holds and row observations are + * the original real QueryClient cuts, now judged by the framework check. */ +declareOracle({ + owns: [`src/query.ts#queryCollectionOptions`], + model: [ + modelReacquisition, + modelPublishedOwners, + lifetimeModel, + directOrderModel, + overlapModel, + ], + check: [checkCuts], +}) +oracleProperty({ + id: 'query-db.ownership.reacquisition', + law: { + statement: + 'A reacquired observer lease owns the current successful result until its own release; a delayed release of an older lease cannot retire it.', + authority: + 'docs/collections/query-collection.md#direct-writes-and-query-sync and the documented Query stale/refetch behavior.', + limits: + 'One released/reacquired on-demand demand, one/two rows, zero/two peer leases and an unrelated publication blocker; no native persistence.', + owner: 'Query DB ownership lifecycle', + }, + histories: reacquisitionHistories, + label: (h) => + JSON.stringify(h, (_key, value) => + value === Infinity ? 'Infinity' : value, + ), + model: modelReacquisition, + driver: async (history, observe) => { try { - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(1) - expect(scanPersistedRows).toHaveBeenCalledTimes(1) - }) - - let rejection: unknown - const refetch = collection.utils.refetch({ throwOnError: true }).then( - () => ({ settled: true, outcome: `fulfilled` as const }), - (error: unknown) => { - rejection = error - return { settled: true, outcome: `rejected` as const } - }, - ) - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) - - const operation = `retained-invalid-result` - const expected = advanceResultSettlementModel( - startResultSettlementModel(operation), - { - type: `query-succeeded`, - operation, - result: `invalid-shape`, - }, - ) - expectPublicRefetchObservation(expected, operation, await refetch) - expect(rejection).toBeInstanceOf(InvalidQueryResultError) - } finally { - persistedScan.resolve([]) - await initialLoadPromise.catch(() => undefined) - } - }) - - it(`keeps refetches pending when an older barrier refresh reaches a newer barrier`, async () => { - oracleRoute(`case`) - const id = `overlapping-defer-generations` - const barrierA = createDeferred() - const barrierB = createDeferred() - const initial = { ...shared, name: `Initial` } - const skippedA = { ...shared, name: `Skipped A` } - const skippedB = { ...shared, name: `Skipped B` } - const replacement = { ...shared, name: `Replacement` } - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockResolvedValueOnce([skippedA]) - .mockResolvedValueOnce([skippedB]) - .mockResolvedValueOnce([{ ...shared, name: `Refresh while B active` }]) - .mockResolvedValue([replacement]) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - startSync: true, - }), - ) - cleanups.push(async () => { - collection.deferDataRefresh = null - barrierA.resolve() - barrierB.resolve() - await collection.cleanup() - queryClient.clear() - }) - - await collection.stateWhenReady() - let expected = new Map() as ResultSettlementModelState - let firstObservation: PublicRefetchObservation = { settled: false } - let secondObservation: PublicRefetchObservation = { settled: false } - - const barrierAPromise = barrierA.promise - collection.deferDataRefresh = barrierAPromise - const clearBarrierA = barrierAPromise.finally(() => { - if (collection.deferDataRefresh === barrierAPromise) { - collection.deferDataRefresh = null - } - }) - expected = advanceResultSettlementModel(expected, { - type: `start-refetch`, - operation: `first`, - }) - const first = collection.utils.refetch({ throwOnError: true }).then( - () => { - firstObservation = { settled: true, outcome: `fulfilled`, rowCount: 1 } - }, - () => { - firstObservation = { settled: true, outcome: `rejected` } - }, - ) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(2) - expect(queryClient.getQueryData([id])).toEqual([skippedA]) - }) - expected = advanceResultSettlementModel(expected, { - type: `query-succeeded`, - operation: `first`, - result: `applicable`, - rowCount: 1, - deferOn: `barrier-a`, - }) - - const barrierBPromise = barrierB.promise - collection.deferDataRefresh = barrierBPromise - const clearBarrierB = barrierBPromise.finally(() => { - if (collection.deferDataRefresh === barrierBPromise) { - collection.deferDataRefresh = null - } - }) - expected = advanceResultSettlementModel(expected, { - type: `start-refetch`, - operation: `second`, - }) - const second = collection.utils.refetch({ throwOnError: true }).then( - () => { - secondObservation = { - settled: true, - outcome: `fulfilled`, - rowCount: 1, - } - }, - () => { - secondObservation = { settled: true, outcome: `rejected` } - }, - ) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(3) - expect(queryClient.getQueryData([id])).toEqual([skippedB]) - }) - expected = advanceResultSettlementModel(expected, { - type: `query-succeeded`, - operation: `second`, - result: `applicable`, - rowCount: 1, - deferOn: `barrier-b`, - }) - - barrierA.resolve() - await clearBarrierA - await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(4)) - expected = advanceResultSettlementModel(expected, { - type: `refresh-succeeded`, - barrier: `barrier-a`, - result: `applicable`, - rowCount: 1, - deferOn: `barrier-b`, - }) - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - - expectPublicRefetchObservation(expected, `first`, firstObservation) - expectPublicRefetchObservation(expected, `second`, secondObservation) - expect(collection.get(shared.id)?.name).toBe(initial.name) - - barrierB.resolve() - await clearBarrierB - await Promise.all([first, second]) - expect(queryFn).toHaveBeenCalledTimes(5) - expected = advanceResultSettlementModel(expected, { - type: `refresh-succeeded`, - barrier: `barrier-b`, - result: `applicable`, - rowCount: 1, - }) - expectPublicRefetchObservation(expected, `first`, firstObservation) - expectPublicRefetchObservation(expected, `second`, secondObservation) - expect(collection.get(shared.id)?.name).toBe(replacement.name) - }) - - it(`settles a fresh applied refetch independently of an older deferred result`, async () => { - oracleRoute(`case`) - const id = `independent-refetch-settlement` - const barrier = createDeferred() - const initial = { ...shared, name: `Initial` } - const skipped = { ...shared, name: `Skipped` } - const fresh = { ...shared, name: `Fresh` } - const queryClient = createQueryClient() - const queryFn = vi - .fn<() => Promise>>() - .mockResolvedValueOnce([initial]) - .mockResolvedValueOnce([skipped]) - .mockResolvedValueOnce([fresh]) - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn, - getKey: (item) => item.id, - startSync: true, - }), - ) - cleanups.push(async () => { - barrier.resolve() - await collection.cleanup() - queryClient.clear() - }) - - await collection.stateWhenReady() - collection.deferDataRefresh = barrier.promise - let oldObservation: PublicRefetchObservation = { settled: false } - const oldRefetch = collection.utils.refetch({ throwOnError: true }).then( - () => { - oldObservation = { settled: true, outcome: `fulfilled` } - }, - () => { - oldObservation = { settled: true, outcome: `rejected` } - }, - ) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(2) - expect(queryClient.getQueryData([id])).toEqual([skipped]) - }) - - let expected = advanceResultSettlementModel( - startResultSettlementModel(`old`), - { - type: `query-succeeded`, - operation: `old`, - result: `applicable`, - rowCount: 1, - deferOn: `barrier-a`, - }, - ) - collection.deferDataRefresh = null - expected = advanceResultSettlementModel(expected, { - type: `start-refetch`, - operation: `fresh`, - }) - let freshObservation: PublicRefetchObservation = { settled: false } - const freshRefetch = collection.utils.refetch({ throwOnError: true }).then( - () => { - freshObservation = { - settled: true, - outcome: `fulfilled`, - rowCount: collection.size, - } - }, - () => { - freshObservation = { settled: true, outcome: `rejected` } - }, - ) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(3) - expect(collection.get(shared.id)?.name).toBe(fresh.name) - }) - expected = advanceResultSettlementModel(expected, { - type: `query-succeeded`, - operation: `fresh`, - result: `applicable`, - rowCount: 1, - }) - for (let turn = 0; turn < 20; turn++) await Promise.resolve() - - expectPublicRefetchObservation(expected, `old`, oldObservation) - expectPublicRefetchObservation(expected, `fresh`, freshObservation) - await collection.cleanup() - barrier.resolve() - await Promise.allSettled([oldRefetch, freshRefetch]) - }) - - /** - * The driver uses the public `loadSubset` and `unloadSubset` entry points, a - * real QueryClient, and a held query function for the refetch. The blocker's - * mutation function holds publication. Each checkpoint follows a settled - * macrotask turn, so the comparison reads rows only after the step's sync - * work has run. `loadSubset` settlement is not compared: this law does not - * say whether the caller waits for the refetch. - */ - it.each(reacquisitionHistories)( - `keeps a reacquired lease's result (staleTime $staleTime, refetch $refetch, settles $settles, peers $peers, rows $rows)`, - async (history) => { - oracleRoute(`case`) const id = `reacquired-lease-${history.staleTime}-${history.refetch}-${history.settles}-${history.peers}-${history.rows}` const detail = categorySubset(`detail`) const peerSubsets = [ @@ -5316,15 +379,19 @@ describe(`query collection ownership lifecycle`, () => { // The release's sync transaction is accepted but held behind the blocker. const pendingBefore = collection._state.pendingSyncedTransactions.length collection._sync.unloadSubset(detail) - await vi.waitFor(() => - expect( - collection._state.pendingSyncedTransactions.length, - ).toBeGreaterThan(pendingBefore), + const accepted = await boundary( + () => + collection._state.pendingSyncedTransactions.length > pendingBefore, ) + observe('release-accepted', { + before: pendingBefore, + after: collection._state.pendingSyncedTransactions.length, + }) + if (!accepted) return const reacquired = Promise.resolve(collection._sync.loadSubset(detail)) void reacquired.catch(() => undefined) await settleTurn() - const held = rows(collection) + observe('held', rows(collection)) if (history.settles === `while-held`) { settleRefetch() @@ -5333,50 +400,71 @@ describe(`query collection ownership lifecycle`, () => { persistence.resolve() await mutation.when(`settled`) await settleTurn() - const published = rows(collection) + observe('published', rows(collection)) if (history.settles === `after-publication`) { settleRefetch() await settleTurn() } await reacquired.catch(() => undefined) - const settled = rows(collection) + observe('settled', rows(collection)) for (const peer of peerSubsets) { collection._sync.unloadSubset(peer) await settleTurn() } - const peerReleased = rows(collection) + observe('peerReleased', rows(collection)) collection._sync.unloadSubset(detail) await settleTurn() - expect({ - held, - published, - settled, - peerReleased, - released: rows(collection), - queryCalls: queryFn.mock.calls.length, - }).toEqual(modelReacquisition(history)) - }, - ) + observe('released', rows(collection)) + observe('queryCalls', queryFn.mock.calls.length) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => { + const release = trace[0] + if (release?.checkpoint !== 'release-accepted') + return violation( + 'release-accepted', + 'release acceptance not observed', + 'lease ownership', + ) + const counts = release.value as { before: number; after: number } + if (!(counts.after > counts.before)) + return violation( + 'release-accepted', + 'the held release did not enter the accepted sync queue', + 'lease ownership', + ) + return checkCuts( + trace.slice(1), + Object.entries(expected).map(([checkpoint, value]) => ({ + checkpoint, + value, + })), + 'lease ownership', + ) + }, +}) - /** - * The driver runs a server DbClient and a hydrated browser DbClient over the - * same Collection descriptor. On the server, an optimistic blocker holds - * publication while `a` is released and, in `while-held` histories, while - * `b` is acquired. In `reacquired` histories, `a`'s Query is removed with - * the public `QueryClient.removeQueries` after its release, then `a` is - * reacquired and its held fetch rejects after `b` claims the row. The - * checkpoints are the dehydrated state after every held - * transaction publishes, and the hydrated client's public rows after it - * acquires and then releases `b`. - */ - it.each(publishedOwnerHistories)( - `publishes only active owners when a lease claims a released row (claims $claims, peer $peer, reacquired $reacquired)`, - async (history) => { - oracleRoute(`case`) +oracleProperty({ + id: 'query-db.ownership.published-owners', + law: { + statement: + 'Publication records only active leases with successful results; hydration cannot resurrect a released owner.', + authority: + 'docs/collections/query-collection.md#direct-writes-and-query-sync; DbClient dehydrate/hydrate row-ownership contract.', + limits: + 'One released lease, one claimant and optional surviving peer; the reacquired original fetch may reject. Native SQLite is a separate receiving path.', + owner: 'Query DB ownership lifecycle', + }, + histories: publishedOwnerHistories, + model: modelPublishedOwners, + driver: async (history, observe) => { + try { const id = `published-owners-${history.claims}-${history.peer}-${history.reacquired}` const subset = (category: string): LoadSubsetOptions => ({ where: new IR.Func(`in`, [ @@ -5489,494 +577,639 @@ describe(`query collection ownership lifecycle`, () => { const browser = createClient() browser.client.hydrate(state) await browser.collection._sync.loadSubset(b) - const hydratedLoaded = rows(browser.collection) + observe('hydratedLoaded', rows(browser.collection)) browser.collection._sync.unloadSubset(b) await settleTurn() - expect({ - dehydratedOwners: Object.keys(owners ?? {}).length, - hydratedLoaded, - hydratedReleased: rows(browser.collection), - }).toEqual(modelPublishedOwners(history)) - }, - ) - - // Law: a fetch that started before a direct write keeps the server's rows - // for every key that write did not touch; a key written after the fetch - // began keeps its accepted row. Reference: the fetched rows, overlaid by the - // direct writes made after the fetch started, in order. Checkpoint: visible - // rows and the Query cache after the fetch settles. Cuts: the initial load, - // a handler's explicit refetch while another handler writes a different - // key or the same key, and a user refetch. The review probes are these cuts. - type MergeCut = - | `initial-load` - | `handler-refetch-other-key` - | `handler-refetch-same-key` - | `user-refetch` - it.each([ - `initial-load`, - `handler-refetch-other-key`, - `handler-refetch-same-key`, - `user-refetch`, - ])( - `keeps server rows a direct write did not touch when an older fetch settles: %s`, - async (cut) => { - oracleRoute(`case`) - type Row = { id: string; value: string } - const id = `older-fetch-merge-${cut}` - let server: Array = - cut === `initial-load` - ? [ - { id: `1`, value: `a` }, - { id: `2`, value: `b` }, - ] - : [ - { id: `1`, value: `old1` }, - { id: `2`, value: `old2` }, - ] - const held: Array>> = [] - let hold = cut === `initial-load` - const queryClient = createQueryClient() - const writtenKey = cut === `handler-refetch-same-key` ? `1` : `2` - const collection = createCollection( - queryCollectionOptions({ - id, - queryClient, - queryKey: [id], - queryFn: async () => { - const snapshot = server.map((row) => ({ ...row })) - if (hold) { - const gate = createDeferred() - held.push(gate) - await gate.promise - } - return snapshot - }, - getKey: (row) => row.id, - startSync: true, - onUpdate: async ({ transaction, collection: handlerCollection }) => { - const modified = transaction.mutations[0].modified - if (modified.value === `A`) { - // Handler A explicitly reads back the accepted server change. - server = server.map((row) => - row.id === `1` ? { id: `1`, value: `A` } : row, - ) - await handlerCollection.utils.refetch() - return - } - server = server.map((row) => - row.id === writtenKey ? { id: writtenKey, value: `B` } : row, - ) - await handlerCollection.utils.writeUpdate({ - id: writtenKey, - value: `B`, - }) - }, - }), - ) - cleanups.push(async () => { - held.forEach((gate) => gate.resolve()) - await collection.cleanup() - queryClient.clear() - }) - const visible = () => - Object.fromEntries( - Array.from(collection.values(), (row) => [row.id, row.value]), - ) - const cached = () => - Object.fromEntries( - (queryClient.getQueryData>([id]) ?? []).map((row) => [ - row.id, - row.value, - ]), - ) - - let expected: Record - if (cut === `initial-load`) { - const fetched = server - const ready = collection.preload() - await vi.waitFor(() => expect(held).toHaveLength(1)) - await collection.utils.writeInsert({ id: `3`, value: `c` }) - expect( - queryClient.getQueryCache().find({ queryKey: [id] })?.state.status, - ).toBe(`pending`) - held[0]!.resolve() - await ready - expected = mergeDirectWritesOverFetch(fetched, [ - { id: `3`, value: `c` }, - ]) - } else if (cut === `user-refetch`) { - await collection.preload() - server = [ - { id: `1`, value: `new1` }, - { id: `2`, value: `new2` }, - ] - const fetched = server - hold = true - const refetch = collection.utils.refetch() - await vi.waitFor(() => expect(held).toHaveLength(1)) - hold = false - await collection.utils.writeInsert({ id: `3`, value: `c` }) - held[0]!.resolve() - await refetch - expected = mergeDirectWritesOverFetch(fetched, [ - { id: `3`, value: `c` }, - ]) - // The refetch settles only once the merged rows are visible. - expect(visible()).toEqual(expected) - } else { - await collection.preload() - hold = true - const first = collection.update(`1`, (draft) => { - draft.value = `A` - }) - await vi.waitFor(() => expect(held).toHaveLength(1)) - hold = false - const fetched = server - await collection - .update(writtenKey, (draft) => { - draft.value = `B-optimistic` - }) - .when(`settled`) - held[0]!.resolve() - await first.when('settled') - expected = mergeDirectWritesOverFetch(fetched, [ - { id: writtenKey, value: `B` }, - ]) - } - await vi.waitFor(() => expect(visible()).toEqual(expected)) - expect(cached()).toEqual(expected) - }, - ) - - /** - * A fetched row lifetime has one or more subset claims. A direct update - * preserves them; a direct delete ends that lifetime and all its claims. - * Inserting or upserting the same key after the delete creates an unowned - * lifetime. A later provider result can claim it again. The reference below - * uses only these row-lifetime rules, not Query cache or ownership helpers. - * - * The bounded legal grammar crosses one/two overlapping subset owners, - * in-memory/persisted metadata, and update, delete, reinsert, upsert, and - * refetch-after-reinsert. The driver uses real QueryClient and public write - * utilities. The independent claim set records demand indices, then maps - * those indices to the documented static on-demand Query key for metadata - * comparison. It compares public rows and exact accepted owner identities - * after each action and release; forgetting ownership on any write fails the - * update grammar, while retaining a deleted lifetime fails the reinsert grammar. - */ - it.each( - ([1, 2] as const).flatMap((ownerCount) => - ([false, true] as const).flatMap((withMetadata) => - ( - [ - `update`, - `delete`, - `delete-insert`, - `delete-upsert`, - `delete-insert-refetch`, - ] as const - ).map((history) => ({ ownerCount, withMetadata, history })), - ), + observe('dehydratedOwners', Object.keys(owners ?? {}).length) + observe('hydratedReleased', rows(browser.collection)) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts( + trace, + [ + { checkpoint: 'hydratedLoaded', value: expected.hydratedLoaded }, + { checkpoint: 'dehydratedOwners', value: expected.dehydratedOwners }, + { checkpoint: 'hydratedReleased', value: expected.hydratedReleased }, + ], + 'published owners', ), - )( - `tracks a direct row lifetime through $history with $ownerCount owners and metadata=$withMetadata`, - (history) => { - oracleRoute(`case`) - return runDirectRowLifetimeHistory(history) - }, - ) +}) - /** - * A's accepted direct row has its own position. A physical fetch started - * later may replace and claim it when A next acquires the shared Query; - * an earlier fetched snapshot or B's local cache edit may not. The small - * reference below orders only authored writes and actual provider fetches. - * Cache edits invalidate the last fetched snapshot but do not create a new - * remote position. It deliberately has no Query observers or cache-version - * classifier. - * - * The legal grammar crosses fetch/write order, another write to the same or - * a disjoint key, a foreign local cache edit, and a final real refetch. The - * driver uses two Collections sharing one physical Query. At A's acquisition - * and subset release it compares public rows with the reference. A fetched - * cached result needs no request; a local cache edit with no later fetch - * requires one to establish authority. The inverse order rejects blindly - * trusting any cached result; the cache-edit grammars reject mistaking - * setQueryData for a remote success. - */ - it.each( - ([`write-first`, `fetch-first`] as const).flatMap((order) => - ([`none`, `same-key`, `other-key`] as const).flatMap((laterWrite) => - ([false, true] as const).flatMap((cacheEdit) => - ([false, true] as const).map((finalRefetch) => ({ - order, - laterWrite, - cacheEdit, - finalRefetch, - })), - ), - ), +/** Row lifetime laws from Query guide Direct Writes and Query Sync. A delete + * ends all fetched claims; a later insert/upsert starts an unowned lifetime. + * Each action below is authored by the finite history, independent of cache + * events, Collection membership and production commit timing. */ +type LifetimeHistory = { + ownerCount: 1 | 2 + withMetadata: boolean + history: + | 'update' + | 'delete' + | 'delete-insert' + | 'delete-upsert' + | 'delete-insert-refetch' +} +const lifetimeId = (h: LifetimeHistory) => + `direct-row-lifetime-${h.ownerCount}-${h.withMetadata}-${h.history}` +const lifetimeHistories: Array = ([1, 2] as const).flatMap( + (ownerCount) => + [false, true].flatMap((withMetadata) => + ( + [ + 'update', + 'delete', + 'delete-insert', + 'delete-upsert', + 'delete-insert-refetch', + ] as const + ).map((history) => ({ ownerCount, withMetadata, history })), ), - )( - `orders shared fetches, writes, and cache edits before acquisition: $order/$laterWrite/cache=$cacheEdit/refetch=$finalRefetch`, - (history) => { - oracleRoute(`case`) - return runDirectWriteOrderHistory(history) - }, +) +function lifetimeModel(h: LifetimeHistory): Array { + const expected: Array = [] + const demands = [categorySubset('detail'), categorySubset('list')].slice( + 0, + h.ownerCount, + ) + // This is only the documented exact Query-key representation. Claim lifetime + // is decided by the independent set of authored demand indices below. + const hashes = demands.map((d) => + hashKey([lifetimeId(h), getLoadSubsetDemandKey(d)]), ) + const claims = new Set() + let name: string | undefined + let direct = false + const row = () => + expected.push({ + checkpoint: `row:${expected.length}`, + value: { + name, + rows: name ? [shared.id] : [], + ...(h.withMetadata + ? { owners: [...claims].map((i) => hashes[i]!).sort() } + : {}), + }, + }) + for (let i = 0; i < h.ownerCount; i++) { + claims.add(i) + name = shared.name + row() + } + if (h.history === 'update') { + name = 'Updated' + row() + } else { + name = undefined + claims.clear() + row() + if (h.history !== 'delete') { + name = 'Reinserted' + direct = true + row() + } + } + if (h.history === 'delete-insert-refetch') { + for (let i = 0; i < h.ownerCount; i++) claims.add(i) + name = 'Server result' + direct = false + row() + } + for (let i = 0; i < h.ownerCount; i++) { + claims.delete(i) + if (claims.size === 0 && !direct) name = undefined + row() + } + expected.push({ + checkpoint: 'queryCalls', + value: h.ownerCount * (h.history === 'delete-insert-refetch' ? 2 : 1), + }) + return expected +} - /** - * Two distinct on-demand Queries can return the same row. For that row, a - * successfully applied physical fetch takes its position when it starts: - * the latest such fetch supplies the value, even when an older fetch settles - * afterward. An older result still claims its own subset, so releasing the - * newer subset does not discard the row or revert its value. An accepted - * direct write participates in the same order for the key it changes. - * - * The reference below has only one row, its last accepted position, and a - * set of subset owners. It does not classify Query cache events or replay - * production's ownership maps. The legal histories cross an existing versus - * first-time older owner and both settlement orders; two neighboring writes - * fall between or after the fetch starts. The driver uses distinct semantic - * demands and real Query observers. One newer result repeats the initial - * value: it still takes a position that an older result cannot undo. After - * each applied settlement and after releasing the newer owner, the public - * Collection row must match the model. - */ - it.each([ - ...([false, true] as const).flatMap((existingOwner) => - ([false, true] as const).map((settleNewerFirst) => ({ - existingOwner, - settleNewerFirst, - directWrite: `none` as const, - sameValue: false, +/** Fetch-start and accepted direct-write positions are authored actions. Cache + * edits invalidate evidence but create no physical fetch position. Comparing + * the two positions yields the authority at acquisition and after release. */ +type DirectOrderHistory = { + order: 'write-first' | 'fetch-first' + laterWrite: 'none' | 'same-key' | 'other-key' + cacheEdit: boolean + finalRefetch: boolean +} +const directOrderHistories: Array = ( + ['write-first', 'fetch-first'] as const +).flatMap((order) => + (['none', 'same-key', 'other-key'] as const).flatMap((laterWrite) => + [false, true].flatMap((cacheEdit) => + [false, true].map((finalRefetch) => ({ + order, + laterWrite, + cacheEdit, + finalRefetch, })), ), + ), +) +function directOrderModel(h: DirectOrderHistory): Array { + let position = 0 + let directX: { value: string; position: number } | undefined + let remoteX: { value: string; position: number } | undefined + if (h.order === 'write-first') + directX = { value: 'Written', position: ++position } + remoteX = { value: 'Server 1', position: ++position } + if (h.order === 'fetch-first') + directX = { value: 'Written', position: ++position } + if (h.laterWrite === 'same-key') + directX = { value: 'Later write', position: ++position } + if (h.laterWrite === 'other-key') position++ + if (h.finalRefetch) remoteX = { value: 'Server 2', position: ++position } + const revalidated = h.cacheEdit && !h.finalRefetch + if (revalidated) remoteX = { value: 'Server 2', position: ++position } + if (!directX) throw Error('Illegal authored direct-write history') + const remoteMayReplace = remoteX.position > directX.position + return [ { - existingOwner: true, - settleNewerFirst: true, - directWrite: `none` as const, - sameValue: true, + checkpoint: 'acquisition-work', + value: { + before: 1 + Number(h.finalRefetch), + after: 1 + Number(h.finalRefetch) + Number(revalidated), + }, }, { - existingOwner: true, - settleNewerFirst: true, - directWrite: `between` as const, - sameValue: false, + checkpoint: 'acquired', + value: { + x: remoteMayReplace ? remoteX.value : directX.value, + y: + h.laterWrite === 'other-key' && !h.finalRefetch && !revalidated + ? 'Other write' + : undefined, + }, }, { - existingOwner: true, - settleNewerFirst: true, - directWrite: `after` as const, - sameValue: false, - }, - ])( - `orders overlapping subset values and owners: existing=$existingOwner/newer-first=$settleNewerFirst/write=$directWrite/equal=$sameValue`, - (history) => { - oracleRoute(`case`) - return runOverlappingSubsetHistory(history) + checkpoint: 'released', + value: remoteMayReplace ? undefined : directX.value, }, - ) + ] +} - it(`does not rescan a stable empty persisted baseline on retained reacquisition`, async () => { - oracleRoute(`case`) - const id = `empty-persisted-baseline-work` - const queryHash = hashKey([id]) - const scanPersistedRows = vi.fn(() => Promise.resolve([])) - const { collection, queryFn } = createOwnershipFixture({ - id, - results: [[], [], []], - scanPersistedRows, - persistedGcTime: Number.POSITIVE_INFINITY, - setupMetadata: (metadata) => - metadata.collection.set(`queryCollection:gc:${queryHash}`, { - queryHash, - mode: `until-revalidated`, - }), +/** A complete finite overlap grammar retains the original seven histories. + * Started positions order accepted values, while independent claim indices + * decide presence. Reacquisition starts a new row lifetime from its own cache. */ +type OverlapHistory = { + existingOwner: boolean + settleNewerFirst: boolean + directWrite: 'none' | 'between' | 'after' + sameValue: boolean +} +const overlapHistories: Array = [ + ...[false, true].flatMap((existingOwner) => + [false, true].map((settleNewerFirst) => ({ + existingOwner, + settleNewerFirst, + directWrite: 'none' as const, + sameValue: false, + })), + ), + { + existingOwner: true, + settleNewerFirst: true, + directWrite: 'none', + sameValue: true, + }, + { + existingOwner: true, + settleNewerFirst: true, + directWrite: 'between', + sameValue: false, + }, + { + existingOwner: true, + settleNewerFirst: true, + directWrite: 'after', + sameValue: false, + }, +] +function overlapModel(h: OverlapHistory): Array { + const expected: Array = [ + { + checkpoint: 'scope-admission', + value: { + distinct: true, + initialMatchesOlder: true, + olderMatchesOlder: true, + newerMatchesNewer: true, + }, + }, + ] + let position = 0 + let acceptedAt = 0 + let value: string | undefined + const owners = new Set<'older' | 'newer'>() + let rowIndex = 0 + const row = () => + expected.push({ + checkpoint: `row:${rowIndex++}`, + value: { + name: owners.size > 0 ? value : undefined, + present: owners.size > 0, + }, }) - const history: Array<`empty-result` | `restart`> = [] - - await collection._sync.loadSubset({}) - history.push(`empty-result`) - expect(rows(collection)).toEqual([]) - expect(scanPersistedRows).toHaveBeenCalledTimes( - expectedEmptyBaselineScans(history), - ) - - for (let result = 0; result < 2; result++) { - collection._sync.unloadSubset({}) - await collection._sync.loadSubset({ refetch: true }) - history.push(`empty-result`) - expect(rows(collection)).toEqual([]) - expect(scanPersistedRows).toHaveBeenCalledTimes( - expectedEmptyBaselineScans(history), - ) + const fetch = (owner: 'older' | 'newer', started: number, name: string) => { + owners.add(owner) + if (started > acceptedAt) { + acceptedAt = started + value = name } - expect(queryFn).toHaveBeenCalledTimes(3) + } + const write = () => { + value = 'Manual' + acceptedAt = ++position + row() + } + if (h.existingOwner) { + fetch('older', ++position, 'Initial') + row() + } + const older = ++position + expected.push({ + checkpoint: 'older-started', + value: h.existingOwner ? 2 : 1, }) + if (h.directWrite === 'between') write() + const newer = ++position + expected.push( + { checkpoint: 'newer-started', value: h.existingOwner ? 3 : 2 }, + { checkpoint: 'physical-queries', value: 2 }, + ) + if (h.directWrite === 'after') write() + const settle = (owner: 'older' | 'newer') => { + fetch( + owner, + owner === 'older' ? older : newer, + owner === 'older' ? 'Older' : h.sameValue ? 'Initial' : 'Newer', + ) + row() + } + if (h.settleNewerFirst) { + settle('newer') + settle('older') + } else { + settle('older') + settle('newer') + } + const release = (owner: 'older' | 'newer') => { + owners.delete(owner) + row() + } + if (h.existingOwner) { + release('older') + release('newer') + } else { + release('newer') + release('older') + } + if (!h.existingOwner && h.settleNewerFirst && h.directWrite === 'none') { + owners.add('older') + value = 'Older' + row() + expected.push({ checkpoint: 'older-reacquired-work', value: 2 }) + owners.delete('older') + row() + owners.add('newer') + value = h.sameValue ? 'Initial' : 'Newer' + row() + expected.push({ checkpoint: 'newer-reacquired-work', value: 2 }) + } + return expected +} - it(`scans a cold durable owner that appeared after an empty sync run`, async () => { - oracleRoute(`case`) - const id = `cold-owner-after-empty-baseline` - const queryHash = hashKey([id]) - const hotStorage = createOwnershipStorage() - const hot = createPersistedOwnershipFixture(id, hotStorage, []) - await hot.collection._sync.loadSubset({}) - expect(storedItems(hotStorage)).toEqual([]) - await hot.collection.cleanup() - - // This seeded durable state represents another writer's committed owner - // while the first Collection had no sync run. The cold Collection cannot - // use the first run's empty-baseline evidence. - const coldSeed = hotStorage.snapshot() - coldSeed.rows.set(shared.id, shared) - coldSeed.rowMetadata.set(shared.id, { - queryCollection: { owners: { [queryHash]: true } }, - }) - coldSeed.collectionMetadata.set(`queryCollection:gc:${queryHash}`, { - queryHash, - mode: `until-revalidated`, +async function runDirectRowLifetimeHistory( + { ownerCount, withMetadata, history }: LifetimeHistory, + observe: RecordFact, +): Promise { + const id = `direct-row-lifetime-${ownerCount}-${withMetadata}-${history}` + const replacement = { ...shared, name: `Reinserted` } + const serverReplacement = { ...shared, name: `Server result` } + const initialResults = Array.from({ length: ownerCount }, () => [shared]) + const laterResults = Array.from({ length: ownerCount }, () => [ + serverReplacement, + ]) + const metadata: MetadataRecorder = { rows: new Map(), writes: [] } + const { collection, queryFn } = createOwnershipFixture({ + id, + results: [...initialResults, ...laterResults], + metadataRecorder: withMetadata ? metadata : undefined, + }) + const demands = [categorySubset(`detail`), categorySubset(`list`)].slice( + 0, + ownerCount, + ) + let cut = 0 + const recordRow = () => + observe('row:' + cut++, { + name: collection.get(shared.id)?.name, + rows: rows(collection), + ...(withMetadata + ? { + owners: persistedOwners( + collection._state.syncedMetadata, + shared.id, + ), + } + : {}), }) - const coldStorage = createOwnershipStorage(coldSeed) - const coldScanRows = vi.spyOn(coldStorage.adapter, `scanRows`) - const cold = createPersistedOwnershipFixture(id, coldStorage, []) - await cold.collection._sync.loadSubset({}) + for (const demand of demands) { + await collection._sync.loadSubset(demand) + recordRow() + } + if (history === `update`) { + await collection.utils.writeUpdate({ id: shared.id, name: `Updated` }) + recordRow() + } else { + await collection.utils.writeDelete(shared.id) + recordRow() + if (history !== `delete`) { + if (history === `delete-upsert`) + await collection.utils.writeUpsert(replacement) + else await collection.utils.writeInsert(replacement) + recordRow() + } + } + if (history === `delete-insert-refetch`) { + await collection.utils.refetch() + recordRow() + } + for (const demand of demands) { + collection._sync.unloadSubset(demand) + recordRow() + } + observe('queryCalls', queryFn.mock.calls.length) +} - // The empty provider result retires the cold durable owner. A design that - // skips every empty-baseline scan leaves the stored row behind. - expect(cold.queryFn).toHaveBeenCalledOnce() - expect(rows(cold.collection)).toEqual([]) - expect(storedItems(coldStorage)).toEqual([]) - expect(coldScanRows).toHaveBeenCalledTimes( - expectedEmptyBaselineScans([`restart`, `empty-result`]), +async function runDirectWriteOrderHistory( + { order, laterWrite, cacheEdit, finalRefetch }: DirectOrderHistory, + observe: RecordFact, +): Promise { + type Row = { id: string; name: string } + const queryClient = createQueryClient() + const queryKey = [`shared-post-write-cached-before-acquisition`] + let requestNumber = 0 + const queryFn = vi.fn(() => + Promise.resolve([ + { + id: `x`, + name: ++requestNumber === 1 ? `Server 1` : `Server 2`, + }, + ]), + ) + const makeCollection = (id: string) => + createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey, + queryFn, + getKey: (row) => row.id, + syncMode: `on-demand`, + staleTime: Infinity, + startSync: true, + }), ) + const a = makeCollection(`cached-newer-a`) + const b = makeCollection(`cached-newer-b`) + cleanups.push(async () => { + await Promise.all([a.cleanup(), b.cleanup()]) + queryClient.clear() + }) + const writeX = async (name: string) => { + if (a.has(`x`)) await a.utils.writeUpdate({ id: `x`, name }) + else await a.utils.writeInsert({ id: `x`, name }) + } + const fetchB = async (first: boolean) => { + if (first) await b._sync.loadSubset({}) + else await b.utils.refetch({ throwOnError: true }) + } + if (order === `write-first`) await writeX(`Written`) + await fetchB(true) + if (order === `fetch-first`) await writeX(`Written`) + if (laterWrite === `same-key`) await writeX(`Later write`) + if (laterWrite === `other-key`) { + await a.utils.writeInsert({ id: `y`, name: `Other write` }) + } + if (cacheEdit) { + queryClient.setQueryData(queryKey, [{ id: `x`, name: `Local cache edit` }]) + } + if (finalRefetch) await fetchB(false) + const callsBeforeAcquisition = queryFn.mock.calls.length + await a._sync.loadSubset({}) + observe('acquisition-work', { + before: callsBeforeAcquisition, + after: queryFn.mock.calls.length, }) + observe('acquired', { x: a.get('x')?.name, y: a.get('y')?.name }) + a._sync.unloadSubset({}) + observe('released', a.get('x')?.name) +} - it(`rejects a superseded accepted result while publishing its successor`, async () => { - oracleRoute(`case`) - const id = `superseded-retained-scan` - const queryKey = [id] as const - const queryHash = hashKey(queryKey) - const initial = { id: `initial`, category: `retained`, name: `Initial` } - const first = { id: `first`, category: `retained`, name: `First` } - const second = { id: `second`, category: `retained`, name: `Second` } - const initialScan = - createDeferred< - Array<{ key: string | number; value: Item; metadata?: unknown }> - >() - const firstRefetchScan = - createDeferred< - Array<{ key: string | number; value: Item; metadata?: unknown }> - >() - const scanPersistedRows = vi - .fn() - .mockReturnValueOnce(initialScan.promise) - .mockReturnValueOnce(firstRefetchScan.promise) - .mockResolvedValue([]) - const { collection, queryFn } = createOwnershipFixture({ - id, - results: [[initial], [first], [second]], - syncMode: `eager`, - scanPersistedRows, - setupMetadata: (metadata) => { - metadata.collection.set(`queryCollection:gc:${queryHash}`, { - queryHash, - mode: `until-revalidated`, - }) - }, - }) - const publications: Array> = [] - const subscription = collection.subscribeChanges(() => { - publications.push(itemIds(collection.toArray)) - }) - cleanups.push(() => { - subscription.unsubscribe() - return Promise.resolve() - }) - cleanups.push(() => { - initialScan.resolve([]) - firstRefetchScan.resolve([]) - return Promise.resolve() +async function runOverlappingSubsetHistory( + { existingOwner, settleNewerFirst, directWrite, sameValue }: OverlapHistory, + observe: RecordFact, +): Promise { + const id = `overlapping-fetch-value-${existingOwner}-${settleNewerFirst}-${directWrite}-${sameValue}` + const older = createDeferred>() + const newer = createDeferred>() + const initial = { ...shared, name: `Initial` } + const olderRow = { ...shared, name: `Older` } + const newerRow = { ...shared, name: sameValue ? `Initial` : `Newer` } + const manual = { ...shared, name: `Manual` } + const { collection, queryClient, queryFn } = createOwnershipFixture({ + id, + results: existingOwner + ? [[initial], older.promise, newer.promise] + : [older.promise, newer.promise], + }) + cleanups.push(() => { + older.resolve([olderRow]) + newer.resolve([newerRow]) + return Promise.resolve() + }) + const olderSubset = { where: eq(new IR.PropRef([`category`]), `shared`) } + const newerSubset = { where: eq(new IR.PropRef([`id`]), `shared`) } + observe('scope-admission', { + distinct: + getLoadSubsetDemandKey(olderSubset) !== + getLoadSubsetDemandKey(newerSubset), + initialMatchesOlder: toBooleanPredicate( + compileSingleRowExpression(olderSubset.where)(initial), + ), + olderMatchesOlder: toBooleanPredicate( + compileSingleRowExpression(olderSubset.where)(olderRow), + ), + newerMatchesNewer: toBooleanPredicate( + compileSingleRowExpression(newerSubset.where)(newerRow), + ), + }) + let cut = 0 + const recordRow = () => + observe('row:' + cut++, { + name: collection.get(shared.id)?.name, + present: collection.has(shared.id), }) + if (existingOwner) { + await collection._sync.loadSubset(olderSubset) + recordRow() + } + const olderLoad = collection._sync.loadSubset( + existingOwner ? { ...olderSubset, refetch: true } : olderSubset, + ) + void Promise.resolve(olderLoad).catch(() => undefined) + const olderStarted = await boundary( + () => queryFn.mock.calls.length === (existingOwner ? 2 : 1), + ) + observe('older-started', queryFn.mock.calls.length) + if (!olderStarted) return + + if (directWrite === `between`) { + await collection.utils.writeUpdate(manual) + recordRow() + } + const newerLoad = collection._sync.loadSubset(newerSubset) + void Promise.resolve(newerLoad).catch(() => undefined) + const newerStarted = await boundary( + () => queryFn.mock.calls.length === (existingOwner ? 3 : 2), + ) + observe('newer-started', queryFn.mock.calls.length) + if (!newerStarted) return + observe( + 'physical-queries', + queryClient.getQueryCache().findAll({ queryKey: [id] }).length, + ) + if (directWrite === `after`) { + await collection.utils.writeUpdate(manual) + recordRow() + } - await vi.waitFor(() => expect(scanPersistedRows).toHaveBeenCalledOnce()) - expect(queryFn).toHaveBeenCalledOnce() - let model: ExplicitRefetchApplicationModel = { - calls: new Map(), - publicValues: new Map([[`query`, `none`]]), + const settle = async (owner: `older` | `newer`) => { + if (owner === `older`) { + older.resolve([olderRow]) + await olderLoad + } else { + newer.resolve([newerRow]) + await newerLoad } - model = reduceExplicitRefetchApplication(model, { - type: `start-call`, - callId: 1, - resultIds: [`query`], - throwOnError: true, - }) - const firstRefetch = collection.utils.refetch({ throwOnError: true }) - void firstRefetch.catch(() => {}) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(2) - expect(scanPersistedRows).toHaveBeenCalledTimes(2) - }) - model = reduceExplicitRefetchApplication(model, { - type: `accept-result`, - callId: 1, - resultId: `query`, - value: first.name, - }) + recordRow() + } + if (settleNewerFirst) { + await settle(`newer`) + await settle(`older`) + } else { + await settle(`older`) + await settle(`newer`) + } - model = reduceExplicitRefetchApplication(model, { - type: `start-call`, - callId: 2, - resultIds: [`query`], - throwOnError: true, - }) - const secondRefetch = collection.utils.refetch({ throwOnError: true }) - await vi.waitFor(() => { - expect(queryFn).toHaveBeenCalledTimes(3) - expect(collection.get(second.id)?.name).toBe(second.name) - }) - model = reduceExplicitRefetchApplication(model, { - type: `accept-result`, - callId: 2, - resultId: `query`, - value: second.name, - }) - model = reduceExplicitRefetchApplication(model, { - type: `retire-application`, - callId: 1, - resultId: `query`, - }) - model = reduceExplicitRefetchApplication(model, { - type: `fulfill-application`, - callId: 2, - resultId: `query`, - }) - await secondRefetch - expectExplicitRefetchObservation( - { - publicValues: { query: collection.get(second.id)?.name }, - refetch: `resolved`, - resultOrder: [`query`], - }, - observeExplicitRefetchApplication(model, 2), - ) + const release = (owner: `older` | `newer`) => { + const subset = owner === `older` ? olderSubset : newerSubset + collection._sync.unloadSubset(subset) + if (owner === `older` && existingOwner) + collection._sync.unloadSubset(subset) + recordRow() + } + if (existingOwner) { + release(`older`) + release(`newer`) + } else { + release(`newer`) + release(`older`) + } - firstRefetchScan.resolve([]) - await expect(firstRefetch).rejects.toMatchObject({ name: `AbortError` }) - expectExplicitRefetchObservation( - { - publicValues: { query: collection.get(second.id)?.name }, - refetch: `rejected`, - }, - observeExplicitRefetchApplication(model, 1), - ) - initialScan.resolve([]) + // Reacquisition starts a new row-ownership lifetime. Only the acquired + // Query's retained cache is projected; an earlier overlap does not + // permanently rewrite a different Query entry. This legal neighbor + // distinguishes continuous-owner precedence from global precedence. + if (!existingOwner && settleNewerFirst && directWrite === `none`) { + await collection._sync.loadSubset(olderSubset) + recordRow() + observe('older-reacquired-work', queryFn.mock.calls.length) + collection._sync.unloadSubset(olderSubset) + recordRow() + await collection._sync.loadSubset(newerSubset) + recordRow() + observe('newer-reacquired-work', queryFn.mock.calls.length) + } +} - await vi.waitFor(() => { - expect(itemIds(collection.toArray)).toEqual([second.id]) - }) - expect(publications).not.toContainEqual([initial.id]) - expect(publications).not.toContainEqual([first.id]) - }) +/** The extraction retains all 20 lifetime, 24 cross-Collection order, and + * seven overlap histories. The driver emits only Collection/cache/work facts; + * models compute all expectations from authored actions before the check. */ +oracleProperty({ + id: 'query-db.ownership.direct-lifetime', + law: { + statement: + 'Direct delete ends fetched ownership; reinsert/upsert starts an unowned lifetime, while update preserves existing claims.', + authority: + 'docs/collections/query-collection.md#direct-writes-and-query-sync', + limits: + 'One key, one/two subset claims, optional metadata, and the five documented direct lifetime operations.', + owner: 'Query DB ownership lifecycle', + }, + histories: lifetimeHistories, + model: lifetimeModel, + driver: async (h, observe) => { + try { + await runDirectRowLifetimeHistory(h, observe) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'direct row lifetime'), +}) +oracleProperty({ + id: 'query-db.ownership.direct-order', + law: { + statement: + 'A later physical fetch may replace and claim a direct row; an earlier fetch or foreign cache edit cannot establish newer source authority.', + authority: + 'docs/collections/query-collection.md#direct-writes-and-query-sync', + limits: + 'Two on-demand Collections share one exact Query; one extra same/other key write, optional cache edit and final refetch.', + owner: 'Query DB ownership lifecycle', + }, + histories: directOrderHistories, + model: directOrderModel, + driver: async (h, observe) => { + try { + await runDirectWriteOrderHistory(h, observe) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'direct fetch order'), +}) +oracleProperty({ + id: 'query-db.ownership.overlap', + law: { + statement: + 'Continuously owned overlapping rows take the latest started accepted fetch or direct write; releasing a newer owner does not revert their values.', + authority: + 'docs/collections/query-collection.md#direct-writes-and-query-sync', + limits: + 'Two exact predicates, one shared key, seven legal histories with both settlement orders, matching value, and neighboring direct-write positions.', + owner: 'Query DB ownership lifecycle', + }, + histories: overlapHistories, + model: overlapModel, + driver: async (h, observe) => { + try { + await runOverlappingSubsetHistory(h, observe) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'overlapping fetch values'), }) diff --git a/packages/query-db-collection/tests/ownership-model-oracle.ts b/packages/query-db-collection/tests/ownership-model-oracle.ts new file mode 100644 index 0000000000..805b166df5 --- /dev/null +++ b/packages/query-db-collection/tests/ownership-model-oracle.ts @@ -0,0 +1,868 @@ +/** + * Independent Query ownership models shared by production refinements and + * ordinary model/checker examples. These reducers retain the original call, + * application, handler, identity and result-settlement laws verbatim. Their + * actions abstract authored Query completion and Collection application events; + * they neither read production classifications nor decide source timing. + */ +import { expect } from 'vitest' + +export type ResultSettlementOperationState = + | { phase: `waiting-for-result` } + | { phase: `waiting-for-refresh`; barrier: string } + | { + phase: `terminal` + outcome: `applied` | `rejected` | `cancelled` + rowCount?: 0 | 1 + } + +export type ResultSettlementModelState = ReadonlyMap< + string, + ResultSettlementOperationState +> + +/** + * `query-succeeded` and `refresh-succeeded` compress the result lifecycle. + * With an applicable result and no `deferOn`, each action combines Query fetch + * fulfillment, completed Collection application and its applied settlement, + * and the terminal public-refetch checkpoint. + * A `deferOn` action instead records the skipped result and its replacement wait; + * a nondeferred `invalid-shape` result records application rejection. The model + * omits intermediate application states, which the separate obligation models + * below retain. + */ +export type ResultSettlementModelAction = + | { type: `start-refetch`; operation: string } + | { + type: `query-succeeded` + operation: string + result: `applicable` | `invalid-shape` + rowCount?: 0 | 1 + deferOn?: string + } + | { + type: `refresh-succeeded` + barrier: string + result: `applicable` | `invalid-shape` + rowCount?: 0 | 1 + deferOn?: string + } + | { type: `retire` } + +export type PublicRefetchObservation = + | { settled: false } + | { settled: true; outcome: `fulfilled` | `rejected`; rowCount?: number } + +export function advanceResultSettlementModel( + state: ResultSettlementModelState, + action: ResultSettlementModelAction, +): ResultSettlementModelState { + const next = new Map(state) + + if (action.type === `start-refetch`) { + if (next.has(action.operation)) { + throw new Error(`A public refetch operation must be unique`) + } + next.set(action.operation, { phase: `waiting-for-result` }) + return next + } + + if (action.type === `retire`) { + next.forEach((operation, id) => { + if (operation.phase !== `terminal`) { + next.set(id, { phase: `terminal`, outcome: `cancelled` }) + } + }) + return next + } + + if (action.type === `refresh-succeeded`) { + const waiting = [...next].filter( + ([, operation]) => + operation.phase === `waiting-for-refresh` && + operation.barrier === action.barrier, + ) + if (waiting.length === 0) { + throw new Error(`A replacement result requires its defer generation`) + } + waiting.forEach(([id]) => { + next.set( + id, + action.deferOn + ? { phase: `waiting-for-refresh`, barrier: action.deferOn } + : action.result === `applicable` + ? { + phase: `terminal`, + outcome: `applied`, + rowCount: action.rowCount, + } + : { phase: `terminal`, outcome: `rejected` }, + ) + }) + return next + } + + const operation = next.get(action.operation) + if (operation?.phase !== `waiting-for-result`) { + throw new Error(`A Query result requires one waiting public refetch`) + } + next.set( + action.operation, + action.deferOn + ? { phase: `waiting-for-refresh`, barrier: action.deferOn } + : action.result === `applicable` + ? { + phase: `terminal`, + outcome: `applied`, + rowCount: action.rowCount, + } + : { phase: `terminal`, outcome: `rejected` }, + ) + return next +} + +export function startResultSettlementModel( + operation: string, +): ResultSettlementModelState { + return advanceResultSettlementModel(new Map(), { + type: `start-refetch`, + operation, + }) +} + +export function expectPublicRefetchObservation( + state: ResultSettlementModelState, + operationId: string, + observation: PublicRefetchObservation, +): void { + const operation = state.get(operationId) + if (!operation) throw new Error(`Unknown public refetch operation`) + + if (operation.phase !== `terminal`) { + expect(observation).toEqual({ settled: false }) + return + } + + expect(observation).toEqual( + operation.outcome === `applied` + ? { + settled: true, + outcome: `fulfilled`, + ...(operation.rowCount === undefined + ? {} + : { rowCount: operation.rowCount }), + } + : { settled: true, outcome: `rejected` }, + ) +} + +/** + * `collection.utils.refetch()` is normally an application barrier. The public + * contract is `docs/collections/query-collection.md#controlling-refetch-behavior`: + * when no user mutation is persisting and no mutation handler is active, + * successful fulfillment waits for every result accepted for Collection + * application, including an application with no row diff. Rejection is + * fail-fast and may leave independent obligations pending. While publication + * is blocked by a persisting mutation or active handler, every call uses the + * Query fetch boundary, including an overlapping external call. + * + * The handler parameter and every matching mutation alias preserve the real + * external Collection identity. This keeps strict equality and identity-keyed + * registries valid inside shared persistence handlers. Handler activity keeps + * refetch at the Query fetch boundary without replacing those public objects. + * The same acyclic fetch boundary applies when a manual transaction, + * `createOptimisticAction`, or a direct handler awaits a captured Collection: + * those documented forms still cannot wait on publication blocked by their + * own transaction. The phase law applies in both start orders. A refresh that + * begins first must change to the fetch boundary if a mutation starts before + * its result application can publish. + * + * Model mapping: + * - `callId` and `resultId` are model-only identities for one public call and + * its ordered Query results; they are not production generation counters. + * - `accept-result` means the observer result has an exact application promise. + * - `fulfill-application` and `reject-application` mean that promise settled. + * - `retire-application` means cancellation or supersession won before the + * application no-cancel point and therefore rejects that exact obligation. + * - `publicValues` represents Collection rows at the public checkpoint. + * - `RefetchCallSettlementModel` splits each tracked Query fetch from the zero + * or more Collection applications it causes. Same-key background work is a + * separate source even when it occurs while that fetch is active. + * - `HandlerRefetchBoundaryModel` excludes the causally queued Collection + * application from the handler call while retaining its later error record. + * - `HandlerIdentityObservation` is a model-only projection of strict + * transaction, mutation, and Collection identity plus identity-keyed + * registry results. Its bounded grammar crosses insert, update, and delete + * through handler parameter and mutation-alias access during explicit refetch. + * Historical operation labels are replay coordinates only, not model state. + * - `MutationRefreshLivenessModel` is a wait-for graph. Its mutation, fetch, + * and application nodes map to the user transaction, Query promise, and + * Collection publication boundary. Invocation labels test both owned and + * overlapping external calls but do not affect the phase-based law. + * - `QueryCacheWorkModel` treats Query ownership as the only edge authorizing + * Collection cache-listener work. The driver subtracts Query Core's own + * microtask baseline before comparing that bounded diagnostic. + * - `FetchRecordLifecycleModel` gives a terminally cancelled fetch no live + * causal record. The driver observes retained state as extra cleanup work at + * the next fetch without using production record classification. + * + * Results skipped before application because refresh is deferred, or because a + * manual-write snapshot is unchanged, are outside this accepted-result model. + * Invalid query shapes remain the input-validation contract in `query.test.ts`. + * A successful tracked result is otherwise admitted even when its on-demand + * Query has no listeners. A Query removed after the call snapshot retains an + * `undefined` result slot rather than turning lifecycle churn into a TypeError. + * + * State minimality: result order is observable in the returned array; pending + * result identity and causal source control call settlement; accepted values + * control publication; `throwOnError` distinguishes rejection from suppressed + * failure; separate calls retain the outcome of a superseded caller. Handler + * Collection identity is observed through the parameter, matching mutation + * aliases, and identity-keyed registries. Direct Collection handlers own one + * Collection, so unrelated multi-Collection mutation aliases are outside this + * driver. + * + * Embedded ORC-012 review record for this extension: ORC-001 through 006 and + * ORC-008 through 010 apply and are evidenced by this prose, independent + * reducers, bounded grammar with an exhaustiveness assertion, real drivers, + * observation helpers, wrong-result controls, state explanation, vocabulary + * mapping, and aggregate cleanup. ORC-007 does not apply because this bounded + * grammar is not a generated campaign. ORC-011 applies to the shared deadlock + * claim: the wait-for-graph formulation is structurally separate from the + * production drivers and from the earlier result/application ledger. This + * executable comment records every ORC-001 through ORC-011 outcome; no + * verdict-critical evidence lives only in the PR description. + */ +export type RefetchApplicationOutcome = `pending` | `resolved` | `rejected` + +export type ExplicitRefetchCallModel = { + resultOrder: Array + pendingResultIds: Set + acceptedValues: Map + throwOnError: boolean + outcome: RefetchApplicationOutcome +} + +export type ExplicitRefetchApplicationModel = { + calls: Map + publicValues: Map +} + +export type ExplicitRefetchApplicationAction = + | { + type: `start-call` + callId: number + resultIds: Array + throwOnError: boolean + } + | { + type: `accept-result` + callId: number + resultId: string + value: string + } + | { type: `fulfill-application`; callId: number; resultId: string } + | { type: `reject-application`; callId: number; resultId: string } + | { type: `retire-application`; callId: number; resultId: string } + +export type ExplicitRefetchObservation = { + publicValues: Record + refetch: RefetchApplicationOutcome + resultOrder?: Array +} + +export function reduceExplicitRefetchApplication( + model: ExplicitRefetchApplicationModel, + action: ExplicitRefetchApplicationAction, +): ExplicitRefetchApplicationModel { + if (action.type === `start-call`) { + if (model.calls.has(action.callId)) { + throw new Error(`Refetch call ${action.callId} already exists`) + } + if (new Set(action.resultIds).size !== action.resultIds.length) { + throw new Error( + `A refetch call cannot contain duplicate result identities`, + ) + } + const calls = new Map(model.calls) + calls.set(action.callId, { + resultOrder: [...action.resultIds], + pendingResultIds: new Set(action.resultIds), + acceptedValues: new Map(), + throwOnError: action.throwOnError, + outcome: action.resultIds.length === 0 ? `resolved` : `pending`, + }) + return { ...model, calls } + } + + const call = model.calls.get(action.callId) + if (!call) throw new Error(`Unknown refetch call ${action.callId}`) + if (!call.resultOrder.includes(action.resultId)) { + throw new Error(`Unknown result ${action.resultId}`) + } + if (!call.pendingResultIds.has(action.resultId)) { + throw new Error(`Result ${action.resultId} already settled`) + } + + if (action.type === `accept-result`) { + if (call.acceptedValues.has(action.resultId)) { + throw new Error(`Result ${action.resultId} already accepted`) + } + const calls = new Map(model.calls) + calls.set(action.callId, { + ...call, + acceptedValues: new Map(call.acceptedValues).set( + action.resultId, + action.value, + ), + }) + return { ...model, calls } + } + + const acceptedValue = call.acceptedValues.get(action.resultId) + if (acceptedValue === undefined) { + throw new Error(`Cannot settle a result before it is accepted`) + } + + const pendingResultIds = new Set(call.pendingResultIds) + pendingResultIds.delete(action.resultId) + const calls = new Map(model.calls) + if (action.type === `fulfill-application`) { + const publicValues = new Map(model.publicValues).set( + action.resultId, + acceptedValue, + ) + calls.set(action.callId, { + ...call, + pendingResultIds, + outcome: + call.outcome === `rejected` + ? `rejected` + : pendingResultIds.size === 0 + ? `resolved` + : `pending`, + }) + return { calls, publicValues } + } + + calls.set(action.callId, { + ...call, + pendingResultIds, + outcome: call.throwOnError + ? `rejected` + : pendingResultIds.size === 0 + ? `resolved` + : `pending`, + }) + return { ...model, calls } +} + +export function observeExplicitRefetchApplication( + model: ExplicitRefetchApplicationModel, + callId: number, +): ExplicitRefetchObservation { + const call = model.calls.get(callId) + if (!call) throw new Error(`Unknown refetch call ${callId}`) + return { + publicValues: Object.fromEntries( + call.resultOrder.map((resultId) => [ + resultId, + model.publicValues.get(resultId), + ]), + ), + refetch: call.outcome, + ...(call.outcome === `resolved` + ? { resultOrder: call.resultOrder } + : undefined), + } +} + +export function expectExplicitRefetchObservation( + actual: ExplicitRefetchObservation, + expected: ExplicitRefetchObservation, +): void { + expect(actual).toEqual(expected) +} + +/** + * This model-only call ledger separates Query fetch settlement from each + * causally owned Collection application. Same-key background applications stay + * observable to the model but do not become call obligations. A successful + * call waits for every owned fetch and application obligation. A throwing call + * rejects at its first owned failure; other obligations may remain pending. + * This is the `Promise.all` aggregate promised by the public utility, not an + * all-settled barrier. + */ +export type RefetchCallSettlementModel = { + queryOrder: Array + fetches: Map + applications: Map< + string, + { + queryId: string + outcome: `pending` | `fulfilled` | `rejected` + source: `call` | `background` + } + > + throwOnError: boolean +} + +export type RefetchCallSettlementAction = + | { + type: `accept-application` + queryId: string + applicationId: string + source?: `call` | `background` + } + | { + type: `settle-fetch` + queryId: string + outcome: `fulfilled` | `rejected` + } + | { type: `remove-query`; queryId: string } + | { + type: `settle-application` + applicationId: string + outcome: `fulfilled` | `rejected` + } + +export function createRefetchCallSettlementModel( + queryOrder: Array, + throwOnError: boolean, +): RefetchCallSettlementModel { + if (new Set(queryOrder).size !== queryOrder.length) { + throw new Error(`A refetch call cannot contain duplicate Query identities`) + } + return { + queryOrder: [...queryOrder], + fetches: new Map(queryOrder.map((queryId) => [queryId, `pending`])), + applications: new Map(), + throwOnError, + } +} + +export function reduceRefetchCallSettlement( + model: RefetchCallSettlementModel, + action: RefetchCallSettlementAction, +): RefetchCallSettlementModel { + if (action.type === `accept-application`) { + if (model.fetches.get(action.queryId) !== `pending`) { + throw new Error(`Applications belong to an active Query fetch`) + } + if (model.applications.has(action.applicationId)) { + throw new Error(`Application ${action.applicationId} already exists`) + } + return { + ...model, + applications: new Map(model.applications).set(action.applicationId, { + queryId: action.queryId, + outcome: `pending`, + source: action.source ?? `call`, + }), + } + } + + if (action.type === `remove-query`) { + if (model.fetches.get(action.queryId) !== `pending`) { + throw new Error(`Query ${action.queryId} is not pending removal`) + } + return { + ...model, + fetches: new Map(model.fetches).set(action.queryId, `removed`), + } + } + + if (action.type === `settle-fetch`) { + if (model.fetches.get(action.queryId) !== `pending`) { + throw new Error(`Query fetch ${action.queryId} already settled`) + } + return { + ...model, + fetches: new Map(model.fetches).set(action.queryId, action.outcome), + } + } + + const application = model.applications.get(action.applicationId) + if (!application) { + throw new Error(`Unknown application ${action.applicationId}`) + } + if (application.outcome !== `pending`) { + throw new Error(`Application ${action.applicationId} already settled`) + } + return { + ...model, + applications: new Map(model.applications).set(action.applicationId, { + ...application, + outcome: action.outcome, + }), + } +} + +export function observeRefetchCallSettlement( + model: RefetchCallSettlementModel, +): { + refetch: RefetchApplicationOutcome + resultOrder?: Array +} { + const fetchOutcomes = [...model.fetches.values()] + const applicationOutcomes = [...model.applications.values()] + .filter(({ source }) => source === `call`) + .map(({ outcome }) => outcome) + if ( + model.throwOnError && + [...fetchOutcomes, ...applicationOutcomes].includes(`rejected`) + ) { + return { refetch: `rejected` } + } + if ( + fetchOutcomes.includes(`pending`) || + applicationOutcomes.includes(`pending`) + ) { + return { refetch: `pending` } + } + return { + refetch: `resolved`, + resultOrder: model.queryOrder.map((queryId) => + model.fetches.get(queryId) === `removed` ? undefined : queryId, + ), + } +} + +/** + * A handler-phase refetch has one Query fetch obligation. Its accepted + * Collection application remains outside the handler boundary because that + * application is queued behind the invoking mutation transaction. A later + * application failure is recorded by Collection error utilities; it cannot + * retroactively change the already-settled handler call. + */ +export type HandlerRefetchBoundaryModel = { + fetch: `pending` | `fulfilled` | `rejected` + application: `unaccepted` | `pending` | `fulfilled` | `rejected` + throwOnError: boolean +} + +export type HandlerRefetchBoundaryAction = + | { type: `accept-application` } + | { type: `settle-fetch`; outcome: `fulfilled` | `rejected` } + | { type: `settle-application`; outcome: `fulfilled` | `rejected` } + +export function reduceHandlerRefetchBoundary( + model: HandlerRefetchBoundaryModel, + action: HandlerRefetchBoundaryAction, +): HandlerRefetchBoundaryModel { + if (action.type === `accept-application`) { + if (model.application !== `unaccepted`) { + throw new Error(`Handler application already accepted`) + } + return { ...model, application: `pending` } + } + if (action.type === `settle-fetch`) { + if (model.fetch !== `pending`) { + throw new Error(`Handler Query fetch already settled`) + } + return { ...model, fetch: action.outcome } + } + if (model.application !== `pending`) { + throw new Error(`Handler application is not pending`) + } + return { ...model, application: action.outcome } +} + +export function observeHandlerRefetchBoundary( + model: HandlerRefetchBoundaryModel, +): { + refetch: RefetchApplicationOutcome + applicationErrorRecorded: boolean +} { + return { + refetch: + model.fetch === `pending` + ? `pending` + : model.fetch === `rejected` && model.throwOnError + ? `rejected` + : `resolved`, + applicationErrorRecorded: model.application === `rejected`, + } +} + +/** + * A refresh started while a mutation is persisting cannot wait for publication + * that the transaction prevents. This law comes from the documented + * `createOptimisticAction` protocol in `packages/db/src/optimistic-action.ts` + * and `docs/guides/mutations.md`: a mutation function may await Query + * `refetch()` before it returns, while its optimistic transaction remains + * active until that function returns. + * + * This reference model is a wait-for graph, not a copy of Query Collection + * branches. `mutation` is the user transaction, `fetch` is the Query boundary, + * and `application` is publication into Collection rows. During that phase the + * refresh depends on `fetch`, regardless of which Collection reference invoked + * it. When the mutation owns the refresh, an `application` dependency would + * create the forbidden cycle mutation -> refresh -> application -> mutation; + * for an external caller it would add an unrelated persistence delay. + */ +/** + * Before the RC, these matrices exercised refetch and the removed clearError + * utility. Both paths now explicitly refetch. Keep the former operation labels + * only as replay coordinates: existing traces, fixture IDs, and all 12 runs of + * each matrix survive. The labels add no semantic history or model distinction. + * To select the former clearError rows, use the test-name pattern + * `legacy replay:.*clearError`; Vitest can quote interpolated string values. + */ +export type LegacyRefreshReplayCoordinate = `refetch` | `clearError` +export const legacyRefreshReplayCoordinates = [`refetch`, `clearError`] as const + +export type LegacyRefreshReplay = { + legacyOperation: LegacyRefreshReplayCoordinate +} + +export type MutationRefreshInvocation = + | `manual-transaction` + | `optimistic-action` + | `captured-handler-collection` + | `external-handler-overlap` + | `external-persisted-application-overlap` + +export type MutationRefreshSchedule = + | `mutation-first` + | `refresh-first-during-fetch` + | `refresh-first-during-application` + +export type MutationRefreshHistory = { + invocation: MutationRefreshInvocation + schedule: MutationRefreshSchedule +} + +export const mutationRefreshInvocations = [ + `manual-transaction`, + `optimistic-action`, + `captured-handler-collection`, + `external-handler-overlap`, + `external-persisted-application-overlap`, +] as const +export const mutationRefreshHistoryGrammar: ReadonlyArray< + MutationRefreshHistory & LegacyRefreshReplay +> = [ + ...mutationRefreshInvocations + .filter( + (invocation) => + invocation !== `external-handler-overlap` && + invocation !== `external-persisted-application-overlap`, + ) + .flatMap((invocation) => + legacyRefreshReplayCoordinates.map((legacyOperation) => ({ + invocation, + legacyOperation, + schedule: `mutation-first` as const, + })), + ), + ...legacyRefreshReplayCoordinates.flatMap((legacyOperation) => + ([`mutation-first`, `refresh-first-during-fetch`] as const).map( + (schedule) => ({ + invocation: `external-handler-overlap` as const, + legacyOperation, + schedule, + }), + ), + ), + ...legacyRefreshReplayCoordinates.map((legacyOperation) => ({ + invocation: `external-persisted-application-overlap` as const, + legacyOperation, + schedule: `refresh-first-during-application` as const, + })), +] + +export type MutationRefreshLivenessModel = { + fetch: `pending` | `fulfilled` | `rejected` + application: `unaccepted` | `pending` | `fulfilled` | `rejected` + boundary: `fetch` | `application` + throwOnError: boolean +} + +export type MutationRefreshLivenessAction = + | { type: `accept-application` } + | { type: `settle-fetch`; outcome: `fulfilled` | `rejected` } + | { type: `settle-application`; outcome: `fulfilled` | `rejected` } + +export function reduceMutationRefreshLiveness( + model: MutationRefreshLivenessModel, + action: MutationRefreshLivenessAction, +): MutationRefreshLivenessModel { + if (action.type === `accept-application`) { + if (model.application !== `unaccepted`) { + throw new Error(`Mutation refresh application already accepted`) + } + return { ...model, application: `pending` } + } + if (action.type === `settle-fetch`) { + if (model.fetch !== `pending`) { + throw new Error(`Mutation refresh fetch already settled`) + } + return { ...model, fetch: action.outcome } + } + if (model.application !== `pending`) { + throw new Error(`Mutation refresh application is not pending`) + } + return { ...model, application: action.outcome } +} + +export function observeMutationRefreshLiveness( + model: MutationRefreshLivenessModel, +): { + refresh: RefetchApplicationOutcome + application: `unaccepted` | RefetchApplicationOutcome +} { + const refresh = + model.fetch === `pending` + ? `pending` + : model.fetch === `rejected` && model.throwOnError + ? `rejected` + : model.boundary === `application` && model.application === `pending` + ? `pending` + : model.boundary === `application` && + model.application === `rejected` && + model.throwOnError + ? `rejected` + : `resolved` + return { + refresh, + application: + model.application === `unaccepted` + ? `unaccepted` + : model.application === `pending` + ? `pending` + : model.application === `fulfilled` + ? `resolved` + : `rejected`, + } +} + +/** + * Query-cache observation is an ownership boundary. A Collection may schedule + * bookkeeping for a Query it owns, but a foreign Query has no modeled edge to + * that Collection and therefore contributes no Collection microtasks. The + * production observation subtracts Query Core's own baseline work, leaving + * only work introduced by registered Query Collections. + */ +export type QueryCacheWorkModel = { + collectionCount: number + queryOwnership: `owned` | `foreign` +} + +export function observeQueryCacheWork(model: QueryCacheWorkModel): { + collectionMicrotasks: number +} { + return { + collectionMicrotasks: + model.queryOwnership === `owned` ? model.collectionCount : 0, + } +} + +export type FetchRecordLifecycleModel = { + state: `active` | `cancelled` +} + +export function observeFetchRecordSuccessorWork( + model: FetchRecordLifecycleModel, +): { + extraCleanupMicrotasks: number +} { + return { + extraCleanupMicrotasks: model.state === `active` ? 1 : 0, + } +} + +export type MutationHandlerKind = `insert` | `update` | `delete` +export type HandlerCollectionAccessPath = `parameter` | `mutation-alias` + +export type HandlerCollectionHistory = { + handlerKind: MutationHandlerKind + accessPath: HandlerCollectionAccessPath +} + +export type HandlerIdentityObservation = HandlerCollectionHistory & { + matchingAliasCount: number + transactionIsExternal: boolean + allMutationsPreserveIdentity: boolean + parameterIsExternal: boolean + allAliasesMatchParameter: boolean + anyAliasMatchesExternal: boolean + externalMapMatchesAnyAlias: boolean + parameterMapMatchesAllAliases: boolean + externalWeakMapMatchesAnyAlias: boolean + parameterWeakMapMatchesAllAliases: boolean +} + +export const mutationHandlerKinds = [`insert`, `update`, `delete`] as const +export const handlerCollectionAccessPaths = [ + `parameter`, + `mutation-alias`, +] as const +export const handlerCollectionHistoryGrammar: ReadonlyArray< + HandlerCollectionHistory & LegacyRefreshReplay +> = mutationHandlerKinds.flatMap((handlerKind) => + handlerCollectionAccessPaths.flatMap((accessPath) => + legacyRefreshReplayCoordinates.map((legacyOperation) => ({ + handlerKind, + accessPath, + legacyOperation, + })), + ), +) + +/** + * A direct mutation handler receives the real Collection. Every matching + * mutation alias preserves that identity. Strict equality and registries keyed + * by the external or parameter reference therefore select the same mutations. + * The model records only this public equality relation. + */ +export function observeHandlerIdentity( + history: HandlerCollectionHistory, + matchingAliasCount: number, +): HandlerIdentityObservation { + if (matchingAliasCount < 1) { + throw new Error(`A direct handler requires a matching mutation alias`) + } + return { + handlerKind: history.handlerKind, + accessPath: history.accessPath, + matchingAliasCount, + transactionIsExternal: true, + allMutationsPreserveIdentity: true, + parameterIsExternal: true, + allAliasesMatchParameter: true, + anyAliasMatchesExternal: true, + externalMapMatchesAnyAlias: true, + parameterMapMatchesAllAliases: true, + externalWeakMapMatchesAnyAlias: true, + parameterWeakMapMatchesAllAliases: true, + } +} + +export function expectHandlerIdentityObservation( + actual: HandlerIdentityObservation, + expected: HandlerIdentityObservation, +): void { + expect(actual).toEqual(expected) +} + +export function checkPostHandlerRefetch( + model: RefetchCallSettlementModel, + observed: { + refetch: ReturnType + rows: string + waiterRows: string | undefined + }, + initial: string, + confirmed: string, +): void { + const expected = observeRefetchCallSettlement(model) + expect(observed.refetch).toEqual(expected) + if (expected.refetch === `pending`) { + expect([initial, confirmed]).toContain(observed.rows) + expect(observed.waiterRows).toBeUndefined() + } else { + expect(observed.rows).toBe(confirmed) + expect(observed.waiterRows).toBe(confirmed) + } +} diff --git a/packages/query-db-collection/tests/ownership-models.test.ts b/packages/query-db-collection/tests/ownership-models.test.ts new file mode 100644 index 0000000000..bd786ab9fc --- /dev/null +++ b/packages/query-db-collection/tests/ownership-models.test.ts @@ -0,0 +1,571 @@ +/** Ordinary examples of the independent model and its checker. These retain + * every prior concrete expectation without presenting model-only checks as + * production refinements or charging them as generated-history reach. */ +import { expect, it } from 'vitest' +import { + advanceResultSettlementModel, + checkPostHandlerRefetch, + createRefetchCallSettlementModel, + expectExplicitRefetchObservation, + expectHandlerIdentityObservation, + expectPublicRefetchObservation, + handlerCollectionHistoryGrammar, + mutationRefreshHistoryGrammar, + observeExplicitRefetchApplication, + observeHandlerIdentity, + observeHandlerRefetchBoundary, + observeMutationRefreshLiveness, + observeRefetchCallSettlement, + reduceExplicitRefetchApplication, + reduceMutationRefreshLiveness, + reduceRefetchCallSettlement, + startResultSettlementModel, +} from './ownership-model-oracle' +import type { + ExplicitRefetchApplicationModel, + ExplicitRefetchObservation, + HandlerIdentityObservation, + HandlerRefetchBoundaryModel, + MutationRefreshLivenessModel, +} from './ownership-model-oracle' + +it.each([`early-receipt`, `partial-rows`, `stale-waiter`] as const)( + `rejects post-handler wrong observation: %s`, + (fault) => { + let model = createRefetchCallSettlementModel([`query`], true) + model = reduceRefetchCallSettlement(model, { + type: `accept-application`, + queryId: `query`, + applicationId: `later-result`, + }) + model = reduceRefetchCallSettlement(model, { + type: `settle-fetch`, + queryId: `query`, + outcome: `fulfilled`, + }) + if (fault === `stale-waiter`) + model = reduceRefetchCallSettlement(model, { + type: `settle-application`, + applicationId: `later-result`, + outcome: `fulfilled`, + }) + const actual = { + refetch: observeRefetchCallSettlement(model), + rows: `confirmed`, + waiterRows: fault === `stale-waiter` ? `confirmed` : undefined, + } + checkPostHandlerRefetch(model, actual, `initial`, `confirmed`) + if (fault === `early-receipt`) + actual.refetch = { refetch: `resolved`, resultOrder: [`query`] } + if (fault === `partial-rows`) actual.rows = `partial` + if (fault === `stale-waiter`) actual.waiterRows = `initial` + expect(() => + checkPostHandlerRefetch(model, actual, `initial`, `confirmed`), + ).toThrow() + }, +) + +it(`keeps an accepted empty-diff result pending until application`, () => { + const initial: ExplicitRefetchApplicationModel = { + calls: new Map(), + publicValues: new Map([[`query`, `same`]]), + } + const started = reduceExplicitRefetchApplication(initial, { + type: `start-call`, + callId: 1, + resultIds: [`query`], + throwOnError: true, + }) + const accepted = reduceExplicitRefetchApplication(started, { + type: `accept-result`, + callId: 1, + resultId: `query`, + value: `same`, + }) + + expectExplicitRefetchObservation( + observeExplicitRefetchApplication(accepted, 1), + { + publicValues: { query: `same` }, + refetch: `pending`, + }, + ) + const fulfilled = reduceExplicitRefetchApplication(accepted, { + type: `fulfill-application`, + callId: 1, + resultId: `query`, + }) + expectExplicitRefetchObservation( + observeExplicitRefetchApplication(fulfilled, 1), + { + publicValues: { query: `same` }, + refetch: `resolved`, + resultOrder: [`query`], + }, + ) +}) + +it(`retains separate outcomes for a retired caller and its successor`, () => { + let model: ExplicitRefetchApplicationModel = { + calls: new Map(), + publicValues: new Map([[`query`, `initial`]]), + } + model = reduceExplicitRefetchApplication(model, { + type: `start-call`, + callId: 1, + resultIds: [`query`], + throwOnError: true, + }) + model = reduceExplicitRefetchApplication(model, { + type: `accept-result`, + callId: 1, + resultId: `query`, + value: `first`, + }) + model = reduceExplicitRefetchApplication(model, { + type: `start-call`, + callId: 2, + resultIds: [`query`], + throwOnError: true, + }) + model = reduceExplicitRefetchApplication(model, { + type: `accept-result`, + callId: 2, + resultId: `query`, + value: `second`, + }) + model = reduceExplicitRefetchApplication(model, { + type: `retire-application`, + callId: 1, + resultId: `query`, + }) + + expectExplicitRefetchObservation( + observeExplicitRefetchApplication(model, 1), + { + publicValues: { query: `initial` }, + refetch: `rejected`, + }, + ) + expectExplicitRefetchObservation( + observeExplicitRefetchApplication(model, 2), + { + publicValues: { query: `initial` }, + refetch: `pending`, + }, + ) + + model = reduceExplicitRefetchApplication(model, { + type: `fulfill-application`, + callId: 2, + resultId: `query`, + }) + expectExplicitRefetchObservation( + observeExplicitRefetchApplication(model, 2), + { + publicValues: { query: `second` }, + refetch: `resolved`, + resultOrder: [`query`], + }, + ) +}) + +it(`rejects a fetch-boundary-only wrong-result control`, () => { + let model: ExplicitRefetchApplicationModel = { + calls: new Map(), + publicValues: new Map([[`query`, `Optimistic`]]), + } + model = reduceExplicitRefetchApplication(model, { + type: `start-call`, + callId: 1, + resultIds: [`query`], + throwOnError: true, + }) + model = reduceExplicitRefetchApplication(model, { + type: `accept-result`, + callId: 1, + resultId: `query`, + value: `Authoritative`, + }) + const expected = observeExplicitRefetchApplication(model, 1) + const fetchBoundaryOnly: ExplicitRefetchObservation = { + publicValues: { query: `Optimistic` }, + refetch: `pending`, + resultOrder: [`query`], + } + fetchBoundaryOnly.refetch = `resolved` + + expect(() => + expectExplicitRefetchObservation(fetchBoundaryOnly, expected), + ).toThrow() + expect(() => + expectExplicitRefetchObservation(expected, expected), + ).not.toThrow() +}) + +it(`rejects illegal explicit-refetch model histories`, () => { + const started = reduceExplicitRefetchApplication( + { calls: new Map(), publicValues: new Map() }, + { + type: `start-call`, + callId: 1, + resultIds: [`query`], + throwOnError: true, + }, + ) + const accepted = reduceExplicitRefetchApplication(started, { + type: `accept-result`, + callId: 1, + resultId: `query`, + value: `accepted`, + }) + + expect(() => + reduceExplicitRefetchApplication(started, { + type: `start-call`, + callId: 1, + resultIds: [`replacement`], + throwOnError: true, + }), + ).toThrow(`already exists`) + expect(() => + reduceExplicitRefetchApplication(accepted, { + type: `accept-result`, + callId: 1, + resultId: `query`, + value: `replacement`, + }), + ).toThrow(`already accepted`) + expect(() => + reduceExplicitRefetchApplication(accepted, { + type: `fulfill-application`, + callId: 1, + resultId: `unknown`, + }), + ).toThrow(`Unknown result`) + const fulfilled = reduceExplicitRefetchApplication(accepted, { + type: `fulfill-application`, + callId: 1, + resultId: `query`, + }) + expect(() => + reduceExplicitRefetchApplication(fulfilled, { + type: `fulfill-application`, + callId: 1, + resultId: `query`, + }), + ).toThrow(`already settled`) +}) + +it(`keeps a suppressed fetch failure pending for its accepted application`, () => { + let model = createRefetchCallSettlementModel([`query`], false) + model = reduceRefetchCallSettlement(model, { + type: `accept-application`, + queryId: `query`, + applicationId: `cached-result`, + }) + model = reduceRefetchCallSettlement(model, { + type: `settle-fetch`, + queryId: `query`, + outcome: `rejected`, + }) + expect(observeRefetchCallSettlement(model)).toEqual({ + refetch: `pending`, + }) + + model = reduceRefetchCallSettlement(model, { + type: `settle-application`, + applicationId: `cached-result`, + outcome: `fulfilled`, + }) + expect(observeRefetchCallSettlement(model)).toEqual({ + refetch: `resolved`, + resultOrder: [`query`], + }) +}) + +it(`excludes unrelated background applications from a refetch call`, () => { + let model = createRefetchCallSettlementModel([`query`], true) + model = reduceRefetchCallSettlement(model, { + type: `accept-application`, + queryId: `query`, + applicationId: `own-result`, + }) + model = reduceRefetchCallSettlement(model, { + type: `accept-application`, + queryId: `query`, + applicationId: `background-result`, + source: `background`, + }) + model = reduceRefetchCallSettlement(model, { + type: `settle-application`, + applicationId: `own-result`, + outcome: `fulfilled`, + }) + model = reduceRefetchCallSettlement(model, { + type: `settle-fetch`, + queryId: `query`, + outcome: `fulfilled`, + }) + model = reduceRefetchCallSettlement(model, { + type: `settle-application`, + applicationId: `background-result`, + outcome: `rejected`, + }) + + expect(observeRefetchCallSettlement(model)).toEqual({ + refetch: `resolved`, + resultOrder: [`query`], + }) +}) + +it(`preserves the result slot of a Query removed during refetch startup`, () => { + let model = createRefetchCallSettlementModel([`first`, `second`], true) + model = reduceRefetchCallSettlement(model, { + type: `accept-application`, + queryId: `first`, + applicationId: `first-result`, + }) + model = reduceRefetchCallSettlement(model, { + type: `remove-query`, + queryId: `second`, + }) + model = reduceRefetchCallSettlement(model, { + type: `settle-fetch`, + queryId: `first`, + outcome: `fulfilled`, + }) + model = reduceRefetchCallSettlement(model, { + type: `settle-application`, + applicationId: `first-result`, + outcome: `fulfilled`, + }) + + expect(observeRefetchCallSettlement(model)).toEqual({ + refetch: `resolved`, + resultOrder: [`first`, undefined], + }) +}) + +it(`rejects fail-fast while an independent application remains pending`, () => { + let model = createRefetchCallSettlementModel([`detail`, `list`], true) + model = reduceRefetchCallSettlement(model, { + type: `accept-application`, + queryId: `detail`, + applicationId: `detail-result`, + }) + model = reduceRefetchCallSettlement(model, { + type: `accept-application`, + queryId: `list`, + applicationId: `list-result`, + }) + for (const queryId of [`detail`, `list`]) { + model = reduceRefetchCallSettlement(model, { + type: `settle-fetch`, + queryId, + outcome: `fulfilled`, + }) + } + model = reduceRefetchCallSettlement(model, { + type: `settle-application`, + applicationId: `detail-result`, + outcome: `rejected`, + }) + + expect(observeRefetchCallSettlement(model)).toEqual({ + refetch: `rejected`, + }) + expect(model.applications.get(`list-result`)?.outcome).toBe(`pending`) +}) + +it(`rejects a handler call that resolves before its Query fetch`, () => { + const model: HandlerRefetchBoundaryModel = { + fetch: `pending`, + application: `unaccepted`, + throwOnError: true, + } + const expected = observeHandlerRefetchBoundary(model) + const returnsAfterInvocation = { + ...expected, + refetch: `resolved` as const, + } + + expect(() => expect(returnsAfterInvocation).toEqual(expected)).toThrow() + expect(expected).toEqual({ + refetch: `pending`, + applicationErrorRecorded: false, + }) +}) + +it(`preserves every direct handler identity legacy replay coordinate`, () => { + const historyKeys = handlerCollectionHistoryGrammar.map( + ({ handlerKind, accessPath, legacyOperation }) => + `${handlerKind}:${accessPath}:${legacyOperation}`, + ) + expect(historyKeys).toEqual([ + `insert:parameter:refetch`, + `insert:parameter:clearError`, + `insert:mutation-alias:refetch`, + `insert:mutation-alias:clearError`, + `update:parameter:refetch`, + `update:parameter:clearError`, + `update:mutation-alias:refetch`, + `update:mutation-alias:clearError`, + `delete:parameter:refetch`, + `delete:parameter:clearError`, + `delete:mutation-alias:refetch`, + `delete:mutation-alias:clearError`, + ]) + expect(new Set(historyKeys).size).toBe(historyKeys.length) + // Six semantic histories each keep their two historical replay runs. + expect( + new Set( + handlerCollectionHistoryGrammar.map( + ({ handlerKind, accessPath }) => `${handlerKind}:${accessPath}`, + ), + ).size, + ).toBe(6) +}) + +it(`rejects handler identity wrong-result controls`, () => { + const history = handlerCollectionHistoryGrammar[0]! + const expected = observeHandlerIdentity(history, 2) + const mutants: Array = [ + { ...expected, transactionIsExternal: false }, + { ...expected, allMutationsPreserveIdentity: false }, + { ...expected, parameterIsExternal: false }, + { ...expected, allAliasesMatchParameter: false }, + { ...expected, anyAliasMatchesExternal: false }, + { ...expected, externalMapMatchesAnyAlias: false }, + { ...expected, parameterMapMatchesAllAliases: false }, + { ...expected, externalWeakMapMatchesAnyAlias: false }, + { ...expected, parameterWeakMapMatchesAllAliases: false }, + ] + + for (const mutant of mutants) { + expect(() => expectHandlerIdentityObservation(mutant, expected)).toThrow() + } + expect(() => + expectHandlerIdentityObservation(expected, expected), + ).not.toThrow() + expect(() => observeHandlerIdentity(history, 0)).toThrow( + `requires a matching mutation alias`, + ) +}) + +it(`rejects a call-entry-only boundary mutant for refresh-first overlap`, () => { + const history = mutationRefreshHistoryGrammar.find( + ({ invocation, legacyOperation, schedule }) => + invocation === `external-handler-overlap` && + legacyOperation === `refetch` && + schedule === `refresh-first-during-fetch`, + ) + expect(history).toBeDefined() + let model: MutationRefreshLivenessModel = { + fetch: `pending`, + application: `unaccepted`, + boundary: `fetch`, + throwOnError: true, + } + model = reduceMutationRefreshLiveness(model, { + type: `accept-application`, + }) + model = reduceMutationRefreshLiveness(model, { + type: `settle-fetch`, + outcome: `fulfilled`, + }) + + const expected = observeMutationRefreshLiveness(model) + const callEntryOnlyMutant = observeMutationRefreshLiveness({ + ...model, + boundary: `application`, + }) + + expect(expected).toEqual({ refresh: `resolved`, application: `pending` }) + expect(callEntryOnlyMutant).not.toEqual(expected) +}) + +it(`preserves every mutation refresh legacy replay coordinate`, () => { + expect(mutationRefreshHistoryGrammar).toHaveLength(12) + expect( + new Set( + mutationRefreshHistoryGrammar.map( + ({ invocation, legacyOperation, schedule }) => + `${invocation}:${legacyOperation}:${schedule}`, + ), + ).size, + ).toBe(mutationRefreshHistoryGrammar.length) + expect( + new Set( + mutationRefreshHistoryGrammar.map( + ({ invocation, schedule }) => `${invocation}:${schedule}`, + ), + ).size, + ).toBe(6) +}) + +it(`rejects silent fulfillment in the public result-settlement checker`, () => { + const deferredOperation = `deferred` + const deferred = advanceResultSettlementModel( + startResultSettlementModel(deferredOperation), + { + type: `query-succeeded`, + operation: deferredOperation, + result: `applicable`, + rowCount: 1, + deferOn: `barrier-a`, + }, + ) + const invalidOperation = `invalid` + const invalid = advanceResultSettlementModel( + startResultSettlementModel(invalidOperation), + { + type: `query-succeeded`, + operation: invalidOperation, + result: `invalid-shape`, + }, + ) + const recoveredOperation = `recovered` + const withRecoveredOperation = advanceResultSettlementModel(invalid, { + type: `start-refetch`, + operation: recoveredOperation, + }) + const recovered = advanceResultSettlementModel(withRecoveredOperation, { + type: `query-succeeded`, + operation: recoveredOperation, + result: `applicable`, + rowCount: 1, + }) + const rebound = advanceResultSettlementModel(deferred, { + type: `refresh-succeeded`, + barrier: `barrier-a`, + result: `applicable`, + deferOn: `barrier-b`, + }) + expect(() => + expectPublicRefetchObservation(deferred, deferredOperation, { + settled: true, + outcome: `fulfilled`, + rowCount: 1, + }), + ).toThrow() + expect(() => + expectPublicRefetchObservation(invalid, invalidOperation, { + settled: true, + outcome: `fulfilled`, + }), + ).toThrow() + expect(() => + expectPublicRefetchObservation(recovered, recoveredOperation, { + settled: true, + outcome: `rejected`, + }), + ).toThrow() + expect(() => + expectPublicRefetchObservation(rebound, deferredOperation, { + settled: true, + outcome: `fulfilled`, + rowCount: 1, + }), + ).toThrow() +}) diff --git a/packages/query-db-collection/tests/ownership-settlement.oracle.test.ts b/packages/query-db-collection/tests/ownership-settlement.oracle.test.ts new file mode 100644 index 0000000000..ac17d98a46 --- /dev/null +++ b/packages/query-db-collection/tests/ownership-settlement.oracle.test.ts @@ -0,0 +1,1754 @@ +/** + * # When does a public refetch settle? + * + * A successful admitted result has an explicit applied, rejected or cancelled + * terminal outcome. Deferring a result is not terminal: its caller waits for + * the replacement. Outside an active mutation phase, successful public refetch + * waits for its exact accepted application obligations, including a nested + * reentrant receipt. Handler-phase refetch uses the fetch boundary so the + * handler can settle, and later application failure is recorded separately. + * Authority: Query guide, Controlling Refetch Behavior, and the ownership + * oracle's original result-settlement and call-obligation contracts. + * + * The existing reducers compute expectations independently of the drivers. + * These finite histories retain the original empty, invalid, recovered, + * deferred, cleanup, overlapping-barrier and reentry cuts. Drivers record + * promise outcomes, public rows, publications, exact error identities and work + * through real QueryClient and Collection paths. Persistence is controlled; + * this file makes no native-host or arbitrary retry claim. + */ +import { hashKey, isCancelledError } from '@tanstack/query-core' +import { createCollection } from '@tanstack/db' +import { afterEach, vi } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration.js' +import { + differs, + oracleProperty, + pass, + violation, +} from '../../db/tests/oracle-property.js' + +import { createDeferred } from '../../db/src/deferred.js' +import { queryCollectionOptions } from '../src/query.js' +import { InvalidQueryResultError } from '../src/errors.js' +import { + advanceResultSettlementModel, + createRefetchCallSettlementModel, + observeExplicitRefetchApplication, + observeHandlerRefetchBoundary, + observeRefetchCallSettlement, + reduceExplicitRefetchApplication, + reduceHandlerRefetchBoundary, + reduceRefetchCallSettlement, + startResultSettlementModel, +} from './ownership-model-oracle' +import { + cleanups, + createOwnershipFixture, + createQueryClient, + reportedError, + rows, + runCleanups, + shared, +} from './ownership-lifecycle-support.js' +import type { Observation, Verdict } from '../../db/tests/oracle-property.js' +import type { + ExplicitRefetchApplicationModel, + HandlerRefetchBoundaryModel, + PublicRefetchObservation, + RefetchApplicationOutcome, + ResultSettlementModelAction, + ResultSettlementModelState, +} from './ownership-model-oracle' +import type { Item } from './ownership-lifecycle-support.js' + +function diagnosticField( + error: unknown, + field: 'name' | 'message', +): string | undefined { + if (typeof error !== 'object' || error === null || !(field in error)) + return undefined + const value = (error as Record)[field] + return typeof value === 'string' ? value : undefined +} +afterEach(runCleanups) +type RecordFact = (checkpoint: string, value: unknown) => void + +/** Wait for a production boundary without computing an expected row. If the + * boundary does not arrive, record the last fact and return; the property's + * check identifies that cut rather than losing the mismatch to a setup error. */ +async function boundary(test: () => boolean): Promise { + for (let turn = 0; turn < 200; turn++) { + if (test()) return true + await new Promise((resolve) => setTimeout(resolve, 5)) + } + return test() +} + +/** Domain checks retain every observation cut in the old driver. No expected + * value is recorded by a production callback: models build the owed facts + * from authored history data, and this check compares the ordered trace. */ +function checkCuts( + actual: ReadonlyArray, + expected: ReadonlyArray, + law: string, +): Verdict { + for (let i = 0; i < expected.length; i++) { + const wanted = expected[i]! + const got = actual[i] + if (!got) + return violation( + wanted.checkpoint, + 'the promised observation cut was not reached', + law, + ) + const difference = differs(wanted.checkpoint, law, got, wanted) + if (difference) return difference + } + return differs('trace-length', law, actual.length, expected.length) ?? pass +} + +async function observeBoundary( + observe: RecordFact, + checkpoint: string, + read: () => T, + ready: (value: T) => boolean, +): Promise { + const reached = await boundary(() => ready(read())) + observe(checkpoint, read()) + return reached +} +async function settle( + promise: PromiseLike, +): Promise< + { outcome: 'resolved'; value: T } | { outcome: 'rejected'; error: unknown } +> { + try { + return { outcome: 'resolved', value: await promise } + } catch (error) { + return { outcome: 'rejected', error } + } +} +/** The same result-settlement reducer judges every original public outcome. + * This projection is the old checker's promised answer, without a Vitest + * assertion. Call, publication, diagnostics, and work remain separate facts. */ +function owedPublic( + state: ResultSettlementModelState, + id: string, +): PublicRefetchObservation { + const operation = state.get(id) + if (!operation) throw Error('Unknown authored public refetch') + if (operation.phase !== 'terminal') return { settled: false } + return operation.outcome === 'applied' + ? { + settled: true, + outcome: 'fulfilled', + ...(operation.rowCount === undefined + ? {} + : { rowCount: operation.rowCount }), + } + : { settled: true, outcome: 'rejected' } +} +function resultAfter( + operation: string, + result: 'applicable' | 'invalid-shape', + rowCount?: 0 | 1, + deferOn?: string, +): ResultSettlementModelState { + return advanceResultSettlementModel(startResultSettlementModel(operation), { + type: 'query-succeeded', + operation, + result, + ...(rowCount === undefined ? {} : { rowCount }), + ...(deferOn ? { deferOn } : {}), + }) +} +function resultFact( + state: ResultSettlementModelState, + id: string, +): Observation { + return { checkpoint: 'public:' + id, value: owedPublic(state, id) } +} +const fact = (checkpoint: string, value: unknown): Observation => ({ + checkpoint, + value, +}) +function emptySettlementModel(): Array { + return [ + resultFact(resultAfter('empty-result', 'applicable', 0), 'empty-result'), + ] +} +function deferredSettlementModel(): Array { + let model = resultAfter('deferred-result', 'applicable', 1, 'barrier-a') + const expected = [ + fact('fetch-calls:2', 2), + resultFact(model, 'deferred-result'), + fact('held-name', 'Initial'), + fact('fetch-calls:3', 3), + ] + model = advanceResultSettlementModel(model, { + type: 'refresh-succeeded', + barrier: 'barrier-a', + result: 'applicable', + rowCount: 1, + }) + return [ + ...expected, + resultFact(model, 'deferred-result'), + fact('replacement-name', 'Replacement'), + ] +} +function cleanupSettlementModel(): Array { + const model = advanceResultSettlementModel( + resultAfter('deferred-cleanup', 'applicable', 1, 'barrier-a'), + { type: 'retire' }, + ) + return [ + fact('fetch-calls:2', 2), + resultFact(model, 'deferred-cleanup'), + fact('cancelled-error', true), + ] +} +function recoveredSettlementModel(): Array { + let model = resultAfter('invalid', 'invalid-shape') + model = advanceResultSettlementModel(model, { + type: 'start-refetch', + operation: 'recovered', + }) + model = advanceResultSettlementModel(model, { + type: 'query-succeeded', + operation: 'recovered', + result: 'applicable', + }) + return [ + fact('initial-load-error', true), + fact('recovery-work', { queryCalls: 2, scanCalls: 1 }), + resultFact(model, 'recovered'), + fact('recovered-name', 'Recovered'), + ] +} +function eagerRecoveredSettlementModel(): Array { + let model = resultAfter('invalid', 'invalid-shape') + model = advanceResultSettlementModel(model, { + type: 'start-refetch', + operation: 'recovered', + }) + model = advanceResultSettlementModel(model, { + type: 'query-succeeded', + operation: 'recovered', + result: 'applicable', + rowCount: 1, + }) + return [ + fact('initial-load-error', true), + fact('initial-error-status', { status: 'error', errorDefined: true }), + fact('fetch-calls:2', 2), + resultFact(model, 'recovered'), + fact('recovery-error', undefined), + fact('recovered-state', { + status: 'error', + errorDefined: true, + name: 'Recovered', + }), + ] +} +function retainedInvalidSettlementModel(): Array { + return [ + fact('initial-work', { queryCalls: 1, scanCalls: 1 }), + fact('fetch-calls:2', 2), + resultFact( + resultAfter('retained-invalid-result', 'invalid-shape'), + 'retained-invalid-result', + ), + fact('invalid-error', true), + ] +} +function overlappingDeferralModel(): Array { + let model = new Map() as ResultSettlementModelState + const step = (action: ResultSettlementModelAction) => { + model = advanceResultSettlementModel(model, action) + } + step({ type: 'start-refetch', operation: 'first' }) + step({ + type: 'query-succeeded', + operation: 'first', + result: 'applicable', + rowCount: 1, + deferOn: 'barrier-a', + }) + const expected = [ + fact('first-skipped', { + queryCalls: 2, + data: [{ ...shared, name: 'Skipped A' }], + }), + ] + step({ type: 'start-refetch', operation: 'second' }) + step({ + type: 'query-succeeded', + operation: 'second', + result: 'applicable', + rowCount: 1, + deferOn: 'barrier-b', + }) + expected.push( + fact('second-skipped', { + queryCalls: 3, + data: [{ ...shared, name: 'Skipped B' }], + }), + fact('fetch-calls:4', 4), + ) + step({ + type: 'refresh-succeeded', + barrier: 'barrier-a', + result: 'applicable', + rowCount: 1, + deferOn: 'barrier-b', + }) + expected.push( + resultFact(model, 'first'), + resultFact(model, 'second'), + fact('held-name', 'Initial'), + fact('fetch-calls:5', 5), + ) + step({ + type: 'refresh-succeeded', + barrier: 'barrier-b', + result: 'applicable', + rowCount: 1, + }) + return [ + ...expected, + resultFact(model, 'first'), + resultFact(model, 'second'), + fact('replacement-name', 'Replacement'), + ] +} +function independentSettlementModel(): Array { + let model = resultAfter('old', 'applicable', 1, 'barrier-a') + model = advanceResultSettlementModel(model, { + type: 'start-refetch', + operation: 'fresh', + }) + model = advanceResultSettlementModel(model, { + type: 'query-succeeded', + operation: 'fresh', + result: 'applicable', + rowCount: 1, + }) + return [ + fact('old-skipped', { + queryCalls: 2, + data: [{ ...shared, name: 'Skipped' }], + }), + fact('fresh-applied', { queryCalls: 3, name: 'Fresh' }), + resultFact(model, 'old'), + resultFact(model, 'fresh'), + ] +} +function applicationFailureModel(h: { + throwOnError: boolean +}): Array { + let model: ExplicitRefetchApplicationModel = { + calls: new Map(), + publicValues: new Map([['query', 'Initial']]), + } + model = reduceExplicitRefetchApplication(model, { + type: 'start-call', + callId: 1, + resultIds: ['query'], + throwOnError: h.throwOnError, + }) + model = reduceExplicitRefetchApplication(model, { + type: 'accept-result', + callId: 1, + resultId: 'query', + value: 'Invalid', + }) + model = reduceExplicitRefetchApplication(model, { + type: 'reject-application', + callId: 1, + resultId: 'query', + }) + const outcome = observeExplicitRefetchApplication(model, 1) + return [ + fact( + 'refetch-return', + h.throwOnError + ? { outcome: 'rejected', expectedError: true } + : { outcome: 'resolved', length: 1 }, + ), + fact('application-outcome', outcome.refetch), + fact('explicit-refetch', outcome), + ] +} +function handlerFailureModel(): Array { + let model: HandlerRefetchBoundaryModel = { + fetch: 'pending', + application: 'unaccepted', + throwOnError: true, + } + model = reduceHandlerRefetchBoundary(model, { type: 'accept-application' }) + model = reduceHandlerRefetchBoundary(model, { + type: 'settle-fetch', + outcome: 'fulfilled', + }) + model = reduceHandlerRefetchBoundary(model, { + type: 'settle-application', + outcome: 'rejected', + }) + return [ + fact('application-error-recorded', true), + fact('handler-boundary', observeHandlerRefetchBoundary(model)), + ] +} +function reentrantSettlementModel( + receiptOutcome: 'fulfilled' | 'rejected', +): Array { + let model = createRefetchCallSettlementModel(['query'], true) + model = reduceRefetchCallSettlement(model, { + type: 'accept-application', + queryId: 'query', + applicationId: 'reentrant-application', + }) + model = reduceRefetchCallSettlement(model, { + type: 'settle-fetch', + queryId: 'query', + outcome: 'fulfilled', + }) + const publication = [ + [{ type: 'update', name: 'Updated', previousName: 'Initial' }], + ] + const expected = [ + fact('reentered', true), + fact('nested-commits', 1), + fact('fetch-calls:2', 2), + fact('refetch-pending', observeRefetchCallSettlement(model).refetch), + fact('held-publications', publication), + fact( + 'refetch-return', + receiptOutcome === 'fulfilled' + ? { outcome: 'resolved', length: 1 } + : { outcome: 'rejected', expectedError: true }, + ), + ] + model = reduceRefetchCallSettlement(model, { + type: 'settle-application', + applicationId: 'reentrant-application', + outcome: receiptOutcome, + }) + return [ + ...expected, + fact('refetch-terminal', observeRefetchCallSettlement(model).refetch), + fact('applied-name', 'Updated'), + fact('terminal-publications', publication), + ] +} + +function invalidResultModel(h: { diagnostic: string }) { + return { + settlement: owedPublic( + resultAfter('invalid-result', 'invalid-shape'), + 'invalid-result', + ), + diagnostic: h.diagnostic, + } +} +function invalidResultCheck( + _h: unknown, + trace: ReadonlyArray, + expected: ReturnType, +): Verdict { + const outcome = trace[0] + const failed = differs( + 'public:invalid-result', + 'public result settlement', + outcome, + { checkpoint: 'public:invalid-result', value: expected.settlement }, + ) + if (failed) return failed + const diagnostics = trace[1] + if (diagnostics?.checkpoint !== 'invalid-diagnostics') + return violation( + 'invalid-diagnostics', + 'invalid-result diagnostics were not observed', + 'public result settlement', + ) + const observed = diagnostics.value as { + invalidClass: boolean + rejectionMessage?: string + rows: Array + consoleCalls: Array<{ + context: unknown + errorName?: string + errorMessage?: string + }> + } + const call = observed.consoleCalls[0] + return ( + differs( + 'invalid-diagnostics', + 'public result settlement', + { + invalidClass: observed.invalidClass, + messageHasDiagnostic: observed.rejectionMessage?.includes( + expected.diagnostic, + ), + rows: observed.rows, + consoleCount: observed.consoleCalls.length, + consolePrefix: + typeof call?.context === 'string' && + call.context.includes('[QueryCollection] Error applying query'), + consoleErrorName: call?.errorName, + consoleErrorMessage: call?.errorMessage?.includes( + '@tanstack/query-db-collection: queryFn must return an array of objects', + ), + }, + { + invalidClass: true, + messageHasDiagnostic: true, + rows: [shared.id], + consoleCount: 1, + consolePrefix: true, + consoleErrorName: 'InvalidQueryResultError', + consoleErrorMessage: true, + }, + ) ?? pass + ) +} +declareOracle({ + owns: [`src/query.ts#queryCollectionOptions`], + model: [ + emptySettlementModel, + deferredSettlementModel, + cleanupSettlementModel, + recoveredSettlementModel, + eagerRecoveredSettlementModel, + retainedInvalidSettlementModel, + overlappingDeferralModel, + independentSettlementModel, + applicationFailureModel, + handlerFailureModel, + reentrantSettlementModel, + invalidResultModel, + ], + check: [checkCuts, invalidResultCheck], +}) +oracleProperty({ + id: 'query-db.ownership.application-failure', + law: { + statement: 'settles an application failure with throwOnError=$throwOnError', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{ throwOnError: true }, { throwOnError: false }], + model: applicationFailureModel, + driver: async ({ throwOnError }: { throwOnError: boolean }, observe) => { + try { + const initial = { ...shared, name: `Initial` } + const invalid = { ...shared, name: `Invalid` } + const applicationError = new Error(`Explicit refetch application failed`) + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockResolvedValueOnce([invalid]) + const consoleError = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + const collection = createCollection( + queryCollectionOptions({ + id: `explicit-refetch-application-failure-${throwOnError}`, + queryClient, + queryKey: [`explicit-refetch-application-failure`, throwOnError], + queryFn, + getKey: (item) => { + if (item.name === invalid.name) throw applicationError + return item.id + }, + startSync: true, + }), + ) + cleanups.push(async () => { + consoleError.mockRestore() + await collection.cleanup() + queryClient.clear() + }) + await collection.stateWhenReady() + + let actualOutcome: RefetchApplicationOutcome + const refetch = collection.utils.refetch({ throwOnError }) + if (throwOnError) { + const result = await settle(refetch) + observe('refetch-return', { + outcome: result.outcome, + expectedError: + result.outcome === 'rejected' && result.error === applicationError, + }) + actualOutcome = result.outcome + } else { + const result = await settle(refetch) + observe( + 'refetch-return', + result.outcome === 'resolved' + ? { outcome: result.outcome, length: result.value.length } + : { outcome: result.outcome, expectedError: false }, + ) + actualOutcome = result.outcome + } + observe('application-outcome', actualOutcome) + observe('explicit-refetch', { + publicValues: { query: collection.get(shared.id)?.name }, + refetch: actualOutcome, + ...(actualOutcome === `resolved` + ? { resultOrder: [`query`] } + : undefined), + }) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.handler-failure', + law: { + statement: + 'records a handler-phase application failure after fetch settlement', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: handlerFailureModel, + driver: async (_history, observe) => { + try { + const initial = { ...shared, name: `Initial` } + const invalid = { ...shared, name: `Invalid` } + const applicationError = new Error(`Handler application failed`) + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockResolvedValueOnce([invalid]) + const consoleError = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + let handlerOutcome: RefetchApplicationOutcome = `pending` + const collection = createCollection( + queryCollectionOptions({ + id: `handler-refetch-application-failure`, + queryClient, + queryKey: [`handler-refetch-application-failure`], + queryFn, + getKey: (item) => { + if (item.name === invalid.name) throw applicationError + return item.id + }, + startSync: true, + onUpdate: async ({ collection: handlerCollection }) => { + await handlerCollection.utils.refetch({ throwOnError: true }).then( + () => { + handlerOutcome = `resolved` + }, + (error: unknown) => { + handlerOutcome = `rejected` + throw error + }, + ) + }, + }), + ) + cleanups.push(async () => { + consoleError.mockRestore() + await collection.cleanup() + queryClient.clear() + }) + await collection.stateWhenReady() + + const mutation = collection.update(shared.id, (draft) => { + draft.name = `Optimistic` + }) + await mutation.when(`settled`) + + if ( + !(await observeBoundary( + observe, + 'application-error-recorded', + () => reportedError(collection) === applicationError, + (found) => found, + )) + ) + return + + observe('handler-boundary', { + refetch: handlerOutcome, + applicationErrorRecorded: + reportedError(collection) === applicationError, + }) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.empty-result', + law: { + statement: 'fulfills a public refetch after applying a valid empty result', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: emptySettlementModel, + driver: async (_history, observe) => { + try { + const id = `empty-result-settlement` + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([shared]) + .mockResolvedValueOnce([]) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + startSync: true, + }), + ) + cleanups.push(async () => { + await collection.cleanup() + queryClient.clear() + }) + + await collection.stateWhenReady() + const refetch = collection.utils.refetch({ throwOnError: true }) + await refetch + + const operation = `empty-result` + + observe('public:' + operation, { + settled: true, + outcome: `fulfilled`, + rowCount: collection.size, + }) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.reentrant-receipt', + law: { + statement: + 'keeps a same-result reentrant $receiptOutcome receipt attached to its refetch', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [`fulfilled`, `rejected`] as const, + model: reentrantSettlementModel, + driver: async (receiptOutcome, observe) => { + try { + const id = `same-result-reentrant-application` + const initial = { ...shared, name: `Initial` } + const updated = { ...shared, name: `Updated` } + const applicationFailure = new Error(`Reentrant application failed`) + const reentrantApplication = createDeferred() + const reentrantApplicationStarted = createDeferred() + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockResolvedValueOnce([updated]) + const baseOptions = queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + startSync: true, + }) + const originalSync = baseOptions.sync + let commitDepth = 0 + let holdReentrantApplication = false + let reentrantCommitCount = 0 + const collection = createCollection({ + ...baseOptions, + sync: { + sync: (params: Parameters[0]) => + originalSync.sync({ + ...params, + commit: (signal) => { + commitDepth++ + try { + const applied = params.commit(signal) + if (holdReentrantApplication && commitDepth > 1) { + reentrantCommitCount++ + reentrantApplicationStarted.resolve(undefined) + return Promise.resolve( + applied === true ? undefined : applied, + ).then(() => reentrantApplication.promise) + } + return applied + } finally { + commitDepth-- + } + }, + }), + }, + }) + const consoleError = + receiptOutcome === `rejected` + ? vi.spyOn(console, `error`).mockImplementation(() => {}) + : undefined + let subscription = collection.subscribeChanges(() => {}, { + includeInitialState: false, + }) + cleanups.push(async () => { + consoleError?.mockRestore() + reentrantApplication.resolve(undefined) + subscription.unsubscribe() + await collection.cleanup() + queryClient.clear() + }) + + await collection.stateWhenReady() + let reentered = false + const publications: Array< + Array<{ type: string; name: string; previousName?: string }> + > = [] + subscription.unsubscribe() + subscription = collection.subscribeChanges( + (batch) => { + if (batch.length > 0) { + publications.push( + batch.map((change) => ({ + type: change.type, + name: change.value.name, + ...(change.type === `update` + ? { previousName: change.previousValue?.name } + : {}), + })), + ) + } + if (reentered || collection.get(shared.id)?.name !== updated.name) + return + reentered = true + // Dropping the last subscriber and immediately adding another makes + // Query DB apply this observer's exact current result again while the + // outer synchronous commit is still delivering its change. + subscription.unsubscribe() + subscription = collection.subscribeChanges(() => {}, { + includeInitialState: false, + }) + }, + { includeInitialState: false }, + ) + holdReentrantApplication = true + + let refetchOutcome: RefetchApplicationOutcome = `pending` + const refetch = collection.utils.refetch({ throwOnError: true }).then( + (result) => { + refetchOutcome = `resolved` + return result + }, + (error: unknown) => { + refetchOutcome = `rejected` + throw error + }, + ) + void refetch.catch(() => undefined) + + await reentrantApplicationStarted.promise + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + observe('reentered', reentered) + observe('nested-commits', reentrantCommitCount) + observe('fetch-calls:2', queryFn.mock.calls.length) + observe('refetch-pending', refetchOutcome) + observe( + 'held-publications', + publications.map((batch) => batch.map((change) => ({ ...change }))), + ) + + if (receiptOutcome === `fulfilled`) { + reentrantApplication.resolve(undefined) + const result = await settle(refetch) + observe( + 'refetch-return', + result.outcome === 'resolved' + ? { outcome: result.outcome, length: result.value.length } + : { outcome: result.outcome, expectedError: false }, + ) + } else { + reentrantApplication.reject(applicationFailure) + const result = await settle(refetch) + observe('refetch-return', { + outcome: result.outcome, + expectedError: + result.outcome === 'rejected' && + result.error === applicationFailure, + }) + } + + observe('refetch-terminal', refetchOutcome) + observe('applied-name', collection.get(shared.id)?.name) + observe( + 'terminal-publications', + publications.map((batch) => batch.map((change) => ({ ...change }))), + ) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.invalid-result', + law: { + statement: + 'rejects a public refetch whose successful Query result has an invalid $invalidShape shape, throwOnError=$throwOnError', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [ + { + invalidShape: `non-array`, + throwOnError: false, + invalidResult: 42, + diagnostic: `Got: number`, + }, + { + invalidShape: `non-array`, + throwOnError: true, + invalidResult: 42, + diagnostic: `Got: number`, + }, + { + invalidShape: `null array member`, + throwOnError: false, + invalidResult: [null], + diagnostic: `Invalid item at index 0: null`, + }, + { + invalidShape: `null array member`, + throwOnError: true, + invalidResult: [null], + diagnostic: `Invalid item at index 0: null`, + }, + ] as const, + model: invalidResultModel, + driver: async ( + { + throwOnError, + invalidResult, + }: { throwOnError: boolean; invalidResult: unknown; diagnostic: string }, + observe, + ) => { + try { + const id = `invalid-result-settlement` + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([shared]) + .mockResolvedValueOnce(invalidResult as Array) + const consoleError = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + startSync: true, + }), + ) + cleanups.push(async () => { + consoleError.mockRestore() + await collection.cleanup() + queryClient.clear() + }) + + await collection.stateWhenReady() + let rejection: unknown + const refetch = collection.utils.refetch({ throwOnError }).then( + () => ({ settled: true, outcome: `fulfilled` as const }), + (error: unknown) => { + rejection = error + return { settled: true, outcome: `rejected` as const } + }, + ) + + const operation = `invalid-result` + + observe('public:' + operation, await refetch) + observe('invalid-diagnostics', { + invalidClass: rejection instanceof InvalidQueryResultError, + rejectionMessage: + rejection instanceof Error ? rejection.message : undefined, + rows: rows(collection), + consoleCalls: consoleError.mock.calls.map(([context, error]) => ({ + context, + errorName: diagnosticField(error, 'name'), + errorMessage: diagnosticField(error, 'message'), + })), + }) + } finally { + await runCleanups() + } + }, + check: invalidResultCheck, +}) + +oracleProperty({ + id: 'query-db.ownership.deferred-result', + law: { + statement: + 'keeps a deferred public refetch pending until its replacement applies', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: deferredSettlementModel, + driver: async (_history, observe) => { + try { + const id = `deferred-result-settlement` + const barrier = createDeferred() + const initial = { ...shared, name: `Initial` } + const skipped = { ...shared, name: `Skipped` } + const replacement = { ...shared, name: `Replacement` } + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockResolvedValueOnce([skipped]) + .mockResolvedValueOnce([replacement]) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + startSync: true, + }), + ) + cleanups.push(async () => { + collection.deferDataRefresh = null + barrier.resolve() + await collection.cleanup() + queryClient.clear() + }) + + await collection.stateWhenReady() + collection.deferDataRefresh = barrier.promise + let observation: PublicRefetchObservation = { settled: false } + const refetch = collection.utils.refetch({ throwOnError: true }).then( + () => { + observation = { + settled: true, + outcome: `fulfilled`, + rowCount: collection.size, + } + }, + () => { + observation = { settled: true, outcome: `rejected` } + }, + ) + + if ( + !(await observeBoundary( + observe, + 'fetch-calls:2', + () => queryFn.mock.calls.length, + (n) => n === 2, + )) + ) + return + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + + const operation = `deferred-result` + + observe('public:' + operation, observation) + observe('held-name', collection.get(shared.id)?.name) + + collection.deferDataRefresh = null + barrier.resolve() + if ( + !(await observeBoundary( + observe, + 'fetch-calls:3', + () => queryFn.mock.calls.length, + (n) => n === 3, + )) + ) + return + await refetch + + observe('public:' + operation, observation) + observe('replacement-name', collection.get(shared.id)?.name) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.deferred-cleanup', + law: { + statement: 'cancels a deferred public refetch when cleanup retires it', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: cleanupSettlementModel, + driver: async (_history, observe) => { + try { + const id = `deferred-result-cleanup` + const barrier = createDeferred() + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([shared]) + .mockResolvedValueOnce([{ ...shared, name: `Skipped` }]) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + startSync: true, + }), + ) + cleanups.push(async () => { + collection.deferDataRefresh = null + barrier.resolve() + await collection.cleanup() + queryClient.clear() + }) + + await collection.stateWhenReady() + collection.deferDataRefresh = barrier.promise + const refetch = collection.utils.refetch({ throwOnError: true }).then( + () => ({ + observation: { settled: true, outcome: `fulfilled` as const }, + }), + (error: unknown) => ({ + observation: { settled: true, outcome: `rejected` as const }, + error, + }), + ) + + if ( + !(await observeBoundary( + observe, + 'fetch-calls:2', + () => queryFn.mock.calls.length, + (n) => n === 2, + )) + ) + return + const operation = `deferred-cleanup` + + await collection.cleanup() + + const result = await refetch + observe('public:' + operation, result.observation) + observe( + 'cancelled-error', + isCancelledError('error' in result ? result.error : undefined), + ) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.recovered-result', + law: { + statement: + 'does not reuse an invalid result rejection for a later valid refetch', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: recoveredSettlementModel, + driver: async (_history, observe) => { + try { + const id = `recovered-result-settlement` + const queryHash = hashKey([id]) + const persistedScan = + createDeferred< + Array<{ key: string | number; value: Item; metadata?: unknown }> + >() + const scanPersistedRows = vi.fn(() => persistedScan.promise) + const invalidResult = 42 as unknown as Array + const recovered = { ...shared, name: `Recovered` } + const consoleError = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + const { collection, queryFn } = createOwnershipFixture({ + id, + results: [invalidResult, [recovered]], + syncMode: `on-demand`, + scanPersistedRows, + setupMetadata: (metadata) => { + metadata.collection.set(`queryCollection:gc:${queryHash}`, { + queryHash, + mode: `until-revalidated`, + }) + }, + }) + cleanups.push(() => { + consoleError.mockRestore() + return Promise.resolve() + }) + + try { + const initialLoad = collection._sync.loadSubset({}) + const initialOutcome = await settle( + Promise.resolve(initialLoad === true ? undefined : initialLoad), + ) + observe( + 'initial-load-error', + initialOutcome.outcome === 'rejected' && + initialOutcome.error instanceof InvalidQueryResultError, + ) + + const refetch = collection.utils.refetch({ throwOnError: true }).then( + () => ({ settled: true, outcome: `fulfilled` as const }), + () => ({ settled: true, outcome: `rejected` as const }), + ) + if ( + !(await observeBoundary( + observe, + 'recovery-work', + () => ({ + queryCalls: queryFn.mock.calls.length, + scanCalls: scanPersistedRows.mock.calls.length, + }), + (work) => work.queryCalls === 2 && work.scanCalls === 1, + )) + ) + return + persistedScan.resolve([]) + + observe('public:' + `recovered`, await refetch) + await observeBoundary( + observe, + 'recovered-name', + () => collection.get(shared.id)?.name, + (name) => name === recovered.name, + ) + } finally { + persistedScan.resolve([]) + } + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.eager-recovery', + law: { + statement: + 'does not reuse an eager initial rejection when a refetch applies a valid result', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: eagerRecoveredSettlementModel, + driver: async (_history, observe) => { + try { + const id = `eager-recovered-result-settlement` + const invalidResult = 42 as unknown as Array + const recovered = { ...shared, name: `Recovered` } + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce(invalidResult) + .mockResolvedValueOnce([recovered]) + const consoleError = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + syncMode: `eager`, + startSync: true, + }), + ) + cleanups.push(async () => { + consoleError.mockRestore() + await collection.cleanup() + queryClient.clear() + }) + + const initialOutcome = await settle(collection.stateWhenReady()) + observe( + 'initial-load-error', + initialOutcome.outcome === 'rejected' && + initialOutcome.error instanceof InvalidQueryResultError, + ) + + observe('initial-error-status', { + status: collection.status, + errorDefined: reportedError(collection) !== undefined, + }) + + let rejection: unknown + const recovery = collection.utils.refetch({ throwOnError: true }).then( + () => ({ + settled: true, + outcome: `fulfilled` as const, + rowCount: collection.size, + }), + (error: unknown) => { + rejection = error + return { settled: true, outcome: `rejected` as const } + }, + ) + if ( + !(await observeBoundary( + observe, + 'fetch-calls:2', + () => queryFn.mock.calls.length, + (n) => n === 2, + )) + ) + return + + observe('public:' + `recovered`, await recovery) + observe('recovery-error', rejection) + // An initial error is final for its sync run. The refetch applies its + // rows, but the Collection keeps its initial error until a restart. + observe('recovered-state', { + status: collection.status, + errorDefined: reportedError(collection) !== undefined, + name: collection.get(shared.id)?.name, + }) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.retained-invalid', + law: { + statement: + 'rejects an invalid retained revalidation before its persisted baseline loads', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: retainedInvalidSettlementModel, + driver: async (_history, observe) => { + try { + const id = `retained-invalid-result-settlement` + const queryHash = hashKey([id]) + const persistedScan = + createDeferred< + Array<{ key: string | number; value: Item; metadata?: unknown }> + >() + const scanPersistedRows = vi.fn(() => persistedScan.promise) + const invalidResult = 42 as unknown as Array + const consoleError = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + const { collection, queryFn } = createOwnershipFixture({ + id, + results: [[shared], invalidResult], + syncMode: `on-demand`, + scanPersistedRows, + setupMetadata: (metadata) => { + metadata.collection.set(`queryCollection:gc:${queryHash}`, { + queryHash, + mode: `until-revalidated`, + }) + }, + }) + cleanups.push(() => { + consoleError.mockRestore() + return Promise.resolve() + }) + + const initialLoad = collection._sync.loadSubset({}) + const initialLoadPromise = Promise.resolve( + initialLoad === true ? undefined : initialLoad, + ) + void initialLoadPromise.catch(() => undefined) + + try { + if ( + !(await observeBoundary( + observe, + 'initial-work', + () => ({ + queryCalls: queryFn.mock.calls.length, + scanCalls: scanPersistedRows.mock.calls.length, + }), + (work) => work.queryCalls === 1 && work.scanCalls === 1, + )) + ) + return + + let rejection: unknown + const refetch = collection.utils.refetch({ throwOnError: true }).then( + () => ({ settled: true, outcome: `fulfilled` as const }), + (error: unknown) => { + rejection = error + return { settled: true, outcome: `rejected` as const } + }, + ) + if ( + !(await observeBoundary( + observe, + 'fetch-calls:2', + () => queryFn.mock.calls.length, + (n) => n === 2, + )) + ) + return + + const operation = `retained-invalid-result` + + observe('public:' + operation, await refetch) + observe('invalid-error', rejection instanceof InvalidQueryResultError) + } finally { + persistedScan.resolve([]) + await initialLoadPromise.catch(() => undefined) + } + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.overlapping-deferral', + law: { + statement: + 'keeps refetches pending when an older barrier refresh reaches a newer barrier', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: overlappingDeferralModel, + driver: async (_history, observe) => { + try { + const id = `overlapping-defer-generations` + const barrierA = createDeferred() + const barrierB = createDeferred() + const initial = { ...shared, name: `Initial` } + const skippedA = { ...shared, name: `Skipped A` } + const skippedB = { ...shared, name: `Skipped B` } + const replacement = { ...shared, name: `Replacement` } + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockResolvedValueOnce([skippedA]) + .mockResolvedValueOnce([skippedB]) + .mockResolvedValueOnce([{ ...shared, name: `Refresh while B active` }]) + .mockResolvedValue([replacement]) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + startSync: true, + }), + ) + cleanups.push(async () => { + collection.deferDataRefresh = null + barrierA.resolve() + barrierB.resolve() + await collection.cleanup() + queryClient.clear() + }) + + await collection.stateWhenReady() + + let firstObservation: PublicRefetchObservation = { settled: false } + let secondObservation: PublicRefetchObservation = { settled: false } + + const barrierAPromise = barrierA.promise + collection.deferDataRefresh = barrierAPromise + const clearBarrierA = barrierAPromise.finally(() => { + if (collection.deferDataRefresh === barrierAPromise) { + collection.deferDataRefresh = null + } + }) + + const first = collection.utils.refetch({ throwOnError: true }).then( + () => { + firstObservation = { + settled: true, + outcome: `fulfilled`, + rowCount: 1, + } + }, + () => { + firstObservation = { settled: true, outcome: `rejected` } + }, + ) + if ( + !(await observeBoundary( + observe, + 'first-skipped', + () => ({ + queryCalls: queryFn.mock.calls.length, + data: queryClient.getQueryData([id]), + }), + (value) => + value.queryCalls === 2 && + JSON.stringify(value.data) === JSON.stringify([skippedA]), + )) + ) + return + + const barrierBPromise = barrierB.promise + collection.deferDataRefresh = barrierBPromise + const clearBarrierB = barrierBPromise.finally(() => { + if (collection.deferDataRefresh === barrierBPromise) { + collection.deferDataRefresh = null + } + }) + + const second = collection.utils.refetch({ throwOnError: true }).then( + () => { + secondObservation = { + settled: true, + outcome: `fulfilled`, + rowCount: 1, + } + }, + () => { + secondObservation = { settled: true, outcome: `rejected` } + }, + ) + if ( + !(await observeBoundary( + observe, + 'second-skipped', + () => ({ + queryCalls: queryFn.mock.calls.length, + data: queryClient.getQueryData([id]), + }), + (value) => + value.queryCalls === 3 && + JSON.stringify(value.data) === JSON.stringify([skippedB]), + )) + ) + return + + barrierA.resolve() + await clearBarrierA + if ( + !(await observeBoundary( + observe, + 'fetch-calls:4', + () => queryFn.mock.calls.length, + (n) => n === 4, + )) + ) + return + + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + + observe('public:' + `first`, firstObservation) + observe('public:' + `second`, secondObservation) + observe('held-name', collection.get(shared.id)?.name) + + barrierB.resolve() + await clearBarrierB + await Promise.all([first, second]) + observe('fetch-calls:5', queryFn.mock.calls.length) + + observe('public:' + `first`, firstObservation) + observe('public:' + `second`, secondObservation) + observe('replacement-name', collection.get(shared.id)?.name) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) + +oracleProperty({ + id: 'query-db.ownership.independent-result', + law: { + statement: + 'settles a fresh applied refetch independently of an older deferred result', + authority: + 'docs/collections/query-collection.md#controlling-refetch-behavior and the independent ownership result/call-settlement models.', + limits: + 'Original bounded real QueryClient history and all its public/persistence/receipt cuts; no native SDK or transport retry claim.', + owner: 'Query DB ownership lifecycle', + }, + histories: [{}], + model: independentSettlementModel, + driver: async (_history, observe) => { + try { + const id = `independent-refetch-settlement` + const barrier = createDeferred() + const initial = { ...shared, name: `Initial` } + const skipped = { ...shared, name: `Skipped` } + const fresh = { ...shared, name: `Fresh` } + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockResolvedValueOnce([skipped]) + .mockResolvedValueOnce([fresh]) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + startSync: true, + }), + ) + cleanups.push(async () => { + barrier.resolve() + await collection.cleanup() + queryClient.clear() + }) + + await collection.stateWhenReady() + collection.deferDataRefresh = barrier.promise + let oldObservation: PublicRefetchObservation = { settled: false } + const oldRefetch = collection.utils.refetch({ throwOnError: true }).then( + () => { + oldObservation = { settled: true, outcome: `fulfilled` } + }, + () => { + oldObservation = { settled: true, outcome: `rejected` } + }, + ) + if ( + !(await observeBoundary( + observe, + 'old-skipped', + () => ({ + queryCalls: queryFn.mock.calls.length, + data: queryClient.getQueryData([id]), + }), + (value) => + value.queryCalls === 2 && + JSON.stringify(value.data) === JSON.stringify([skipped]), + )) + ) + return + + collection.deferDataRefresh = null + + let freshObservation: PublicRefetchObservation = { settled: false } + const freshRefetch = collection.utils + .refetch({ throwOnError: true }) + .then( + () => { + freshObservation = { + settled: true, + outcome: `fulfilled`, + rowCount: collection.size, + } + }, + () => { + freshObservation = { settled: true, outcome: `rejected` } + }, + ) + if ( + !(await observeBoundary( + observe, + 'fresh-applied', + () => ({ + queryCalls: queryFn.mock.calls.length, + name: collection.get(shared.id)?.name, + }), + (value) => value.queryCalls === 3 && value.name === fresh.name, + )) + ) + return + + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + + observe('public:' + `old`, oldObservation) + observe('public:' + `fresh`, freshObservation) + await collection.cleanup() + barrier.resolve() + await Promise.allSettled([oldRefetch, freshRefetch]) + } finally { + await runCleanups() + } + }, + check: (_h, trace, expected) => + checkCuts(trace, expected, 'public result settlement'), +}) diff --git a/packages/query-db-collection/tests/ownership-upper.oracle.test.ts b/packages/query-db-collection/tests/ownership-upper.oracle.test.ts new file mode 100644 index 0000000000..e870d11420 --- /dev/null +++ b/packages/query-db-collection/tests/ownership-upper.oracle.test.ts @@ -0,0 +1,2026 @@ +import { hashKey } from '@tanstack/query-core' +import { + IR, + createCollection, + createOptimisticAction, + createTransaction, + eq, + getLoadSubsetDemandKey, +} from '@tanstack/db' +import { afterEach, vi } from 'vitest' +import { + differs, + oracleProperty, + pass, + violation, +} from '../../db/tests/oracle-property.js' +import { declareOracle } from '../../db/tests/oracle-declaration.js' +import { createDeferred } from '../../db/src/deferred.js' +import { queryCollectionOptions } from '../src/query.js' +import { + cleanups, + createOwnershipFixture, + createQueryClient, + detailOnly, + listOnly, + runCleanups, + shared, +} from './ownership-lifecycle-support.js' +import { + createRefetchCallSettlementModel, + handlerCollectionHistoryGrammar, + mutationRefreshHistoryGrammar, + observeExplicitRefetchApplication, + observeFetchRecordSuccessorWork, + observeHandlerIdentity, + observeHandlerRefetchBoundary, + observeMutationRefreshLiveness, + observeQueryCacheWork, + observeRefetchCallSettlement, + reduceExplicitRefetchApplication, + reduceMutationRefreshLiveness, + reduceRefetchCallSettlement, +} from './ownership-model-oracle' +import type { Observation, Verdict } from '../../db/tests/oracle-property.js' + +import type { Item, OwnershipFixture } from './ownership-lifecycle-support.js' +import type { QueryCollectionUtils } from '../src/query.js' +import type { QueryClient } from '@tanstack/query-core' +import type { Transaction } from '@tanstack/db' +import type { + HandlerIdentityObservation, + MutationHandlerKind, + MutationRefreshLivenessModel, + RefetchApplicationOutcome, +} from './ownership-model-oracle' + +afterEach(runCleanups) +type Emit = (checkpoint: string, value: unknown) => void +async function recordBoundary( + checkpoint: string, + read: () => T, + arrived: (value: T) => boolean, + emit: Emit, +): Promise { + for (let turn = 0; turn < 200; turn++) { + if (arrived(read())) { + emit(checkpoint, read()) + return true + } + await new Promise((resolve) => setTimeout(resolve, 5)) + } + emit(checkpoint, read()) + return false +} +async function settled( + promise: Promise, +): Promise< + { outcome: 'fulfilled'; value: T } | { outcome: 'rejected'; error: unknown } +> { + return promise.then( + (value) => ({ outcome: 'fulfilled', value }), + (error) => ({ outcome: 'rejected', error }), + ) +} +function compareCuts( + actual: ReadonlyArray, + expected: ReadonlyArray, + law: string, +): Verdict { + for (let i = 0; i < expected.length; i++) { + const wanted = expected[i]! + if (!actual[i]) + return violation( + wanted.checkpoint, + 'the required observation cut did not arrive', + law, + ) + const bad = differs(wanted.checkpoint, law, actual[i], wanted) + if (bad) return bad + } + return differs('trace-length', law, actual.length, expected.length) ?? pass +} + +/** + * Public explicit refetch and mutation-phase overlap share the existing call, + * application and wait-for-graph models. The histories below author only call + * scope, source fulfillment, application fulfillment/rejection and mutation + * phase. None of their expected rows or settlements reads the production trace. + * Numeric registration coordinates preserve the old replay cases during this + * scratch extraction; final replay names are assigned by the parent. + */ +type UpperHistory = { + site: number + id: string + initialName: string + authoritativeName: string + legacyOperation: 'refetch' | 'clearError' + schedule: + | 'mutation-first' + | 'refresh-first-during-fetch' + | 'refresh-first-during-application' + invocation: + | 'manual-transaction' + | 'optimistic-action' + | 'captured-handler-collection' + | 'external-handler-overlap' + | 'external-persisted-application-overlap' + handlerKind: 'insert' | 'update' | 'delete' + accessPath: 'parameter' | 'mutation-alias' +} +const baseHistory: UpperHistory = { + site: 0, + id: '', + initialName: 'Initial', + authoritativeName: 'Authoritative', + legacyOperation: 'refetch', + schedule: 'mutation-first', + invocation: 'manual-transaction', + handlerKind: 'update', + accessPath: 'parameter', +} +const explicitHistories: Array = [ + 2572, 2599, 2647, 2710, 2797, 2906, 3018, 3205, +].map((site) => ({ ...baseHistory, site })) +explicitHistories.push( + { ...baseHistory, site: 3107, id: 'explicit-refetch-changed-application' }, + { + ...baseHistory, + site: 3107, + id: 'explicit-refetch-empty-diff-application', + initialName: 'Same', + authoritativeName: 'Same', + }, +) +const refreshHistories: Array = mutationRefreshHistoryGrammar.map( + (h) => ({ + ...baseHistory, + ...h, + site: + h.invocation === 'external-handler-overlap' + ? 3321 + : h.invocation === 'external-persisted-application-overlap' + ? 3437 + : h.invocation === 'captured-handler-collection' + ? 3720 + : 3607, + }), +) +const identityHistories: Array = + handlerCollectionHistoryGrammar.map((h) => ({ + ...baseHistory, + ...h, + site: 3962, + })) +const fact = (checkpoint: string, value: unknown): Observation => ({ + checkpoint, + value, +}) + +/** The wait-for graph has no publication edge while a mutation persists. A + * fetch settlement therefore discharges refresh, and the later application + * settlement separately discharges publication. */ +function phaseModel( + fetch: 'fulfilled' | 'rejected' = 'fulfilled', + throwOnError = true, +) { + let model: MutationRefreshLivenessModel = { + fetch: 'pending', + application: 'unaccepted', + boundary: 'fetch', + throwOnError, + } + model = reduceMutationRefreshLiveness(model, { type: 'accept-application' }) + model = reduceMutationRefreshLiveness(model, { + type: 'settle-fetch', + outcome: fetch, + }) + const held = observeMutationRefreshLiveness(model) + model = reduceMutationRefreshLiveness(model, { + type: 'settle-application', + outcome: 'fulfilled', + }) + return { held, applied: observeMutationRefreshLiveness(model) } +} +function explicitModel(h: UpperHistory): Array { + const phase = phaseModel( + h.site === 2710 ? 'rejected' : 'fulfilled', + h.site !== 2710, + ) + if (h.site === 2572) { + const model = reduceExplicitRefetchApplication( + { calls: new Map(), publicValues: new Map() }, + { type: 'start-call', callId: 1, resultIds: [], throwOnError: true }, + ) + return [ + fact('queryCalls', 0), + fact('application', observeExplicitRefetchApplication(model, 1)), + fact('results', []), + ] + } + if (h.site === 2599) { + let model = reduceExplicitRefetchApplication( + { calls: new Map(), publicValues: new Map([['query', 'Before']]) }, + { + type: 'start-call', + callId: 1, + resultIds: ['query'], + throwOnError: true, + }, + ) + model = reduceExplicitRefetchApplication(model, { + type: 'accept-result', + callId: 1, + resultId: 'query', + value: 'After', + }) + model = reduceExplicitRefetchApplication(model, { + type: 'fulfill-application', + callId: 1, + resultId: 'query', + }) + return [ + fact('application', observeExplicitRefetchApplication(model, 1)), + fact('query-data', 'After'), + ] + } + if (h.site === 2647) { + let model = createRefetchCallSettlementModel(['first', 'second'], true) + model = reduceRefetchCallSettlement(model, { + type: 'accept-application', + queryId: 'first', + applicationId: 'first-result', + }) + model = reduceRefetchCallSettlement(model, { + type: 'remove-query', + queryId: 'second', + }) + model = reduceRefetchCallSettlement(model, { + type: 'settle-fetch', + queryId: 'first', + outcome: 'fulfilled', + }) + model = reduceRefetchCallSettlement(model, { + type: 'settle-application', + applicationId: 'first-result', + outcome: 'fulfilled', + }) + return [fact('call', observeRefetchCallSettlement(model))] + } + if (h.site === 2710) + return [ + fact('fetch-complete', { fetches: 2, isFetching: false }), + fact('held', phase.held), + fact('held-row', 'Optimistic'), + fact('applied', phase.applied), + fact('result-count', 1), + fact('fetch-error', true), + ] + if (h.site === 2797) { + let model = reduceExplicitRefetchApplication( + { + calls: new Map(), + publicValues: new Map([ + ['detail', detailOnly.name], + ['list', listOnly.name], + ]), + }, + { + type: 'start-call', + callId: 1, + resultIds: ['detail', 'list'], + throwOnError: true, + }, + ) + model = reduceExplicitRefetchApplication(model, { + type: 'accept-result', + callId: 1, + resultId: 'list', + value: 'List refreshed', + }) + model = reduceExplicitRefetchApplication(model, { + type: 'fulfill-application', + callId: 1, + resultId: 'list', + }) + const intermediate = observeExplicitRefetchApplication(model, 1) + model = reduceExplicitRefetchApplication(model, { + type: 'accept-result', + callId: 1, + resultId: 'detail', + value: 'Detail refreshed', + }) + model = reduceExplicitRefetchApplication(model, { + type: 'fulfill-application', + callId: 1, + resultId: 'detail', + }) + return [ + fact('fetch-started', 4), + fact('list-applied', 'List refreshed'), + fact('list-publication', intermediate), + fact( + 'aggregate-publication', + observeExplicitRefetchApplication(model, 1), + ), + ] + } + if (h.site === 2906) { + let model = createRefetchCallSettlementModel(['query'], true) + model = reduceRefetchCallSettlement(model, { + type: 'accept-application', + queryId: 'query', + applicationId: 'own-result', + }) + model = reduceRefetchCallSettlement(model, { + type: 'accept-application', + queryId: 'query', + applicationId: 'background-result', + source: 'background', + }) + model = reduceRefetchCallSettlement(model, { + type: 'settle-application', + applicationId: 'own-result', + outcome: 'fulfilled', + }) + model = reduceRefetchCallSettlement(model, { + type: 'settle-fetch', + queryId: 'query', + outcome: 'fulfilled', + }) + model = reduceRefetchCallSettlement(model, { + type: 'settle-application', + applicationId: 'background-result', + outcome: 'rejected', + }) + return [ + fact('background-injected', true), + fact('pre-background-row', 'Own result'), + fact('call-outcome', observeRefetchCallSettlement(model).refetch), + fact('result-count', 1), + fact('successor-row', 'Successor'), + ] + } + if (h.site === 3018) + return [ + fact('initial-scan-started', 1), + fact('parallel-scan-started', 2), + fact('application-failure', { + outcome: 'rejected', + errorIsAuthored: true, + }), + fact('list-applied', 'Refreshed list'), + ] + if (h.site === 3107) + return [ + fact('fetch-complete', { fetches: 2, updates: 2, isFetching: false }), + fact('held', phase.held), + fact('applied', phase.applied), + fact('applied-row', h.authoritativeName), + ] + if (h.site === 3205) + return [ + fact('peer-fetch-started', 2), + fact('explicit-fetch-started', 3), + fact('fetch-complete', false), + fact('held', phase.held), + fact('applied', phase.applied), + fact('result-order', ['query']), + fact('applied-row', 'Authoritative'), + ] + throw Error('Unknown explicit-refetch history') +} + +/** Invocation identity selects a production path, not a different phase law. + * The phase model predicts the same cut for captured, parameter and external + * references, including refresh-first schedules. */ +function refreshModel(h: UpperHistory): Array { + const phase = phaseModel() + if (h.site === 3321) + return [ + ...(h.schedule === 'refresh-first-during-fetch' + ? [fact('refresh-started', 2)] + : []), + fact('fetch-complete', { fetches: 2, isFetching: false }), + fact('held', phase.held), + fact('applied', phase.applied), + fact('applied-row', 'Authoritative'), + ] + if (h.site === 3437) + return [ + fact('initial-scan-started', { fetches: 1, scans: 1 }), + fact('refresh-started', 2), + fact('application-preparing', { isFetching: false, scans: 2 }), + fact('refresh-settled', phase.held.refresh), + fact('held-row-is-source', false), + fact('source-applied', 'Authoritative'), + fact('applied', phase.applied), + ] + return [ + fact('refresh-started', 2), + fact('fetch-complete', false), + fact('held', phase.held), + ] +} +function identityModel(h: UpperHistory): Array { + const pending = observeHandlerRefetchBoundary({ + fetch: 'pending', + application: 'unaccepted', + throwOnError: true, + }) + const fetched = observeHandlerRefetchBoundary({ + fetch: 'fulfilled', + application: 'pending', + throwOnError: true, + }) + const names = + h.handlerKind === 'insert' + ? ['First', 'Second', 'Server inserted first', 'Server inserted second'] + : h.handlerKind === 'update' + ? ['Server updated first', 'Server updated second'] + : [] + return [ + fact('refresh-started', 2), + fact('handler-before-result', pending.refetch), + fact('handler-identity', observeHandlerIdentity(h, 2)), + fact('handler-refetch-settled', fetched.refetch), + fact('published-names', names.sort()), + ] +} +function workModel(h: UpperHistory): Array { + return h.site === 3820 + ? [ + fact( + 'foreign-work', + observeQueryCacheWork({ + collectionCount: 2, + queryOwnership: 'foreign', + }), + ), + ] + : [ + fact('cancelled-fetch-started', 2), + fact( + 'cancelled-record-work', + observeFetchRecordSuccessorWork({ state: 'cancelled' }), + ), + ] +} + +async function driver2572(history: UpperHistory, emit: Emit): Promise { + const { collection, queryFn } = createOwnershipFixture({ + id: `explicit-refetch-empty-aggregate`, + results: [], + syncMode: `on-demand`, + }) + + const results = await collection.utils.refetch({ throwOnError: true }) + + emit('queryCalls', queryFn.mock.calls.length) + emit('application', { + publicValues: {}, + refetch: `resolved`, + resultOrder: [], + }) + emit('results', results) +} + +async function driver2599(history: UpperHistory, emit: Emit): Promise { + const before = { ...shared, name: `Before` } + const after = { ...shared, name: `After` } + const { collection } = createOwnershipFixture({ + id: `explicit-refetch-unsubscribed-on-demand`, + results: [[before], [after]], + syncMode: `on-demand`, + }) + await collection._sync.loadSubset({}) + const subscription = collection.subscribeChanges(() => {}) + subscription.unsubscribe() + + const results = await collection.utils.refetch({ throwOnError: true }) + + emit('application', { + publicValues: { query: collection.get(shared.id)?.name }, + refetch: `resolved`, + resultOrder: [`query`], + }) + emit('query-data', (results[0]?.data as Array)[0]?.name) +} + +async function driver2647(history: UpperHistory, emit: Emit): Promise { + const queryClient = createQueryClient() + const first = { limit: 1 } + const second = { limit: 2 } + let unloadSecond = () => {} + let call = 0 + const queryFn = vi.fn(() => { + call++ + if (call === 3) unloadSecond() + return Promise.resolve([ + { id: String(call), category: `query`, name: `Call ${call}` }, + ]) + }) + const collection = createCollection( + queryCollectionOptions({ + id: `refetch-removes-later-query`, + queryClient, + queryKey: [`refetch-removes-later-query`], + queryFn, + getKey: (item) => item.id, + syncMode: `on-demand`, + startSync: true, + }), + ) + unloadSecond = () => collection._sync.unloadSubset(second) + cleanups.push(async () => { + await collection.cleanup() + queryClient.clear() + }) + await collection._sync.loadSubset(first) + await collection._sync.loadSubset(second) + + const results = await collection.utils.refetch({ throwOnError: true }) + + emit('call', { + refetch: `resolved`, + resultOrder: results.map((result, index) => + result === undefined ? undefined : index === 0 ? `first` : `second`, + ), + }) +} + +async function driver2710(history: UpperHistory, emit: Emit): Promise { + const initial = { ...shared, name: `Initial` } + const fetchError = new Error(`Explicit refetch fetch failed`) + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockRejectedValueOnce(fetchError) + const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) + const collection = createCollection( + queryCollectionOptions({ + id: `explicit-refetch-suppressed-fetch-failure`, + queryClient, + queryKey: [`explicit-refetch-suppressed-fetch-failure`], + queryFn, + getKey: (item) => item.id, + startSync: true, + }), + ) + const persistence = createDeferred() + const transaction = createTransaction({ + mutationFn: () => persistence.promise, + }) + cleanups.push(async () => { + persistence.resolve() + await transaction.when(`settled`).catch(() => undefined) + consoleError.mockRestore() + await collection.cleanup() + queryClient.clear() + }) + await collection.stateWhenReady() + transaction.mutate(() => { + collection.update(shared.id, (draft) => { + draft.name = `Optimistic` + }) + }) + + let actualOutcome: RefetchApplicationOutcome = `pending` + const refetch = collection.utils.refetch({ throwOnError: false }).then( + (result) => { + actualOutcome = `resolved` + return result + }, + (error: unknown) => { + actualOutcome = `rejected` + throw error + }, + ) + void refetch.catch(() => undefined) + if ( + !(await recordBoundary( + 'fetch-complete', + () => ({ + fetches: queryFn.mock.calls.length, + isFetching: collection.utils.isFetching, + }), + (v) => v.fetches === 2 && !v.isFetching, + emit, + )) + ) + return + + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + emit('held', { refresh: actualOutcome, application: `pending` }) + emit('held-row', collection.get(shared.id)?.name) + + persistence.resolve() + await transaction.when(`settled`) + const results = await refetch + + emit('applied', { refresh: actualOutcome, application: `resolved` }) + emit('result-count', results.length) + emit('fetch-error', results[0]?.isError) +} + +async function driver2797(history: UpperHistory, emit: Emit): Promise { + const firstRefresh = createDeferred>() + const secondRefresh = createDeferred>() + const refreshedDetail = { ...detailOnly, name: `Detail refreshed` } + const refreshedList = { ...listOnly, name: `List refreshed` } + const { collection, queryFn } = createOwnershipFixture({ + id: `explicit-refetch-aggregate-order`, + results: [ + [detailOnly], + [listOnly], + firstRefresh.promise, + secondRefresh.promise, + ], + }) + cleanups.push(() => { + firstRefresh.resolve([refreshedDetail]) + secondRefresh.resolve([refreshedList]) + return Promise.resolve() + }) + const detail = { where: eq(new IR.PropRef([`category`]), `detail`) } + const list = { where: eq(new IR.PropRef([`category`]), `list`) } + await collection._sync.loadSubset(detail) + await collection._sync.loadSubset(list) + + let refetchOutcome: RefetchApplicationOutcome = `pending` + const refetch = collection.utils.refetch({ throwOnError: true }).then( + (results) => { + refetchOutcome = `resolved` + return results + }, + (error: unknown) => { + refetchOutcome = `rejected` + throw error + }, + ) + void refetch.catch(() => {}) + if ( + !(await recordBoundary( + 'fetch-started', + () => queryFn.mock.calls.length, + (v) => v === 4, + emit, + )) + ) + return + + secondRefresh.resolve([refreshedList]) + if ( + !(await recordBoundary( + 'list-applied', + () => collection.get(listOnly.id)?.name, + (v) => v === 'List refreshed', + emit, + )) + ) + return + + emit('list-publication', { + publicValues: { + detail: collection.get(detailOnly.id)?.name, + list: collection.get(listOnly.id)?.name, + }, + refetch: refetchOutcome, + }) + + firstRefresh.resolve([refreshedDetail]) + const results = await refetch + + emit('aggregate-publication', { + publicValues: { + detail: collection.get(detailOnly.id)?.name, + list: collection.get(listOnly.id)?.name, + }, + refetch: refetchOutcome, + resultOrder: results.map((result) => { + return result && Array.isArray(result.data) + ? (result.data[0] as Item | undefined)?.category + : undefined + }), + }) +} + +async function driver2906(history: UpperHistory, emit: Emit): Promise { + const id = `explicit-refetch-excludes-background-application` + const queryKey = [id] as const + const initial = { ...shared, name: `Initial` } + const ownResult = { ...shared, name: `Own result` } + const background = { ...shared, name: `Background` } + const successor = { ...shared, name: `Successor` } + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockResolvedValueOnce([ownResult]) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey, + queryFn, + getKey: (item) => item.id, + startSync: true, + }), + ) + const persistence = createDeferred() + let transaction: ReturnType | undefined + let armed = false + let backgroundInjected = false + let nameBeforeBackground: string | undefined + const unsubscribeCache = queryClient.getQueryCache().subscribe((event) => { + if ( + armed && + event.type === `updated` && + event.action.type === `success` && + !event.action.manual + ) { + armed = false + nameBeforeBackground = collection.get(shared.id)?.name + transaction = createTransaction({ + mutationFn: () => persistence.promise, + }) + transaction.mutate(() => { + collection.update(shared.id, (draft) => { + draft.name = `Optimistic` + }) + }) + queryClient.setQueryData(queryKey, [background]) + backgroundInjected = true + } + }) + cleanups.push(async () => { + unsubscribeCache() + persistence.resolve() + await transaction?.when(`settled`).catch(() => undefined) + await collection.cleanup() + queryClient.clear() + }) + await collection.stateWhenReady() + + armed = true + let actualOutcome: RefetchApplicationOutcome = `pending` + const refetch = collection.utils.refetch({ throwOnError: true }).then( + (results) => { + actualOutcome = `resolved` + return results + }, + (error: unknown) => { + actualOutcome = `rejected` + throw error + }, + ) + void refetch.catch(() => undefined) + if ( + !(await recordBoundary( + 'background-injected', + () => backgroundInjected, + (v) => v === true, + emit, + )) + ) + return + emit('pre-background-row', nameBeforeBackground) + + queryClient.setQueryData(queryKey, [successor]) + + persistence.resolve() + await transaction?.when(`settled`) + const results = await refetch + + emit('call-outcome', actualOutcome) + emit('result-count', results.length) + emit('successor-row', collection.get(shared.id)?.name) +} + +async function driver3018(history: UpperHistory, emit: Emit): Promise { + const id = `explicit-refetch-fail-fast` + const detail = { where: eq(new IR.PropRef([`category`]), `detail`) } + const list = { where: eq(new IR.PropRef([`category`]), `list`) } + const listKey = [id, getLoadSubsetDemandKey(list)] + const invalidDetail = { ...detailOnly, name: `Invalid detail` } + const refreshedList = { ...listOnly, name: `Refreshed list` } + const applicationError = new Error(`Detail application failed`) + const pendingListScan = + createDeferred< + Array<{ key: string | number; value: Item; metadata?: unknown }> + >() + const scanPersistedRows = vi.fn(() => pendingListScan.promise) + const consoleError = vi.spyOn(console, `error`).mockImplementation(() => {}) + const { collection } = createOwnershipFixture({ + id, + results: [[detailOnly], [listOnly], [invalidDetail], [refreshedList]], + getKey: (item) => { + if (item.name === invalidDetail.name) throw applicationError + return item.id + }, + scanPersistedRows, + setupMetadata: (metadata) => { + const queryHash = hashKey(listKey) + metadata.collection.set(`queryCollection:gc:${queryHash}`, { + queryHash, + mode: `until-revalidated`, + }) + }, + }) + cleanups.push(() => { + pendingListScan.resolve([]) + consoleError.mockRestore() + return Promise.resolve() + }) + await collection._sync.loadSubset(detail) + const initialListLoad = Promise.resolve(collection._sync.loadSubset(list)) + void initialListLoad.catch(() => undefined) + if ( + !(await recordBoundary( + 'initial-scan-started', + () => scanPersistedRows.mock.calls.length, + (v) => v === 1, + emit, + )) + ) + return + + const refetch = collection.utils.refetch({ throwOnError: true }) + void refetch.catch(() => undefined) + if ( + !(await recordBoundary( + 'parallel-scan-started', + () => scanPersistedRows.mock.calls.length, + (v) => v === 2, + emit, + )) + ) + return + const receipt = await settled(refetch) + emit('application-failure', { + outcome: receipt.outcome, + errorIsAuthored: + receipt.outcome === 'rejected' && receipt.error === applicationError, + }) + + pendingListScan.resolve([]) + await initialListLoad.catch(() => undefined) + + if ( + !(await recordBoundary( + 'list-applied', + () => collection.get(listOnly.id)?.name, + (v) => v === 'Refreshed list', + emit, + )) + ) + return +} + +async function driver3107(history: UpperHistory, emit: Emit): Promise { + const { id, initialName, authoritativeName } = history + + const initial = { ...shared, name: initialName } + const authoritative = { ...shared, name: authoritativeName } + const { collection, queryClient, queryFn } = createOwnershipFixture({ + id, + results: [[initial], [authoritative]], + syncMode: `eager`, + }) + await collection.stateWhenReady() + + const persistence = createDeferred() + const transaction = createTransaction({ + mutationFn: () => persistence.promise, + }) + transaction.mutate(() => { + collection.update(shared.id, (draft) => { + draft.name = `Optimistic` + }) + }) + cleanups.push(async () => { + persistence.resolve() + await transaction.when(`settled`) + }) + + let refetchOutcome: RefetchApplicationOutcome = `pending` + const refetch = collection.utils.refetch({ throwOnError: true }).then( + (result) => { + refetchOutcome = `resolved` + return result + }, + (error: unknown) => { + refetchOutcome = `rejected` + throw error + }, + ) + void refetch.catch(() => {}) + + if ( + !(await recordBoundary( + 'fetch-complete', + () => ({ + fetches: queryFn.mock.calls.length, + updates: queryClient + .getQueryCache() + .find({ queryKey: [id], exact: true })?.state.dataUpdateCount, + isFetching: collection.utils.isFetching, + }), + (v) => v.fetches === 2 && v.updates === 2 && !v.isFetching, + emit, + )) + ) + return + + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + + emit('held', { + refresh: refetchOutcome, + application: + collection.get(shared.id)?.name === authoritative.name + ? `resolved` + : `pending`, + }) + + persistence.resolve() + await transaction.when(`settled`) + await refetch + + emit('applied', { refresh: refetchOutcome, application: `resolved` }) + emit('applied-row', collection.get(shared.id)?.name) +} + +async function driver3205(history: UpperHistory, emit: Emit): Promise { + const queryClient = createQueryClient() + const concurrentResult = createDeferred>() + const explicitResult = createDeferred>() + const persistence = createDeferred() + const initial = { ...shared, name: `Initial` } + const authoritative = { ...shared, name: `Authoritative` } + let call = 0 + const queryFn = vi.fn(() => { + call++ + if (call === 1) return Promise.resolve([initial]) + if (call === 2) return concurrentResult.promise + return explicitResult.promise + }) + const eager = createCollection( + queryCollectionOptions({ + id: `authority-gated-eager-refetch`, + queryClient, + queryKey: [`shared-refetch-authority`], + queryFn, + getKey: (item) => item.id, + syncMode: `eager`, + startSync: true, + }), + ) + const onDemand = createCollection( + queryCollectionOptions({ + id: `authority-gated-on-demand-writer`, + queryClient, + queryKey: [`shared-refetch-authority`], + queryFn, + getKey: (item) => item.id, + syncMode: `on-demand`, + startSync: true, + }), + ) + const transaction = createTransaction({ + mutationFn: () => persistence.promise, + }) + let transactionStarted = false + cleanups.push(async () => { + concurrentResult.resolve([authoritative]) + explicitResult.resolve([authoritative]) + persistence.resolve() + if (transactionStarted) { + await transaction.when(`settled`).catch(() => undefined) + } + await Promise.allSettled([eager.cleanup(), onDemand.cleanup()]) + queryClient.clear() + }) + await eager.stateWhenReady() + await onDemand._sync.loadSubset({}) + + transactionStarted = true + transaction.mutate(() => { + eager.update(shared.id, (draft) => { + draft.name = `Optimistic` + }) + }) + onDemand.utils.writeUpdate({ ...shared, name: `Manual` }) + void onDemand.utils.refetch({ throwOnError: true }).catch(() => {}) + if ( + !(await recordBoundary( + 'peer-fetch-started', + () => queryFn.mock.calls.length, + (v) => v === 2, + emit, + )) + ) + return + + let refetchOutcome: RefetchApplicationOutcome = `pending` + const refetch = eager.utils.refetch({ throwOnError: true }).then( + (results) => { + refetchOutcome = `resolved` + return results + }, + (error: unknown) => { + refetchOutcome = `rejected` + throw error + }, + ) + void refetch.catch(() => undefined) + if ( + !(await recordBoundary( + 'explicit-fetch-started', + () => queryFn.mock.calls.length, + (v) => v === 3, + emit, + )) + ) + return + + explicitResult.resolve([authoritative]) + if ( + !(await recordBoundary( + 'fetch-complete', + () => eager.utils.isFetching, + (v) => v === false, + emit, + )) + ) + return + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + + emit('held', { + refresh: refetchOutcome, + application: + eager.get(shared.id)?.name === authoritative.name + ? `resolved` + : `pending`, + }) + + persistence.resolve() + await transaction.when(`settled`) + const results = await refetch + + emit('applied', { refresh: refetchOutcome, application: `resolved` }) + emit( + 'result-order', + results.map(() => `query`), + ) + emit('applied-row', eager.get(shared.id)?.name) +} + +async function driver3321(history: UpperHistory, emit: Emit): Promise { + const { legacyOperation, schedule } = history + + const initial = { ...shared, name: `Initial` } + const authoritative = { ...shared, name: `Authoritative` } + const refreshResult = createDeferred>() + const handlerStarted = createDeferred() + const releaseHandler = createDeferred() + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockReturnValueOnce(refreshResult.promise) + const collection = createCollection( + queryCollectionOptions({ + id: `external-${legacyOperation}-${schedule}-overlaps-handler`, + queryClient, + queryKey: [`external-${legacyOperation}-${schedule}-overlaps-handler`], + queryFn, + getKey: (item) => item.id, + startSync: true, + onUpdate: async () => { + handlerStarted.resolve() + await releaseHandler.promise + }, + }), + ) + cleanups.push(async () => { + refreshResult.resolve([authoritative]) + releaseHandler.resolve() + await collection.cleanup() + queryClient.clear() + }) + await collection.stateWhenReady() + + let refetchOutcome: RefetchApplicationOutcome = `pending` + const startRefresh = () => { + const refresh = collection.utils.refetch({ throwOnError: true }).then( + (result) => { + refetchOutcome = `resolved` + return result + }, + (error: unknown) => { + refetchOutcome = `rejected` + throw error + }, + ) + void refresh.catch(() => {}) + return refresh + } + const startMutation = () => + collection.update(shared.id, (draft) => { + draft.name = `Optimistic` + }) + + let mutation: ReturnType + let refresh: ReturnType + if (schedule === `mutation-first`) { + mutation = startMutation() + await handlerStarted.promise + refresh = startRefresh() + } else { + refresh = startRefresh() + if ( + !(await recordBoundary( + 'refresh-started', + () => queryFn.mock.calls.length, + (v) => v === 2, + emit, + )) + ) + return + mutation = startMutation() + await handlerStarted.promise + } + refreshResult.resolve([authoritative]) + if ( + !(await recordBoundary( + 'fetch-complete', + () => ({ + fetches: queryFn.mock.calls.length, + isFetching: collection.utils.isFetching, + }), + (v) => v.fetches === 2 && !v.isFetching, + emit, + )) + ) + return + + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + + emit('held', { + refresh: refetchOutcome, + application: + collection.get(shared.id)?.name === authoritative.name + ? `resolved` + : `pending`, + }) + + releaseHandler.resolve() + await mutation.when(`settled`) + await refresh + + emit('applied', { refresh: refetchOutcome, application: `resolved` }) + emit('applied-row', collection.get(shared.id)?.name) +} + +async function driver3437(history: UpperHistory, emit: Emit): Promise { + const { legacyOperation } = history + + const id = `external-${legacyOperation}-persisted-application-overlap` + const queryHash = hashKey([id]) + const initial = { ...shared, name: `Initial` } + const authoritative = { ...shared, name: `Authoritative` } + const optimistic = { + id: `optimistic`, + category: `mutation`, + name: `Optimistic`, + } + const initialScan = + createDeferred< + Array<{ key: string | number; value: Item; metadata?: unknown }> + >() + const applicationScan = + createDeferred< + Array<{ key: string | number; value: Item; metadata?: unknown }> + >() + const refreshResult = createDeferred>() + const persistence = createDeferred() + const scanPersistedRows = vi + .fn() + .mockReturnValueOnce(initialScan.promise) + .mockReturnValueOnce(applicationScan.promise) + const { collection, queryFn } = createOwnershipFixture({ + id, + syncMode: `eager`, + results: [[initial], refreshResult.promise], + scanPersistedRows, + setupMetadata: (metadata) => + metadata.collection.set(`queryCollection:gc:${queryHash}`, { + queryHash, + mode: `until-revalidated`, + }), + }) + cleanups.push(() => { + initialScan.resolve([]) + applicationScan.resolve([]) + refreshResult.resolve([authoritative]) + persistence.resolve() + return Promise.resolve() + }) + + if ( + !(await recordBoundary( + 'initial-scan-started', + () => ({ + fetches: queryFn.mock.calls.length, + scans: scanPersistedRows.mock.calls.length, + }), + (v) => v.fetches === 1 && v.scans === 1, + emit, + )) + ) + return + + let refetchOutcome: RefetchApplicationOutcome = `pending` + const refresh = collection.utils.refetch({ throwOnError: true }).then( + (result) => { + refetchOutcome = `resolved` + return result + }, + (error: unknown) => { + refetchOutcome = `rejected` + throw error + }, + ) + void refresh.catch(() => undefined) + if ( + !(await recordBoundary( + 'refresh-started', + () => queryFn.mock.calls.length, + (v) => v === 2, + emit, + )) + ) + return + refreshResult.resolve([authoritative]) + if ( + !(await recordBoundary( + 'application-preparing', + () => ({ + isFetching: collection.utils.isFetching, + scans: scanPersistedRows.mock.calls.length, + }), + (v) => !v.isFetching && v.scans === 2, + emit, + )) + ) + return + + const transaction = createTransaction({ + mutationFn: () => persistence.promise, + }) + transaction.mutate(() => collection.insert(optimistic)) + void transaction.when(`settled`).catch(() => undefined) + applicationScan.resolve([]) + + if ( + !(await recordBoundary( + 'refresh-settled', + () => refetchOutcome, + (v) => v === 'resolved', + emit, + )) + ) + return + emit( + 'held-row-is-source', + collection.get(shared.id)?.name === authoritative.name, + ) + + persistence.resolve() + await transaction.when(`settled`) + await refresh + + if ( + !(await recordBoundary( + 'source-applied', + () => collection.get(shared.id)?.name, + (v) => v === 'Authoritative', + emit, + )) + ) + return + emit('applied', { refresh: refetchOutcome, application: `resolved` }) +} + +async function driver3607(history: UpperHistory, emit: Emit): Promise { + const { invocation, legacyOperation } = history + + const id = `mutation-refresh-${invocation}-${legacyOperation}` + const initial = { ...shared, name: `Initial` } + const authoritative = { ...shared, name: `Authoritative` } + const refreshResult = createDeferred>() + const releaseMutation = createDeferred() + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockReturnValueOnce(refreshResult.promise) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + startSync: true, + }), + ) + let refreshOutcome: RefetchApplicationOutcome = `pending` + const mutationFn = async (): Promise => { + const refresh = collection.utils.refetch({ throwOnError: true }) + await refresh.then( + () => { + refreshOutcome = `resolved` + }, + (error: unknown) => { + refreshOutcome = `rejected` + throw error + }, + ) + await releaseMutation.promise + } + const transactionReference: { + current?: Transaction> + } = {} + cleanups.push(async () => { + refreshResult.resolve([authoritative]) + if (transactionReference.current) { + // An immediate direct write drains the accepted sync result and + // breaks the deliberately reproduced cycle during RED cleanup. + collection.utils.writeUpdate(authoritative) + releaseMutation.resolve() + await transactionReference.current.when(`settled`) + } + await collection.cleanup() + queryClient.clear() + }) + await collection.stateWhenReady() + + let transaction: Transaction> + if (invocation === `manual-transaction`) { + transaction = createTransaction>({ + mutationFn, + }) + transaction.mutate(() => { + collection.update(shared.id, (draft) => { + draft.name = `Optimistic` + }) + }) + } else { + const action = createOptimisticAction({ + onMutate: () => { + collection.update(shared.id, (draft) => { + draft.name = `Optimistic` + }) + }, + mutationFn: async () => mutationFn(), + }) + transaction = action(`refresh`) + } + transactionReference.current = transaction + void transaction.when(`settled`).catch(() => undefined) + if ( + !(await recordBoundary( + 'refresh-started', + () => queryFn.mock.calls.length, + (v) => v === 2, + emit, + )) + ) + return + + refreshResult.resolve([authoritative]) + if ( + !(await recordBoundary( + 'fetch-complete', + () => collection.utils.isFetching, + (v) => v === false, + emit, + )) + ) + return + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + + emit('held', { + refresh: refreshOutcome, + application: + collection.get(shared.id)?.name === authoritative.name + ? `resolved` + : `pending`, + }) +} + +async function driver3720(history: UpperHistory, emit: Emit): Promise { + const { legacyOperation } = history + + const id = `captured-handler-refresh-${legacyOperation}` + const initial = { ...shared, name: `Initial` } + const authoritative = { ...shared, name: `Authoritative` } + const refreshResult = createDeferred>() + const releaseHandler = createDeferred() + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce([initial]) + .mockReturnValueOnce(refreshResult.promise) + let refreshOutcome: RefetchApplicationOutcome = `pending` + const collectionReference: { current?: OwnershipFixture[`collection`] } = {} + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + startSync: true, + onUpdate: async () => { + const capturedCollection = collectionReference.current + if (!capturedCollection) { + throw new Error(`The captured Collection is not available`) + } + const refresh = capturedCollection.utils.refetch({ + throwOnError: true, + }) + await refresh.then( + () => { + refreshOutcome = `resolved` + }, + (error: unknown) => { + refreshOutcome = `rejected` + throw error + }, + ) + await releaseHandler.promise + }, + }), + ) + collectionReference.current = collection + const transactionReference: { + current?: ReturnType + } = {} + cleanups.push(async () => { + refreshResult.resolve([authoritative]) + if (transactionReference.current) { + collection.utils.writeUpdate(authoritative) + releaseHandler.resolve() + await transactionReference.current.when(`settled`) + } + await collection.cleanup() + queryClient.clear() + }) + await collection.stateWhenReady() + + const transaction = collection.update(shared.id, (draft) => { + draft.name = `Optimistic` + }) + transactionReference.current = transaction + void transaction.when(`settled`).catch(() => undefined) + if ( + !(await recordBoundary( + 'refresh-started', + () => queryFn.mock.calls.length, + (v) => v === 2, + emit, + )) + ) + return + + refreshResult.resolve([authoritative]) + if ( + !(await recordBoundary( + 'fetch-complete', + () => collection.utils.isFetching, + (v) => v === false, + emit, + )) + ) + return + for (let turn = 0; turn < 20; turn++) await Promise.resolve() + + emit('held', { + refresh: refreshOutcome, + application: + collection.get(shared.id)?.name === authoritative.name + ? `resolved` + : `pending`, + }) +} + +async function driver3820(history: UpperHistory, emit: Emit): Promise { + const queryClient = createQueryClient() + const baselineClient = createQueryClient() + const first = createCollection( + queryCollectionOptions({ + id: `foreign-query-work-first`, + queryClient, + queryKey: [`foreign-query-work-first`], + queryFn: () => Promise.resolve([]), + getKey: (item) => item.id, + startSync: true, + }), + ) + const second = createCollection( + queryCollectionOptions({ + id: `foreign-query-work-second`, + queryClient, + queryKey: [`foreign-query-work-second`], + queryFn: () => Promise.resolve([]), + getKey: (item) => item.id, + startSync: true, + }), + ) + await Promise.all([first.stateWhenReady(), second.stateWhenReady()]) + + const nativeQueueMicrotask = globalThis.queueMicrotask + let scheduledMicrotasks = 0 + const queueMicrotaskSpy = vi + .spyOn(globalThis, `queueMicrotask`) + .mockImplementation((callback) => { + scheduledMicrotasks++ + nativeQueueMicrotask(callback) + }) + cleanups.push(async () => { + queueMicrotaskSpy.mockRestore() + await Promise.all([first.cleanup(), second.cleanup()]) + queryClient.clear() + baselineClient.clear() + }) + + const measureFetchMicrotasks = async ( + client: QueryClient, + queryKey: ReadonlyArray, + ): Promise => { + scheduledMicrotasks = 0 + await client.fetchQuery({ queryKey, queryFn: () => Promise.resolve([]) }) + for (let turn = 0; turn < 5; turn++) await Promise.resolve() + return scheduledMicrotasks + } + const baseline = await measureFetchMicrotasks(baselineClient, [ + `baseline-foreign-query`, + ]) + const withCollections = await measureFetchMicrotasks(queryClient, [ + `foreign-query`, + ]) + + emit('foreign-work', { collectionMicrotasks: withCollections - baseline }) +} + +async function driver3889(history: UpperHistory, emit: Emit): Promise { + const measureSuccessorFetchMicrotasks = async ( + cancelFirst: boolean, + ): Promise => { + const id = `cancelled-fetch-record-${cancelFirst}` + const initial = { ...shared, name: `Initial` } + const authoritative = { ...shared, name: `Authoritative` } + const cancelledResult = createDeferred>() + const queryClient = createQueryClient() + const queryFn = vi.fn<() => Promise>>() + queryFn.mockResolvedValueOnce([initial]) + if (cancelFirst) queryFn.mockReturnValueOnce(cancelledResult.promise) + queryFn.mockResolvedValue([authoritative]) + const collection = createCollection( + queryCollectionOptions({ + id, + queryClient, + queryKey: [id], + queryFn, + getKey: (item) => item.id, + syncMode: `eager`, + startSync: true, + }), + ) + + try { + await collection.stateWhenReady() + if (cancelFirst) { + const cancelledRefetch = collection.utils.refetch({ + throwOnError: true, + }) + if ( + !(await recordBoundary( + 'cancelled-fetch-started', + () => queryFn.mock.calls.length, + (v) => v === 2, + emit, + )) + ) + throw new Error('The cancelled fetch did not start') + await queryClient.cancelQueries({ queryKey: [id], exact: true }) + await cancelledRefetch + for (let turn = 0; turn < 5; turn++) await Promise.resolve() + } + + const nativeQueueMicrotask = globalThis.queueMicrotask + let scheduledMicrotasks = 0 + const queueMicrotaskSpy = vi + .spyOn(globalThis, `queueMicrotask`) + .mockImplementation((callback) => { + scheduledMicrotasks++ + nativeQueueMicrotask(callback) + }) + try { + await collection.utils.refetch({ throwOnError: true }) + for (let turn = 0; turn < 5; turn++) await Promise.resolve() + return scheduledMicrotasks + } finally { + queueMicrotaskSpy.mockRestore() + } + } finally { + cancelledResult.resolve([authoritative]) + await collection.cleanup() + queryClient.clear() + } + } + + const baseline = await measureSuccessorFetchMicrotasks(false) + const afterCancellation = await measureSuccessorFetchMicrotasks(true) + + emit('cancelled-record-work', { + extraCleanupMicrotasks: afterCancellation - baseline, + }) +} + +async function driver3962(history: UpperHistory, emit: Emit): Promise { + const { handlerKind, accessPath, legacyOperation } = history + const initial = [ + { id: `first`, category: `shared`, name: `First` }, + { id: `second`, category: `shared`, name: `Second` }, + ] + const inserted = [ + { id: `inserted-first`, category: `shared`, name: `Client first` }, + { id: `inserted-second`, category: `shared`, name: `Client second` }, + ] + const authoritative = + handlerKind === `insert` + ? [ + ...initial, + { ...inserted[0]!, name: `Server inserted first` }, + { ...inserted[1]!, name: `Server inserted second` }, + ] + : handlerKind === `update` + ? [ + { ...initial[0]!, name: `Server updated first` }, + { ...initial[1]!, name: `Server updated second` }, + ] + : [] + const refresh = createDeferred>() + const queryClient = createQueryClient() + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValueOnce(initial) + .mockReturnValueOnce(refresh.promise) + const externalCollectionReference: { current?: object } = {} + const inspectIdentity = createDeferred() + const externalTransactionReference: { current?: object } = {} + let identityObservation: HandlerIdentityObservation | undefined + let handlerOutcome: RefetchApplicationOutcome = `pending` + const handleMutation = async ( + actualHandlerKind: MutationHandlerKind, + handlerCollection: { + utils: Pick< + QueryCollectionUtils, + `refetch` + > + }, + transaction: { + mutations: ReadonlyArray<{ collection: unknown }> + }, + ): Promise => { + await inspectIdentity.promise + const externalCollection = externalCollectionReference.current + if (!externalCollection) { + throw new Error(`The external Collection is not available`) + } + const externalTransaction = externalTransactionReference.current + if (!externalTransaction) { + throw new Error(`The external transaction is not available`) + } + const externalMutations = (externalTransaction as typeof transaction) + .mutations + const aliases = transaction.mutations.map( + ({ collection: mutationCollection }) => mutationCollection as object, + ) + const externalMap = new Map([ + [externalCollection, `external`], + ]) + const parameterMap = new Map([ + [handlerCollection, `parameter`], + ]) + const externalWeakMap = new WeakMap([ + [externalCollection, `external`], + ]) + const parameterWeakMap = new WeakMap([ + [handlerCollection, `parameter`], + ]) + identityObservation = { + handlerKind: actualHandlerKind, + accessPath, + matchingAliasCount: aliases.length, + transactionIsExternal: transaction === externalTransaction, + allMutationsPreserveIdentity: transaction.mutations.every( + (mutation, index) => mutation === externalMutations[index], + ), + parameterIsExternal: handlerCollection === externalCollection, + allAliasesMatchParameter: aliases.every( + (alias) => alias === handlerCollection, + ), + anyAliasMatchesExternal: aliases.some( + (alias) => alias === externalCollection, + ), + externalMapMatchesAnyAlias: aliases.some((alias) => + externalMap.has(alias), + ), + parameterMapMatchesAllAliases: aliases.every((alias) => + parameterMap.has(alias), + ), + externalWeakMapMatchesAnyAlias: aliases.some((alias) => + externalWeakMap.has(alias), + ), + parameterWeakMapMatchesAllAliases: aliases.every((alias) => + parameterWeakMap.has(alias), + ), + } + const refetchCollection = + accessPath === `parameter` + ? handlerCollection + : (aliases[0] as typeof handlerCollection) + const boundary = refetchCollection.utils.refetch({ throwOnError: true }) + await boundary.then( + () => { + handlerOutcome = `resolved` + }, + (error: unknown) => { + handlerOutcome = `rejected` + throw error + }, + ) + } + const collection = createCollection( + queryCollectionOptions({ + id: `handler-scope-${handlerKind}-${accessPath}-${legacyOperation}`, + queryClient, + queryKey: [`handler-scope`, handlerKind, accessPath, legacyOperation], + queryFn, + getKey: (item) => item.id, + startSync: true, + onInsert: ({ transaction, collection: handlerCollection }) => + handleMutation(`insert`, handlerCollection, transaction), + onUpdate: ({ transaction, collection: handlerCollection }) => + handleMutation(`update`, handlerCollection, transaction), + onDelete: ({ transaction, collection: handlerCollection }) => + handleMutation(`delete`, handlerCollection, transaction), + }), + ) + externalCollectionReference.current = collection + cleanups.push(async () => { + inspectIdentity.resolve() + refresh.resolve(authoritative) + await collection.cleanup() + queryClient.clear() + }) + await collection.stateWhenReady() + + const mutation = + handlerKind === `insert` + ? collection.insert(inserted) + : handlerKind === `update` + ? collection.update( + initial.map(({ id }) => id), + (drafts) => { + drafts[0]!.name = `Client updated first` + drafts[1]!.name = `Client updated second` + }, + ) + : collection.delete(initial.map(({ id }) => id)) + externalTransactionReference.current = mutation + inspectIdentity.resolve() + void mutation.when(`settled`).catch(() => undefined) + if ( + !(await recordBoundary( + 'refresh-started', + () => queryFn.mock.calls.length, + (v) => v === 2, + emit, + )) + ) + return + emit('handler-before-result', handlerOutcome) + if (!identityObservation) { + throw new Error(`The mutation handler did not record Collection identity`) + } + emit('handler-identity', identityObservation) + + refresh.resolve(authoritative) + + if ( + !(await recordBoundary( + 'handler-refetch-settled', + () => handlerOutcome, + (v) => v === 'resolved', + emit, + )) + ) + return + + await mutation.when(`settled`) + + emit( + 'published-names', + [...collection.values()].map(({ name }) => name).sort(), + ) +} +const upperTitles: Record = { + '2572': 'resolves a refetch with no tracked Query results', + '2599': 'applies a successful on-demand refetch without Query listeners', + '2647': 'skips a snapshotted Query removed while an earlier refetch starts', + '2710': + 'settles a suppressed fetch failure at the fetch boundary while a mutation persists', + '2797': 'waits for every Query result and preserves tracked-key order', + '2906': 'does not adopt a background application from the same Query', + '3018': 'rejects before an independent accepted application settles', + '3107': + 'settles a $caseName accepted result at fetch and applies it after mutation', + '3205': + 'settles an eager refetch at fetch while another collection refetches', + '3321': + 'settles an external refetch at fetch with $schedule overlap (legacy replay: $legacyOperation)', + '3437': + 'settles an external refetch when mutation starts during persisted application preparation (legacy replay: $legacyOperation)', + '3607': + 'settles a $invocation refetch at the fetch boundary (legacy replay: $legacyOperation)', + '3720': + 'settles a captured handler Collection refetch at the fetch boundary (legacy replay: $legacyOperation)', + '3820': 'does not schedule Collection work for a foreign Query', + '3889': + 'does not defer reverted fetch record retirement to a successor fetch', + '3962': + 'preserves $handlerKind handler identity through $accessPath refetch (legacy replay: $legacyOperation)', +} +function upperLabel(h: UpperHistory) { + return upperTitles[h.site]!.replace(/\$(\w+)/g, (_match, key: string) => + key === 'caseName' + ? h.initialName === 'Same' + ? 'empty-diff' + : 'changed' + : String(h[key as keyof UpperHistory]), + ) +} +const driverBySite: Record< + number, + (h: UpperHistory, emit: Emit) => Promise +> = { + 2572: driver2572, + 2599: driver2599, + 2647: driver2647, + 2710: driver2710, + 2797: driver2797, + 2906: driver2906, + 3018: driver3018, + 3107: driver3107, + 3205: driver3205, + 3321: driver3321, + 3437: driver3437, + 3607: driver3607, + 3720: driver3720, + 3820: driver3820, + 3889: driver3889, + 3962: driver3962, +} +async function observeUpper(h: UpperHistory, emit: Emit): Promise { + try { + await driverBySite[h.site]!(h, emit) + } finally { + await runCleanups() + } +} + +/** These are the same finite authored paths and replay coordinates as the old + * owner. Their models are unchanged reducers, and every old work, identity, + * cache and public-row cut is emitted before cleanup. */ +const commonAuthority = + 'docs/collections/query-collection.md#explicit-refetch-inside-a-handler and #utility-methods' +oracleProperty({ + id: 'query-db.ownership.explicit-refetch', + law: { + statement: + 'A public refetch observes its own tracked Query calls and applications, preserves key order, and uses the fetch boundary while a mutation persists.', + authority: commonAuthority, + limits: + 'The original 10 finite histories with controlled Query results, retained-scan holds and one real QueryClient; arbitrary polling, retry and external I/O remain separate paths.', + owner: 'Query DB ownership lifecycle', + }, + histories: explicitHistories, + label: upperLabel, + model: explicitModel, + driver: observeUpper, + check: (_h, trace, expected) => + compareCuts(trace, expected, 'explicit refetch obligation'), +}) +oracleProperty({ + id: 'query-db.ownership.refresh-overlap', + law: { + statement: + 'Every refetch overlapping persisting mutation work uses the Query fetch boundary, in either start order and through every captured Collection identity.', + authority: commonAuthority, + limits: + 'The original 12 finite invocation/schedule/replay cases; one mutation, one physical refresh and controlled local persistence preparation.', + owner: 'Query DB ownership lifecycle', + }, + histories: refreshHistories, + label: upperLabel, + model: refreshModel, + driver: observeUpper, + check: (_h, trace, expected) => + compareCuts(trace, expected, 'mutation refresh boundary'), +}) +oracleProperty({ + id: 'query-db.ownership.handler-identity', + law: { + statement: + 'Handler Collection, transaction and mutation aliases retain their external identity, and the handler refetch settles at its fetch boundary before publication.', + authority: commonAuthority, + limits: + 'The original 12 insert/update/delete, parameter/mutation-alias and legacy replay cases with two mutations in one direct handler.', + owner: 'Query DB ownership lifecycle', + }, + histories: identityHistories, + label: upperLabel, + model: identityModel, + driver: observeUpper, + check: (_h, trace, expected) => + compareCuts(trace, expected, 'handler public identity'), +}) +oracleProperty({ + id: 'query-db.ownership.work', + law: { + statement: + 'A foreign Query schedules no Collection cache work, and a cancelled fetch leaves no deferred cleanup work for its successor.', + authority: + 'The ownership/cache-work and cancelled-fetch lifecycle laws documented in ownership-lifecycle.oracle.test.ts.', + limits: + 'The original two deterministic microtask diagnostics after subtracting Query Core baseline work; these are work counts, not elapsed-time bounds.', + owner: 'Query DB ownership lifecycle', + }, + histories: [ + { ...baseHistory, site: 3820 }, + { ...baseHistory, site: 3889 }, + ], + label: upperLabel, + model: workModel, + driver: observeUpper, + check: (_h, trace, expected) => + compareCuts(trace, expected, 'Query ownership work'), + calibrations: [ + { + name: 'foreign Query treated as owned', + law: 'Query ownership work', + corrupt: (trace, h) => + h.site === 3820 + ? trace.map((o) => + o.checkpoint === 'foreign-work' + ? { + ...o, + value: observeQueryCacheWork({ + collectionCount: 2, + queryOwnership: 'owned', + }), + } + : o, + ) + : undefined, + }, + { + name: 'cancelled record retained into successor', + law: 'Query ownership work', + corrupt: (trace, h) => + h.site === 3889 + ? trace.map((o) => + o.checkpoint === 'cancelled-record-work' + ? { + ...o, + value: observeFetchRecordSuccessorWork({ state: 'active' }), + } + : o, + ) + : undefined, + }, + ], +}) + +declareOracle({ + owns: ['src/query.ts#queryCollectionOptions'], + model: [phaseModel, explicitModel, refreshModel, identityModel, workModel], + check: [compareCuts], + grammar: [explicitHistories, refreshHistories, identityHistories], +}) diff --git a/packages/query-db-collection/tests/persisted-direct-ownership-driver.ts b/packages/query-db-collection/tests/persisted-direct-ownership-driver.ts new file mode 100644 index 0000000000..0b12da86ed --- /dev/null +++ b/packages/query-db-collection/tests/persisted-direct-ownership-driver.ts @@ -0,0 +1,253 @@ +/** Production receiving path for the primary Query direct-row ownership law. + * The first physical response is held across one accepted direct write. + * Receipts identify application; public and durable observations are copied + * before cleanup. Post-cleanup durability and Query events are diagnostics. + */ +import { QueryClient, hashKey } from '@tanstack/query-core' +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { + trackStartupSync, + untilStartup, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { queryCollectionOptions } from '../src/query' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' +import type { + DirectOwnershipHistory, + DirectOwnershipTrace, + DirectOwnershipResponse as Response, + DirectOwnershipRow as Row, + OwnedDirectRow as SeenRow, +} from './persisted-direct-ownership-oracle' + +function owners(metadata: unknown, sourceHash: string): Array { + if (!metadata || typeof metadata !== 'object') return [] + const query = (metadata as Record).queryCollection + if (!query || typeof query !== 'object') return [] + const claims = (query as Record).owners + if (!claims || typeof claims !== 'object') return [] + return Object.keys(claims) + .map((owner) => (owner === sourceHash ? 'source' : owner)) + .sort() +} +function gate() { + let release!: () => void + const promise = new Promise((resolve) => { + release = resolve + }) + return { promise, release } +} +const sortRows = (rows: Array) => + rows.sort((left, right) => left.id.localeCompare(right.id)) + +export async function observePersistedDirectOwnership( + persistence: PersistedCollectionPersistence, + h: DirectOwnershipHistory, + id: string, + observe?: (checkpoint: string, value: unknown) => void, +): Promise { + const trace: DirectOwnershipTrace = [] + const record = (checkpoint: string, value: unknown) => { + trace.push({ checkpoint, value }) + observe?.(checkpoint, value) + } + + const queryKey = [id] + const queryHash = hashKey(queryKey) + const savedKey = h.overlapsSaved ? 'x' : 'saved' + const seed = createCollection( + persistedCollectionOptions({ + id, + getKey: (row) => row.id, + persistence, + sync: { + sync: ({ begin, write, commit, markReady, metadata }) => { + begin() + write({ + type: 'insert', + value: { id: savedKey, value: h.savedValue }, + }) + metadata!.row.set(savedKey, { + queryCollection: { owners: { [queryHash]: true } }, + }) + if (h.retained) + metadata!.collection.set('queryCollection:gc:' + queryHash, { + queryHash, + mode: 'until-revalidated', + }) + commit() + markReady() + }, + }, + }), + ) + await withOracleCleanup(() => seed.preload(), [() => seed.cleanup()]) + const client = new QueryClient({ + defaultOptions: { + queries: { retry: false, staleTime: Infinity, gcTime: Infinity }, + }, + }) + const response = gate() + let calls = 0 + let responseReturned = false + let resultApplied: Promise | undefined + const events: Array = [] + client.getQueryCache().subscribe((event) => { + if (event.type === 'updated') + events.push({ + event: 'query-' + event.action.type, + fetching: event.query.state.fetchStatus, + data: event.query.state.data, + }) + }) + const decode = (value: Response): Array => + Array.isArray(value) ? value : value.edges.map(({ node }) => ({ ...node })) + const options = queryCollectionOptions({ + id, + queryKey, + queryClient: client, + getKey: (row: Row) => row.id, + select: decode, + enabled: h.write === 'after-fetch-start', + persistedGcTime: Infinity, + queryFn: async (): Promise => { + calls++ + await response.promise + responseReturned = true + const values = [ + ...(!h.overlapsSaved && h.keepsSaved + ? [{ id: 'saved', value: h.sourceValue }] + : []), + ...(h.includesDirect ? [{ id: 'x', value: h.sourceValue }] : []), + ] + return h.shape === 'raw' + ? values + : { edges: values.map((node) => ({ node })) } + }, + }) + const sourceSync = options.sync.sync + let writes: Array = [] + const tracked = trackStartupSync({ + ...options.sync, + sync: (params) => + sourceSync({ + ...params, + write: (message) => { + // Observe the actual adapter input before the persistence wrapper or + // core can normalize it. Undefined remains an observable violation. + record('sync-write', message.type) + writes.push(message) + params.write(message) + if ( + responseReturned && + 'value' in message && + message.value.id === 'x' + ) + // A row-bearing write proves the fetched row crossed the boundary. + // Its operation remains an independent protocol judgment. + record('reach', 'fetched-row-write') + }, + commit: (signal) => { + const receipt = params.commit(signal) + events.push({ event: 'commit', responseReturned, writes }) + writes = [] + if (responseReturned) + resultApplied = Promise.resolve(receipt).then(() => undefined) + return receipt + }, + }), + }) + const collection = createCollection( + persistedCollectionOptions({ + ...options, + sync: tracked.sync, + persistence, + }), + ) + const durable = async () => { + if (!persistence.adapter.scanRows) + throw new Error('Candidate requires persisted row metadata scans') + return sortRows( + ( + await persistence.adapter.scanRows({ + kind: 'eager', + collectionId: id, + }) + ).map(({ value, metadata }) => ({ + ...(value as Row), + owners: owners(metadata, queryHash), + })), + ) + } + await withOracleCleanup(async () => { + const preload = collection.preload() + void preload.catch(() => {}) + await untilStartup( + () => + collection.has(savedKey) && + calls === Number(h.write === 'after-fetch-start'), + ) + const direct = { id: 'x', value: h.directValue } + if (h.operation === 'insert') await collection.utils.writeInsert(direct) + else await collection.utils.writeUpsert(direct) + const fetched = + h.write === 'before-fetch' + ? collection.utils.refetch({ throwOnError: true }) + : undefined + void fetched?.catch(() => {}) + response.release() + await untilStartup(() => resultApplied !== undefined) + await resultApplied + await tracked.initialResult + await fetched + await preload + record('diagnostics', { events, fetching: client.isFetching() }) + record( + 'first', + sortRows( + Array.from(collection.values(), ({ id: key, value }) => ({ + id: key, + value, + owners: owners(collection._state.syncedMetadata.get(key), queryHash), + })), + ), + ) + record('firstDurable', await durable()) + for (const state of [ + h.shape, + h.retained ? 'retained' : 'no-retention', + h.write, + h.overlapsSaved + ? 'overlapping-saved-direct-key' + : 'distinct-saved-direct-key', + h.includesDirect ? 'source-includes-direct' : 'source-omits-direct', + ...(!h.retained && h.write === 'after-fetch-start' + ? ['no-retention-direct-after-fetch'] + : []), + ]) + record('reach', state) + if ( + h.retained && + !h.overlapsSaved && + h.write === 'before-fetch' && + h.includesDirect && + h.directValue !== h.sourceValue + ) + record('reach', 'retained-direct-before-returned-key') + if ( + h.overlapsSaved && + h.retained && + h.write === 'before-fetch' && + h.includesDirect && + h.sourceValue === h.savedValue && + h.directValue !== h.savedValue + ) + record('reach', 'returned-value-equals-saved-after-direct-change') + await collection.cleanup() + record('afterCleanupDurableDiagnostic', await durable()) + record('completed', true) + }, [response.release, () => collection.cleanup(), () => client.clear()]) + + return trace +} diff --git a/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts b/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts new file mode 100644 index 0000000000..c4aebf0169 --- /dev/null +++ b/packages/query-db-collection/tests/persisted-direct-ownership-oracle.test.ts @@ -0,0 +1,29 @@ +/** Controlled and real SQLite receivers of the primary ownership judgment. */ +import { describe } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { oracleProperty } from '../../db/tests/oracle-property' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' +import { persistedDirectOwnershipOptions } from './persisted-direct-ownership-oracle' +import { observePersistedDirectOwnership } from './persisted-direct-ownership-driver' + +for (const host of ['controlled', 'node-sqlite'] as const) + describe(host, () => { + const options = persistedDirectOwnershipOptions(async (h, id, observe) => { + const storage = createStartupHost(host) + await withOracleCleanup(async () => { + await observePersistedDirectOwnership( + storage.persistence, + h, + id, + observe, + ) + }, [() => storage.close()]) + }, host) + oracleProperty({ + ...options, + check: (h, trace, expected) => options.check(h, trace, expected), + }) + }) + +declareOracle({ owns: ['src/query.ts#queryCollectionOptions'] }) diff --git a/packages/query-db-collection/tests/persisted-direct-ownership-oracle.ts b/packages/query-db-collection/tests/persisted-direct-ownership-oracle.ts new file mode 100644 index 0000000000..c63c9681ec --- /dev/null +++ b/packages/query-db-collection/tests/persisted-direct-ownership-oracle.ts @@ -0,0 +1,521 @@ +/** + * Persisted receiving extension of the primary Query ownership oracle. + * + * The Query guide's Direct Writes and Query Sync law orders direct writes and + * physical fetches per key. A later complete fetch can claim or remove a direct + * row; a pre-write result preserves the row without inventing source ownership. + * Understanding the Data Stores also requires an acquired Collection to project + * the successful fetched response. With no mutation, refetch owes application. + * + * The primary owner's direct-row lifetime judgment owns these claims. This + * extension adds exact public/durable ownership at the first eager result and + * crosses saved retention with same-run ownerless writes. Its value judgment + * reuses that owner's independent mergeDirectWritesOverFetch model. The small + * ownership projection adds the receiving metadata boundary; it copies no + * production ownership classifier. It creates no competing primary owner. + * + * One first eager response and one inserted/upserted direct key are modeled. + * Raw/derived responses, retained/unretained saved baselines, both authored + * fetch/write orders, inclusion/omission and independent saved/direct/source numeric values vary. Saved and direct keys may overlap; overlapping histories use legal upserts. + * No failed I/O, arbitrary mutations, peers, or eager subset unload is claimed. + * At every Query adapter write invocation, the declared operation must satisfy + * SyncConfig.write's ChangeMessageOrDeleteKeyMessage contract: insert, update, + * or delete. This boundary law constrains neither write counts nor ordering. + */ +import fc from 'fast-check' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { mergeDirectWritesOverFetch } from './direct-write-precedence-oracle' +import type { + ConstructedHistory, + Observation, + OraclePropertyOptions, + Verdict, +} from '../../db/tests/oracle-property' + +export type DirectOwnershipRow = { id: string; value: number } +export type DirectOwnershipResponse = + Array | { edges: Array<{ node: DirectOwnershipRow }> } +export type DirectOwnershipHistory = { + shape: 'raw' | 'derived' + retained: boolean + write: 'before-fetch' | 'after-fetch-start' + operation: 'insert' | 'upsert' + includesDirect: boolean + keepsSaved: boolean + directValue: number + sourceValue: number + savedValue: number + overlapsSaved: boolean +} +export type OwnedDirectRow = DirectOwnershipRow & { owners: Array } +export type DirectOwnershipTrace = Array +export const directOwnershipHistoryArb = fc + .record({ + shape: fc.constantFrom('raw' as const, 'derived' as const), + retained: fc.boolean(), + write: fc.constantFrom( + 'before-fetch' as const, + 'after-fetch-start' as const, + ), + operation: fc.constantFrom('insert' as const, 'upsert' as const), + includesDirect: fc.boolean(), + keepsSaved: fc.boolean(), + directValue: fc.integer({ min: -10, max: 10 }), + sourceValue: fc.integer({ min: -10, max: 10 }), + savedValue: fc.integer({ min: -10, max: 10 }), + overlapsSaved: fc.boolean(), + }) + .filter((h) => !h.overlapsSaved || h.operation === 'upsert') +export const directOwnershipStates = [ + 'raw', + 'derived', + 'retained', + 'no-retention', + 'before-fetch', + 'after-fetch-start', + 'source-includes-direct', + 'source-omits-direct', + 'no-retention-direct-after-fetch', + 'retained-direct-before-returned-key', + 'fetched-row-write', + 'overlapping-saved-direct-key', + 'distinct-saved-direct-key', + 'returned-value-equals-saved-after-direct-change', +] as const + +/** Authored causal order predicts values; only a later result may claim x. */ +export function expectedDirectOwnership( + h: DirectOwnershipHistory, +): Array { + const fetched = [ + ...(!h.overlapsSaved && h.keepsSaved + ? [{ id: 'saved', value: h.sourceValue }] + : []), + ...(h.includesDirect ? [{ id: 'x', value: h.sourceValue }] : []), + ] + const values = mergeDirectWritesOverFetch( + fetched, + h.write === 'after-fetch-start' ? [{ id: 'x', value: h.directValue }] : [], + ) + // Primary direct-row lifetime law: an accepted update/upsert preserves the + // row's existing owners. The guide assigns no inferred owner only to a new + // direct insert. Thus an older fetch cannot claim a new key, while an + // updated saved key retains its already established source owner. + return Object.entries(values) + .map(([id, value]) => ({ + id, + value, + owners: + id === 'x' && h.write === 'after-fetch-start' && !h.overlapsSaved + ? [] + : ['source'], + })) + .sort((a, b) => a.id.localeCompare(b.id)) +} + +export const directOwnershipHistories: Array = ( + ['raw', 'derived'] as const +) + .flatMap((shape) => + [false, true].flatMap((retained) => + (['before-fetch', 'after-fetch-start'] as const).flatMap((write) => + (['insert', 'upsert'] as const).flatMap((operation) => + [false, true].flatMap((includesDirect) => + [false, true].map((keepsSaved) => ({ + shape, + retained, + write, + operation, + includesDirect, + keepsSaved, + directValue: 0, + sourceValue: 1, + savedValue: 1, + overlapsSaved: false, + })), + ), + ), + ), + ), + ) + .flatMap((h) => [ + h, + ...(h.operation === 'upsert' ? [{ ...h, overlapsSaved: true }] : []), + ]) + +// Compare captured prefixes before asking for completion. A later driver or +// cleanup error must not erase an earlier wrong public/durable observation. +export function checkDirectOwnership( + h: DirectOwnershipHistory, + trace: ReadonlyArray, + expected = expectedDirectOwnership(h), +): Verdict { + for (const o of trace) { + // The public sync input type requires an operation even when a receiving + // implementation could produce the right rows without that discriminant. + // Judge the raw boundary value; do not infer an operation from row state. + if ( + o.checkpoint === 'sync-write' && + o.value !== 'insert' && + o.value !== 'update' && + o.value !== 'delete' + ) + return { + kind: 'violation', + checkpoint: o.checkpoint, + law: 'declared sync-write operation', + difference: + 'SyncConfig.write requires insert, update, or delete; observed ' + + JSON.stringify(o.value), + } + if (o.checkpoint !== 'first' && o.checkpoint !== 'firstDurable') continue + const actual = o.value as Array + const projectOwners = (rows: Array) => + rows.map(({ id, owners }) => ({ id, owners })) + if ( + JSON.stringify(projectOwners(actual)) !== + JSON.stringify(projectOwners(expected)) + ) + return { + kind: 'violation', + checkpoint: o.checkpoint, + law: 'direct row ownership', + difference: + 'expected ' + + JSON.stringify(projectOwners(expected)) + + ', observed ' + + JSON.stringify(projectOwners(actual)), + } + if (JSON.stringify(actual) !== JSON.stringify(expected)) + return { + kind: 'violation', + checkpoint: o.checkpoint, + law: 'direct row value', + difference: + 'expected ' + + JSON.stringify(expected) + + ', observed ' + + JSON.stringify(actual), + } + } + if (!trace.some((o) => o.checkpoint === 'completed')) + return { + kind: 'violation', + checkpoint: 'completed', + law: 'receiving completion', + difference: + 'receiving driver returned without completing its observations', + } + for (const cut of ['first', 'firstDurable']) + if (!trace.some((o) => o.checkpoint === cut)) + return { + kind: 'violation', + checkpoint: cut, + law: 'direct row observation', + difference: 'required receiving observation is missing', + } + return { kind: 'pass' } +} + +export type DirectOwnershipReceiver = ( + h: DirectOwnershipHistory, + id: string, + observe: (checkpoint: string, value: unknown) => void, +) => Promise + +/** Each receiving path gets fresh constructions and its own obligation ledger. */ +export function persistedDirectOwnershipOptions( + receive: DirectOwnershipReceiver, + receiver: string, +): OraclePropertyOptions> { + const obligations = new WeakMap< + DirectOwnershipHistory, + ReadonlyArray + >() + // Fixed inputs also owe the states authored in their histories. This checks + // their real receipt-cut observations and lets checker calibrations use an + // early eligible fixed input instead of replaying every fixed input first. + for (const h of directOwnershipHistories) + obligations.set(h, [ + h.shape, + h.retained ? 'retained' : 'no-retention', + h.write, + h.includesDirect ? 'source-includes-direct' : 'source-omits-direct', + h.overlapsSaved + ? 'overlapping-saved-direct-key' + : 'distinct-saved-direct-key', + ...(!h.retained && h.write === 'after-fetch-start' + ? ['no-retention-direct-after-fetch'] + : []), + ...(h.retained && + !h.overlapsSaved && + h.write === 'before-fetch' && + h.includesDirect && + h.directValue !== h.sourceValue + ? ['retained-direct-before-returned-key', 'fetched-row-write'] + : []), + ...(h.overlapsSaved && + h.retained && + h.write === 'before-fetch' && + h.includesDirect && + h.sourceValue === h.savedValue && + h.directValue !== h.savedValue + ? ['returned-value-equals-saved-after-direct-change'] + : []), + ]) + const constructed: Array> = + directOwnershipStates + .filter( + (state) => + state !== 'retained-direct-before-returned-key' && + state !== 'fetched-row-write', + ) + .map((state) => ({ + name: state, + enters: + state === 'retained' + ? [ + state, + 'retained-direct-before-returned-key', + 'fetched-row-write', + ] + : [state], + build(h) { + const built = { ...h } + if (state === 'raw' || state === 'derived') built.shape = state + if (state === 'retained' || state === 'no-retention') + built.retained = state === 'retained' + if (state === 'before-fetch' || state === 'after-fetch-start') + built.write = state + if ( + state === 'source-includes-direct' || + state === 'source-omits-direct' + ) + built.includesDirect = state === 'source-includes-direct' + if (state === 'no-retention-direct-after-fetch') { + built.retained = false + built.write = 'after-fetch-start' + } + if (state === 'retained') { + built.overlapsSaved = false + built.write = 'before-fetch' + built.includesDirect = true + if (built.directValue === built.sourceValue) + built.sourceValue = + built.directValue === 10 ? 9 : built.directValue + 1 + } + if ( + state === 'overlapping-saved-direct-key' || + state === 'distinct-saved-direct-key' + ) + built.overlapsSaved = state === 'overlapping-saved-direct-key' + if (state === 'returned-value-equals-saved-after-direct-change') { + built.overlapsSaved = true + built.retained = true + built.write = 'before-fetch' + built.includesDirect = true + built.sourceValue = built.savedValue + if (built.directValue === built.savedValue) + built.directValue = + built.savedValue === 10 ? 9 : built.savedValue + 1 + } + // Inserting an existing key is illegal; overlap exercises upsert. + if (built.overlapsSaved) built.operation = 'upsert' + // The pinned framework associates seeded constructions only. This ledger + // also enforces every generated output; builders always return new objects. + obligations.set( + built, + state === 'retained' + ? [ + state, + 'retained-direct-before-returned-key', + 'fetched-row-write', + ] + : [state], + ) + return built + }, + })) + let sequence = 0 + return { + id: 'query.persisted-direct-ownership', + law: { + statement: + 'A later complete physical fetch replaces and claims a direct row; an older result preserves it without claiming source ownership. Query adapter sync writes declare an insert, update, or delete operation.', + authority: + 'docs/collections/query-collection.md: Understanding the Data Stores item1 and Direct Writes and Query Sync; primary ownership direct-order/direct-row-lifetime laws; docs/reference/interfaces/SyncConfig.md and ChangeMessage.md, ChangeMessageOrDeleteKeyMessage and OperationType API types.', + owner: + 'packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts', + limits: + 'First eager result, one saved row and one same-run insert/upsert, numeric values, successful I/O. Public/durable cuts follow applied receipts; cleanup is diagnostic. No arbitrary mutations, peers, concurrent writers or eager unload claim.', + }, + histories: { + arbitrary: directOwnershipHistoryArb, + runs: 40, + fixedSeeds: [563], + states: directOwnershipStates, + constructed, + fixed: directOwnershipHistories.map((history, index) => ({ + name: 'direct ownership structural history ' + index, + history, + })), + }, + model: expectedDirectOwnership, + driver: (h, observe) => + receive(h, 'direct-ownership-' + receiver + '-' + sequence++, observe), + check: (h, trace, expected) => { + const product = checkDirectOwnership(h, trace, expected) + const errors = trace + .filter((o) => o.checkpoint === 'browserErrors') + .flatMap((o) => o.value as ReadonlyArray) + if (product.kind === 'violation') + return errors.length + ? { + ...product, + difference: + String(product.difference) + + '; secondary browser diagnostics: ' + + errors.join('; '), + } + : product + const reached = new Set( + trace.filter((o) => o.checkpoint === 'reach').map((o) => o.value), + ) + const missing = (obligations.get(h) ?? []).filter( + (state) => !reached.has(state), + ) + if (missing.length) + return { + kind: 'violation', + checkpoint: 'constructed states', + law: 'constructed reach', + difference: + missing.join(', ') + + (errors.length + ? '; secondary browser diagnostics: ' + errors.join('; ') + : ''), + } + return errors.length + ? { + kind: 'violation', + checkpoint: 'browser diagnostics', + law: 'detached-browser-error', + difference: errors.join('; '), + } + : { kind: 'pass' } + }, + calibrations: [ + { + name: 'omits the declared sync-write operation', + law: 'declared sync-write operation', + corrupt: (trace) => { + const first = trace.findIndex((o) => o.checkpoint === 'sync-write') + return first < 0 + ? undefined + : trace.map((o, index) => + index === first ? { ...o, value: undefined } : o, + ) + }, + }, + { + name: 'rejects an empty normal return', + law: 'receiving completion', + corrupt: () => [], + }, + { + name: 'rejects a truncated normal return', + law: 'receiving completion', + corrupt: (trace) => trace.filter((o) => o.checkpoint !== 'completed'), + }, + ...directOwnershipStates.map((state) => ({ + name: 'omits constructed state ' + state, + law: 'constructed reach', + corrupt: ( + trace: ReadonlyArray, + h: DirectOwnershipHistory, + ) => + obligations.get(h)?.includes(state) + ? trace.filter((o) => o.checkpoint !== 'reach' || o.value !== state) + : undefined, + })), + ...(['first', 'firstDurable'] as const).map((cut) => ({ + name: 'claims direct row from older result at ' + cut, + law: 'direct row ownership', + corrupt: ( + trace: ReadonlyArray, + h: DirectOwnershipHistory, + ) => + h.write !== 'after-fetch-start' || h.overlapsSaved + ? undefined + : trace.map((o) => + o.checkpoint === cut + ? { + ...o, + value: (o.value as Array).map((row) => + row.id === 'x' ? { ...row, owners: ['source'] } : row, + ), + } + : o, + ), + })), + ...(['first', 'firstDurable'] as const).map((cut) => ({ + name: 'keeps direct value after later returned result at ' + cut, + law: 'direct row value', + corrupt: ( + trace: ReadonlyArray, + h: DirectOwnershipHistory, + ) => + h.write !== 'before-fetch' || + !h.includesDirect || + h.sourceValue === h.directValue + ? undefined + : trace.map((o) => + o.checkpoint === cut + ? { + ...o, + value: (o.value as Array).map((row) => + row.id === 'x' ? { ...row, value: h.directValue } : row, + ), + } + : o, + ), + })), + { + name: 'reports detached browser error', + law: 'detached-browser-error', + corrupt: (trace) => [ + ...trace, + { checkpoint: 'browserErrors', value: ['detached browser failure'] }, + ], + }, + { + name: 'preserves product failure before browser diagnostics', + law: 'direct row ownership', + corrupt: (trace, h) => + h.write !== 'after-fetch-start' || h.overlapsSaved + ? undefined + : [ + ...trace.map((o) => + o.checkpoint === 'first' + ? { + ...o, + value: (o.value as Array).map((row) => + row.id === 'x' ? { ...row, owners: ['source'] } : row, + ), + } + : o, + ), + { + checkpoint: 'browserErrors', + value: ['secondary detached failure'], + }, + ], + }, + ], + } +} +declareOracle({ + model: [expectedDirectOwnership], + grammar: [directOwnershipHistoryArb, directOwnershipHistories], + check: [checkDirectOwnership], +}) diff --git a/packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.test.ts b/packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.test.ts new file mode 100644 index 0000000000..c65220e8f9 --- /dev/null +++ b/packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.test.ts @@ -0,0 +1,105 @@ +// @vitest-environment jsdom +// Query's client runtime schedules the stale callback; the SQLite host stays real. +import { describe } from 'vitest' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { oracleProperty, pass, violation } from '../../db/tests/oracle-property' +import { + arbitrary, + constructed, + cutoffViolations, + expected, + obligations, + observeCutoff, + states, +} from './persisted-rejected-write-cutoff-oracle' +import type { History, Trace } from './persisted-rejected-write-cutoff-oracle' + +declareOracle({ owns: ['src/query.ts#queryCollectionOptions'] }) + +for (const host of ['controlled', 'node-sqlite'] as const) + describe(host, () => { + let sequence = 0 + oracleProperty({ + id: 'query.persisted-rejected-write-cutoff', + law: { + statement: + 'A rejected direct write and stale observer callback leave a fetched cache authoritative for an Infinity-stale peer acquisition.', + authority: + 'Query guide Understanding the Data Stores item 1, Direct Writes and Query Sync; Query Core staleTime and refetchOnMount; primary ownership direct-write-order judgment.', + owner: 'Query ownership lifecycle receiving extension', + limits: + 'Empty eager persisted startup, one successful fetched row, rejected missing-key update/delete, raw or direct wrapped array; no accepted mutation, invalidation, failed I/O or concurrent writer.', + }, + histories: { + arbitrary, + runs: 20, + fixedSeeds: [1794], + states, + constructed, + }, + model: expected, + driver: async (h, observe) => { + const storage = createStartupHost(host) + await withOracleCleanup( + () => + observeCutoff( + storage.persistence, + h, + 'cutoff-' + host + '-' + sequence++, + observe, + ), + [() => storage.close()], + ) + }, + check: (h, trace) => { + const bad = cutoffViolations(h, trace)[0] + return bad + ? violation(bad.checkpoint, bad.difference, 'fetched cache authority') + : pass + }, + calibrations: [ + { + name: 'keeps an earlier count mismatch on a truncated trace', + law: 'fetched cache authority', + corrupt: (trace: ReadonlyArray) => { + const at = trace.findIndex((o) => o.checkpoint === 'firstFetches') + return at < 0 + ? undefined + : trace + .slice(0, at + 1) + .map((o) => + o.checkpoint === 'firstFetches' ? { ...o, value: 2 } : o, + ) + }, + }, + ...states.map((state) => ({ + name: 'omits constructed state ' + state, + law: 'fetched cache authority', + corrupt: (trace: ReadonlyArray, h: History) => + obligations.get(h)?.includes(state) + ? trace.filter( + (o) => o.checkpoint !== 'reach' || o.value !== state, + ) + : undefined, + })), + { + name: 'adds a request for a fetched cache', + law: 'fetched cache authority', + corrupt: (trace: ReadonlyArray) => + trace.map((o) => + o.checkpoint === 'peerFetches' ? { ...o, value: 2 } : o, + ), + }, + { + name: 'changes peer value without a fetch', + law: 'fetched cache authority', + corrupt: (trace: ReadonlyArray) => + trace.map((o) => + o.checkpoint === 'peer' ? { ...o, value: [] } : o, + ), + }, + ], + }) + }) diff --git a/packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.ts b/packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.ts new file mode 100644 index 0000000000..19c1859dce --- /dev/null +++ b/packages/query-db-collection/tests/persisted-rejected-write-cutoff-oracle.ts @@ -0,0 +1,518 @@ +/** + * A rejected direct write changes neither row authority nor Query freshness. + * Query's staleTime is observer-local: an observer that becomes stale cannot + * convert a successfully fetched response into a local cache edit. A new peer + * with staleTime Infinity acquires that response without another request. + * Authority: Query guide Understanding the Data Stores item 1, Direct Writes + * and Query Sync; Query Core staleTime/refetchOnMount options; existing primary + * ownership direct-write-order model's cacheStillFetched rule. Failed update + * or delete has no accepted write in that rule. + * + * The clock abstraction changes Date.now and only Query Core's timer provider. + * It records and delivers the actual scheduled stale callback. SQLite, browser + * workers and host timers stay real. No Query state or Collection row is faked. + * One eager startup, an empty saved baseline, one successful response and one + * later on-demand peer. No failed I/O, accepted write, cache edit or invalidation. + */ +import fc from 'fast-check' +import { QueryClient, hashKey, timeoutManager } from '@tanstack/query-core' +import { createCollection } from '@tanstack/db' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { queryCollectionOptions } from '../src/query' +import { + startupTurn, + untilStartup, +} from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { declareOracle } from '../../db/tests/oracle-declaration' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' +import type { ConstructedHistory } from '../../db/tests/oracle-property' +import type { TimeoutProvider } from '@tanstack/query-core' + +export type Row = { id: string; value: number } +export type History = { + shape: 'raw' | 'wrapped' + write: + | 'none' + | 'before-update' + | 'before-delete' + | 'during-update' + | 'during-delete' + firstValue: number + nextValue: number + staleMs: number +} +export type SeenRow = Row & { owners: Array } +export type Trace = Array<{ checkpoint: string; value: unknown }> +export const arbitrary = fc.record({ + shape: fc.constantFrom('raw' as const, 'wrapped' as const), + write: fc.constantFrom( + 'none' as const, + 'before-update' as const, + 'before-delete' as const, + 'during-update' as const, + 'during-delete' as const, + ), + firstValue: fc.integer({ min: -10, max: 10 }), + nextValue: fc.integer({ min: -10, max: 10 }), + staleMs: fc.integer({ min: 10, max: 1000 }), +}) +export const states = [ + 'raw', + 'wrapped', + 'no-write', + 'before-rejected-write', + 'during-rejected-write', + 'update-rejection', + 'delete-rejection', + 'stale-callback-delivered', + 'peer-acquired-fetched-cache', +] as const +export const obligations = new WeakMap>() +export const constructed: Array> = states.map( + (state) => ({ + name: state, + enters: [state], + build(h) { + const built = { ...h } + if (state === 'raw' || state === 'wrapped') built.shape = state + if (state === 'no-write') built.write = 'none' + if (state === 'before-rejected-write') built.write = 'before-update' + if (state === 'during-rejected-write') built.write = 'during-update' + if (state === 'update-rejection') built.write = 'during-update' + if (state === 'delete-rejection') built.write = 'during-delete' + obligations.set(built, [state]) + return built + }, + }), +) +// A failed operation adds no authored event to the response authority model. +// Each legal history therefore leaves the first fetched response authoritative. +export function expected(h: History) { + return { + rows: [{ id: 'x', value: h.firstValue, owners: ['source'] }], + calls: 1, + rejection: + h.write === 'none' + ? undefined + : h.write.endsWith('update') + ? 'UpdateOperationItemNotFoundError' + : 'DeleteOperationItemNotFoundError', + } +} +function gate() { + let release!: () => void + const promise = new Promise((r) => (release = r)) + return { promise, release } +} +function rowOwners(metadata: unknown, queryHash: string): Array { + if (!metadata || typeof metadata !== 'object') return [] + const part = (metadata as Record).queryCollection + if (!part || typeof part !== 'object') return [] + const owners = (part as Record).owners + return owners && typeof owners === 'object' + ? Object.keys(owners) + .map((owner) => (owner === queryHash ? 'source' : owner)) + .sort() + : [] +} +// The fixture advances only timers that Query Core scheduled. It does not call +// an observer update itself. Cleanup removes every scheduled Query timer first. +function queryClock() { + let now = Date.now(), + next = 1, + delivered = 0 + const nativeNow = Date.now + const tasks = new Map void }>() + const provider: TimeoutProvider = { + setTimeout(callback, delay) { + const id = next++ + tasks.set(id, { at: now + delay, callback }) + return id + }, + clearTimeout(id) { + if (id !== undefined) tasks.delete(id) + }, + setInterval() { + throw new Error('This history schedules no Query interval') + }, + clearInterval() {}, + } + Date.now = () => now + timeoutManager.setTimeoutProvider(provider) + return { + get delivered() { + return delivered + }, + get scheduled() { + return tasks.size + }, + advance(ms: number) { + const target = now + ms + for (;;) { + const due = [...tasks] + .filter(([, t]) => t.at <= target) + .sort((a, b) => a[1].at - b[1].at)[0] + if (!due) break + tasks.delete(due[0]) + now = due[1].at + delivered++ + due[1].callback() + } + now = target + }, + restore() { + const pending = tasks.size + Date.now = nativeNow + timeoutManager.setTimeoutProvider({ + setTimeout: (cb, delay) => globalThis.setTimeout(cb, delay), + clearTimeout: (id) => globalThis.clearTimeout(id), + setInterval: (cb, delay) => globalThis.setInterval(cb, delay), + clearInterval: (id) => globalThis.clearInterval(id), + }) + if (pending) throw new Error('Leaked Query timers: ' + pending) + }, + } +} +export async function observeCutoff( + persistence: PersistedCollectionPersistence, + h: History, + id: string, + observe?: (checkpoint: string, value: unknown) => void, +): Promise { + const trace: Trace = [] + const record = (checkpoint: string, value: unknown) => { + trace.push({ checkpoint, value }) + observe?.(checkpoint, value) + } + const clock = queryClock() + const client = new QueryClient({ + defaultOptions: { + queries: { retry: false, gcTime: Infinity, staleTime: Infinity }, + }, + }) + const hydration = gate() + const key = [id], + queryHash = hashKey(key) + const receipts: Array> = [] + let enabled = false, + entered = false, + calls = 0, + commits = 0, + rejection: string | undefined + const cacheActions: Array = [] + const unsubscribe = client.getQueryCache().subscribe((event) => { + if (event.type === 'updated') cacheActions.push(event.action.type) + }) + type Response = Array | { rows: Array } + const encode = (value: number): Response => + h.shape === 'raw' ? [{ id: 'x', value }] : { rows: [{ id: 'x', value }] } + const decode = (response: Response) => + Array.isArray(response) ? response : response.rows + const fn = (): Promise => + Promise.resolve(encode(++calls === 1 ? h.firstValue : h.nextValue)) + const options = queryCollectionOptions({ + id, + queryKey: key, + queryClient: client, + queryFn: fn, + select: decode, + getKey: (r: Row) => r.id, + staleTime: h.staleMs, + enabled: () => enabled, + }) + const sync = options.sync.sync + const collection = createCollection( + persistedCollectionOptions({ + ...options, + persistence, + sync: { + ...options.sync, + sync(params) { + const capability = params.metadata!.persistence! + return sync({ + ...params, + commit(signal) { + commits++ + const receipt = params.commit(signal) + receipts.push(Promise.resolve(receipt).then(() => undefined)) + return receipt + }, + metadata: { + ...params.metadata!, + persistence: { + ...capability, + async hydrateBaseline() { + await capability.hydrateBaseline() + entered = true + await hydration.promise + }, + }, + }, + }) + }, + }, + }), + ) + const snapshot = (c: { + values: () => Iterable + _state: { syncedMetadata: Map } + }) => + Array.from(c.values(), (row) => ({ + id: row.id, + value: row.value, + owners: rowOwners(c._state.syncedMetadata.get(row.id), queryHash), + })).sort((a, b) => a.id.localeCompare(b.id)) + const durable = async () => { + if (!persistence.adapter.scanRows) + throw new Error('Cutoff receiver requires durable scans') + return ( + await persistence.adapter.scanRows({ kind: 'eager', collectionId: id }) + ) + .map((row) => ({ + ...(row.value as Row), + owners: rowOwners(row.metadata, queryHash), + })) + .sort((a, b) => a.id.localeCompare(b.id)) + } + const makePeer = () => + createCollection( + queryCollectionOptions({ + id: id + '-peer', + queryKey: key, + queryClient: client, + queryFn: fn, + select: decode, + getKey: (r: Row) => r.id, + syncMode: 'on-demand', + staleTime: Infinity, + startSync: true, + }), + ) + let peer: ReturnType | undefined + const rejectWrite = async () => { + try { + if (h.write.endsWith('update')) + await collection.utils.writeUpdate({ id: 'missing', value: 7 }) + else await collection.utils.writeDelete('missing') + } catch (error) { + rejection = (error as Error).constructor.name + } + } + return withOracleCleanup(async () => { + const preload = collection.preload() + void preload.catch(() => {}) + await untilStartup( + () => + client.getQueryCache().find({ queryKey: key, exact: true }) !== + undefined, + ) + if (h.write.startsWith('before')) await rejectWrite() + enabled = true + const fetched = client.fetchQuery({ queryKey: key, queryFn: fn }) + void fetched.catch(() => {}) + await untilStartup(() => entered) + if (h.write.startsWith('during')) await rejectWrite() + hydration.release() + await fetched + await preload + record('rejection', rejection) + record('first', snapshot(collection)) + record('firstDurable', await durable()) + const state = client.getQueryState(key)! + record('firstFetches', calls) + const before = { + commits, + actions: cacheActions.length, + updatedAt: state.dataUpdatedAt, + dataCount: state.dataUpdateCount, + } + const scheduled = clock.scheduled + clock.advance(h.staleMs + 1) + // Query's listener runs in the delivered callback. Its already-resolved + // write-settlement continuation drains before the next host task. Await + // every public applied receipt it returned; repeat if application returned + // another receipt meanwhile. A valid no-op optimization owes no commit. + await startupTurn() + for (;;) { + const captured = receipts.length + await Promise.all(receipts) + await startupTurn() + if (receipts.length === captured) break + } + const afterState = client.getQueryState(key)! + record('stalePremise', { + scheduled, + delivered: clock.delivered, + adapterCommits: commits - before.commits, + fetches: calls, + updatedAtUnchanged: afterState.dataUpdatedAt === before.updatedAt, + dataCountUnchanged: afterState.dataUpdateCount === before.dataCount, + actions: cacheActions.slice(before.actions), + invalidated: afterState.isInvalidated, + }) + record('afterStale', snapshot(collection)) + record('afterStaleDurable', await durable()) + peer = makePeer() + await peer._sync.loadSubset({}) + record('peer', snapshot(peer)) + record('peerFetches', calls) + record('afterPeer', snapshot(collection)) + record('afterPeerDurable', await durable()) + for (const stateName of [ + h.shape, + ...(h.write === 'none' + ? ['no-write'] + : rejection + ? [ + h.write.startsWith('before') + ? 'before-rejected-write' + : 'during-rejected-write', + h.write.endsWith('update') + ? 'update-rejection' + : 'delete-rejection', + ] + : []), + ...(clock.delivered === 1 ? ['stale-callback-delivered'] : []), + ...(calls === 1 ? ['peer-acquired-fetched-cache'] : []), + ]) + record('reach', stateName) + record('completed', true) + return trace + }, [ + hydration.release, + () => peer?.cleanup(), + () => collection.cleanup(), + () => { + unsubscribe() + client.clear() + }, + () => clock.restore(), + ]) +} +export function cutoffViolations( + h: History, + trace: ReadonlyArray<{ checkpoint: string; value: unknown }>, +): Array<{ checkpoint: string; difference: string; law?: string }> { + const predicted = expected(h) + const failures: Array<{ + checkpoint: string + difference: string + law?: string + }> = [] + const browserFailures: Array<{ + checkpoint: string + difference: string + law?: string + }> = [] + const value = (cut: string) => trace.find((o) => o.checkpoint === cut)?.value + const expectedByCut: Record = { + rejection: predicted.rejection, + first: predicted.rows, + firstDurable: predicted.rows, + afterStale: predicted.rows, + afterStaleDurable: predicted.rows, + peer: predicted.rows, + afterPeer: predicted.rows, + afterPeerDurable: predicted.rows, + firstFetches: predicted.calls, + peerFetches: predicted.calls, + completed: true, + } + // Judge recorded facts in arrival order. A later missing cut or cleanup + // failure cannot replace an already recorded public mismatch. + for (const observation of trace) { + if ( + Object.prototype.hasOwnProperty.call( + expectedByCut, + observation.checkpoint, + ) && + JSON.stringify(observation.value) !== + JSON.stringify(expectedByCut[observation.checkpoint]) + ) + failures.push({ + checkpoint: observation.checkpoint, + difference: + 'expected ' + + JSON.stringify(expectedByCut[observation.checkpoint]) + + ', observed ' + + JSON.stringify(observation.value), + }) + if (observation.checkpoint === 'stalePremise') { + const premise = observation.value as + | { + scheduled: number + delivered: number + fetches: number + updatedAtUnchanged: boolean + dataCountUnchanged: boolean + invalidated: boolean + } + | undefined + if ( + !premise || + premise.scheduled < 1 || + premise.delivered !== 1 || + premise.fetches !== 1 || + !premise.updatedAtUnchanged || + !premise.dataCountUnchanged || + premise.invalidated + ) + failures.push({ + checkpoint: 'stalePremise', + difference: + 'Actual scheduled stale callback and unchanged fetch freshness were not established', + }) + } + if ( + observation.checkpoint === 'browserErrors' && + (!Array.isArray(observation.value) || observation.value.length) + ) + browserFailures.push({ + checkpoint: 'browserErrors', + difference: + 'Receiving page reported ' + JSON.stringify(observation.value), + law: 'browser receiving fidelity', + }) + } + // Completeness is a separate terminal judgment after all recorded facts. + const required = [ + ...Object.keys(expectedByCut), + 'stalePremise', + ...(value('browserReceiver') === true ? ['browserErrors'] : []), + ] + for (const checkpoint of required) + if (!trace.some((o) => o.checkpoint === checkpoint)) + failures.push({ + checkpoint, + difference: 'Missing required receiving observation', + }) + const reached = new Set( + trace.filter((o) => o.checkpoint === 'reach').map((o) => o.value), + ) + const missing = (obligations.get(h) ?? []).filter( + (state) => !reached.has(state), + ) + if (missing.length) + failures.push({ + checkpoint: 'constructed states', + difference: missing.join(', '), + }) + failures.push(...browserFailures) + const primary = failures[0], + diagnostics = value('browserErrors') + if ( + primary && + primary.checkpoint !== 'browserErrors' && + Array.isArray(diagnostics) && + diagnostics.length + ) + primary.difference += + '; secondary browser errors: ' + JSON.stringify(diagnostics) + return failures +} +declareOracle({ + model: [expected], + grammar: [arbitrary], + check: [cutoffViolations], +}) diff --git a/packages/query-db-collection/tests/query-receiving-fidelity.test.ts b/packages/query-db-collection/tests/query-receiving-fidelity.test.ts new file mode 100644 index 0000000000..ebd8f4c321 --- /dev/null +++ b/packages/query-db-collection/tests/query-receiving-fidelity.test.ts @@ -0,0 +1,130 @@ +/** Calibration of the shared receiving judgments, with no production model. */ +import { expect, it } from 'vitest' +import { + deliverQueryHistory, + recordQueryHistory, +} from '../../browser-db-sqlite-persistence/e2e/query-ownership-browser-support' +import { + directOwnershipHistories, + expectedDirectOwnership, + persistedDirectOwnershipOptions, +} from './persisted-direct-ownership-oracle' +import { + expectedImmediateMaterialization, + immediateEagerMaterializationOptions, + immediateMaterializationHistories, +} from './immediate-eager-materialization-oracle' +import type { Observation, Verdict } from '../../db/tests/oracle-property' + +function rejection(verdict: Verdict, law: string, secondary = false) { + expect(verdict.kind).toBe('violation') + if (verdict.kind !== 'violation') throw new Error('Missing violation') + expect(verdict.law).toBe(law) + if (secondary) + expect(String(verdict.difference)).toContain('secondary-page-error') +} +it('keeps direct receiving completion and secondary diagnostics observable', () => { + const options = persistedDirectOwnershipOptions(async () => {}, 'calibration') + // Fresh inputs isolate product/completion diagnostics here. The actual fixed + // production replays and reach calibrations enforce their entered states. + for (const fixed of directOwnershipHistories) { + const h = { ...fixed } + const expected = expectedDirectOwnership(h) + const valid: Array = [ + { checkpoint: 'first', value: expected }, + { checkpoint: 'firstDurable', value: expected }, + { checkpoint: 'completed', value: true }, + ] + expect(options.check(h, valid, expected).kind).toBe('pass') + rejection(options.check(h, [], expected), 'receiving completion') + rejection( + options.check(h, valid.slice(0, 2), expected), + 'receiving completion', + ) + const wrong = [ + { checkpoint: 'first', value: [{ id: 'wrong', value: 0, owners: [] }] }, + ] + rejection(options.check(h, wrong, expected), 'direct row ownership') + rejection( + options.check( + h, + [ + ...wrong, + { checkpoint: 'browserErrors', value: ['secondary-page-error'] }, + ], + expected, + ), + 'direct row ownership', + true, + ) + } +}) +it('keeps immediate receiving completion and secondary diagnostics observable', () => { + const options = immediateEagerMaterializationOptions( + async () => {}, + 'calibration', + ) + for (const fixed of immediateMaterializationHistories) { + const h = { ...fixed, rows: fixed.rows.map((row) => ({ ...row })) } + const expected = expectedImmediateMaterialization(h) + const valid: Array = [ + { checkpoint: 'creation-return', value: expected }, + { checkpoint: 'completed', value: true }, + ] + expect(options.check(h, valid, expected).kind).toBe('pass') + rejection(options.check(h, [], expected), 'receiving completion') + rejection( + options.check(h, valid.slice(0, 1), expected), + 'receiving completion', + ) + const wrong = [ + { + checkpoint: 'creation-return', + value: { ...expected, status: 'loading' }, + }, + ] + rejection( + options.check(h, wrong, expected), + 'immediate eager materialization', + ) + rejection( + options.check( + h, + [ + ...wrong, + { checkpoint: 'browserErrors', value: ['secondary-page-error'] }, + ], + expected, + ), + 'immediate eager materialization', + true, + ) + } +}) + +it('retains cleanup failure and detached browser diagnostics together', async () => { + const result = await recordQueryHistory((observe) => { + observe('prefix', 1) + return Promise.reject(new Error('cleanup failure')) + }) + const trace: Array = [] + let thrown: unknown + try { + deliverQueryHistory( + { ...result, browserErrors: ['secondary-page-error'] }, + (checkpoint, value) => trace.push({ checkpoint, value }), + ) + } catch (error) { + thrown = error + } + expect(thrown).toBeInstanceOf(Error) + expect((thrown as Error).message).toBe('cleanup failure') + expect((thrown as Error).cause).toMatchObject({ + message: 'cleanup failure', + browserErrors: ['secondary-page-error'], + }) + expect(trace).toEqual([ + { checkpoint: 'prefix', value: 1 }, + { checkpoint: 'browserErrors', value: ['secondary-page-error'] }, + ]) +}) diff --git a/packages/query-db-collection/tests/scope-omission-reacquisition-oracle.test.ts b/packages/query-db-collection/tests/scope-omission-reacquisition-oracle.test.ts new file mode 100644 index 0000000000..be68a4d484 --- /dev/null +++ b/packages/query-db-collection/tests/scope-omission-reacquisition-oracle.test.ts @@ -0,0 +1,37 @@ +// @vitest-environment jsdom +import { describe, it } from 'vitest' +import { oracleReplayRequested } from '../../db/tests/oracle-config' +import { oracleProperty } from '../../db/tests/oracle-property' +import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { + calibrateGeneratedConstructions, + observeScope, + options, +} from './scope-omission-reacquisition-oracle' + +declareOracle({ owns: ['src/query.ts#queryCollectionOptions'] }) +for (const host of ['controlled', 'node-sqlite'] as const) + describe(host, () => { + let sequence = 0 + const receive: Parameters[0] = async (h, id, observe) => { + const storage = createStartupHost(host) + await withOracleCleanup( + () => observeScope(storage.persistence, h, id, observe), + [() => storage.close()], + ) + } + const property = options(receive, host) + oracleProperty({ + ...property, + check: (h, trace, expected) => property.check(h, trace, expected), + }) + it.skipIf(oracleReplayRequested())( + 'rejects missing states from generated constructions', + () => + calibrateGeneratedConstructions((h, observe) => + receive(h, 'generated-control-' + host + '-' + sequence++, observe), + ), + ) + }) diff --git a/packages/query-db-collection/tests/scope-omission-reacquisition-oracle.ts b/packages/query-db-collection/tests/scope-omission-reacquisition-oracle.ts new file mode 100644 index 0000000000..316868ad37 --- /dev/null +++ b/packages/query-db-collection/tests/scope-omission-reacquisition-oracle.ts @@ -0,0 +1,503 @@ +/** + * Scope-local omission ends a direct row's lifetime. A distinct retained Query + * remains usable under Infinity stale/gc rules. Acquiring that retained Query + * projects its own result after A removed the direct row. + * Authority: Query guide Direct Writes and Query Sync, paragraphs on complete + * scope omission and released-row reacquisition. + * Primary owner: ownership-lifecycle oracle. This receiving extension reuses its + * direct/fetch overlay judgment and adds retained-scope next-use observations. + * + * Two overlapping exact predicates, one ownerless insert/upsert, a later + * complete A response, and optional sibling y. No transport failures, stale + * refetch, window/cursor, cache edits, concurrent writer or arbitrary schedules. + */ +import fc from 'fast-check' +import { QueryClient } from '@tanstack/query-core' +import { IR, createCollection, eq } from '@tanstack/db' +import { untilStartup } from '../../db-adapter-conformance/tests/persisted-startup-driver' +import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' +import { queryCollectionOptions } from '../src/query' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { mergeDirectWritesOverFetch } from './direct-write-precedence-oracle' +import type { SyncPersistenceCapabilityV1 } from '@tanstack/db' +import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' +import type { + ConstructedHistory, + Observation, + OraclePropertyOptions, +} from '../../db/tests/oracle-property' + +type Row = { id: string; value: number; category: string } +type Response = Array | { edges: Array<{ node: Row }> } +export type History = { + shape: 'raw' | 'derived' + operation: 'insert' | 'upsert' + cacheOrder: 'before-write' | 'after-write' + includesDirect: boolean + sibling: boolean + bValue: number + directValue: number + sourceValue: number + siblingValue: number +} +export const arbitrary = fc.record({ + shape: fc.constantFrom('raw' as const, 'derived' as const), + operation: fc.constantFrom('insert' as const, 'upsert' as const), + cacheOrder: fc.constantFrom('before-write' as const, 'after-write' as const), + includesDirect: fc.boolean(), + sibling: fc.boolean(), + bValue: fc.integer({ min: -10, max: 10 }), + directValue: fc.integer({ min: -10, max: 10 }), + sourceValue: fc.integer({ min: -10, max: 10 }), + siblingValue: fc.integer({ min: -10, max: 10 }), +}) +export const states = [ + 'raw', + 'derived', + 'insert', + 'upsert', + 'old-cache-after-omission', + 'new-cache-after-omission', + 'returned-direct-key', + 'nonempty-omission', +] as const +export const obligations = new WeakMap>() +export const constructed: Array> = states.map( + (state) => ({ + name: state, + enters: [state], + build(h) { + const n = { ...h } + if (state === 'raw' || state === 'derived') n.shape = state + if (state === 'insert' || state === 'upsert') n.operation = state + if ( + state === 'old-cache-after-omission' || + state === 'new-cache-after-omission' + ) { + n.includesDirect = false + n.cacheOrder = + state === 'old-cache-after-omission' ? 'before-write' : 'after-write' + } + if (state === 'returned-direct-key') n.includesDirect = true + if (state === 'nonempty-omission') { + n.includesDirect = false + n.sibling = true + } + obligations.set(n, [state]) + return n + }, + }), +) +const sourceRows = (h: History): Array => [ + ...(h.includesDirect + ? [{ id: 'x', value: h.sourceValue, category: 'shared' }] + : []), + ...(h.sibling + ? [{ id: 'y', value: h.siblingValue, category: 'shared' }] + : []), +] +/** A later complete A response retires the direct row when absent. B then + * starts a new lifetime from its retained result. If A returns x, A already + * owns its newer value when B acquires, so B cannot revert that value. */ +export function expected(h: History) { + const direct = { id: 'x', value: h.directValue } + const afterA = mergeDirectWritesOverFetch(sourceRows(h), []) + return { + direct: mergeDirectWritesOverFetch([], [direct]), + afterA, + acquired: { ...afterA, ...(!h.includesDirect ? { x: h.bValue } : {}) }, + fetches: 2, + } +} +export async function observeScope( + persistence: PersistedCollectionPersistence | undefined, + h: History, + id: string, + observe: (cut: string, value: unknown) => void, +) { + const client = new QueryClient({ + defaultOptions: { + queries: { retry: false, staleTime: Infinity, gcTime: Infinity }, + }, + }) + const encode = (rows: Array): Response => + h.shape === 'raw' ? rows : { edges: rows.map((node) => ({ node })) } + const decode = (response: Response) => + Array.isArray(response) + ? response + : response.edges.map(({ node }) => ({ ...node })) + const bRow = { id: 'x', value: h.bValue, category: 'shared' } + const direct = { ...bRow, value: h.directValue } + const b = { where: eq(new IR.PropRef(['id']), 'x') } + const a = { where: eq(new IR.PropRef(['category']), 'shared') } + let calls = 0 + let deliveredA: Array | undefined + const events: Array = [] + let scan: + | SyncPersistenceCapabilityV1['scanPersistedRows'] + | undefined + const responses = [encode([bRow]), encode(sourceRows(h))] + const makeOptions = (suffix: string) => + queryCollectionOptions({ + id: id + suffix, + queryKey: [id], + queryClient: client, + queryFn: () => { + events.push(calls === 0 ? 'fetch-b' : 'fetch-a') + const response = responses[calls++] + if (!response) throw new Error('Unexpected source request ' + calls) + if (calls === 2) deliveredA = decode(response) + return Promise.resolve(response) + }, + select: decode, + getKey: (row: Row) => row.id, + syncMode: 'on-demand', + staleTime: Infinity, + startSync: true, + }) + const base = makeOptions('') + const sourceSync = base.sync.sync + const wrapped = { + ...base, + sync: { + ...base.sync, + sync: (params: Parameters[0]) => { + scan = params.metadata?.persistence?.scanPersistedRows + return sourceSync(params) + }, + }, + } + const main = createCollection( + persistence + ? persistedCollectionOptions({ ...wrapped, persistence }) + : wrapped, + ) + const peerB = createCollection(makeOptions('-peer-b')) + const values = (collection: { values: () => Iterable }) => + Object.fromEntries( + Array.from(collection.values(), (row) => [row.id, row.value]).sort( + ([left], [right]) => String(left).localeCompare(String(right)), + ), + ) + const record = async (cut: string) => { + const snapshot = values(main) + observe(cut, snapshot) + if (persistence) { + if (!scan) throw new Error('Missing durable receiving scan') + const rows = await scan() + observe( + cut + 'Durable', + Object.fromEntries( + rows + .map((row) => { + const value = row.value as Row + return [value.id, value.value] + }) + .sort(([left], [right]) => + String(left).localeCompare(String(right)), + ), + ), + ) + } + return snapshot + } + const acquirePeers = async () => { + await peerB._sync.loadSubset(b) + } + await withOracleCleanup(async () => { + await untilStartup(() => main.status === 'ready') + if (h.cacheOrder === 'before-write') await acquirePeers() + if (h.operation === 'insert') await main.utils.writeInsert(direct) + else await main.utils.writeUpsert(direct) + events.push('write-accepted') + const directRows = await record('direct') + if (h.cacheOrder === 'after-write') await acquirePeers() + // The physical B entry is retained by a distinct real Collection, not seeded + // with setQueryData. Derived select has no inverse; raw shared entries also + // stay unchanged under the on-demand direct-write rule. + const retainedRows = values(peerB) + observe('retainedB', retainedRows) + const cachedResponse = client.getQueryCache().findAll({ queryKey: [id] })[0] + ?.state.data + const shape = Array.isArray(cachedResponse) + ? 'raw' + : cachedResponse !== null && + typeof cachedResponse === 'object' && + Array.isArray((cachedResponse as { edges?: unknown }).edges) + ? 'derived' + : undefined + await main._sync.loadSubset(a) + const afterA = await record('afterA') + await main._sync.loadSubset(b) + await record('acquired') + observe('fetches', calls) + const sourceReturnedX = deliveredA?.some((row) => row.id === 'x') + const cacheBeforeWrite = + events.indexOf('fetch-b') < events.indexOf('write-accepted') + for (const state of [ + ...(shape ? [shape] : []), + ...(directRows.x === h.directValue ? [h.operation] : []), + ...(sourceReturnedX === false && !('x' in afterA) && 'x' in retainedRows + ? [ + cacheBeforeWrite + ? 'old-cache-after-omission' + : 'new-cache-after-omission', + ] + : []), + ...(sourceReturnedX === true && 'x' in afterA + ? ['returned-direct-key'] + : []), + ...(sourceReturnedX === false && !('x' in afterA) && 'y' in afterA + ? ['nonempty-omission'] + : []), + ]) + observe('reach', state) + observe('completed', true) + }, [() => main.cleanup(), () => peerB.cleanup(), () => client.clear()]) +} +export function check(h: History, trace: ReadonlyArray) { + const wanted = expected(h) + for (const o of trace) { + if (o.checkpoint === 'completed' && o.value !== true) + return { + kind: 'violation' as const, + checkpoint: 'completed', + law: 'receiving completion', + difference: 'completion was not established', + } + const cut = o.checkpoint.replace(/Durable$/, '') + const answer = + cut in wanted + ? wanted[cut as keyof typeof wanted] + : o.checkpoint === 'retainedB' + ? { x: h.bValue } + : undefined + if ( + answer !== undefined && + JSON.stringify( + typeof answer === 'number' + ? o.value + : Object.entries(o.value as Record).sort(), + ) !== + JSON.stringify( + typeof answer === 'number' + ? answer + : Object.entries(answer as Record).sort(), + ) + ) + return { + kind: 'violation' as const, + checkpoint: o.checkpoint, + law: 'scope-local row lifetime', + difference: + 'expected ' + + JSON.stringify(answer) + + ', observed ' + + JSON.stringify(o.value), + } + } + for (const cut of [ + 'direct', + 'directDurable', + 'retainedB', + 'afterA', + 'afterADurable', + 'acquired', + 'acquiredDurable', + 'fetches', + 'completed', + ]) + if (!trace.some((o) => o.checkpoint === cut)) + return { + kind: 'violation' as const, + checkpoint: cut, + law: 'receiving completion', + difference: 'missing observation', + } + const reached = new Set( + trace.filter((o) => o.checkpoint === 'reach').map((o) => o.value), + ) + const missing = (obligations.get(h) ?? []).filter((s) => !reached.has(s)) + if (missing.length) + return { + kind: 'violation' as const, + checkpoint: 'constructed states', + law: 'constructed reach', + difference: missing.join(','), + } + const errors = trace + .filter((o) => o.checkpoint === 'browserErrors') + .flatMap((o) => o.value as Array) + if (errors.length) + return { + kind: 'violation' as const, + checkpoint: 'browserErrors', + law: 'browser diagnostics', + difference: errors.join(';'), + } + return { kind: 'pass' as const } +} +export function options( + receive: ( + h: History, + id: string, + observe: (cut: string, value: unknown) => void, + ) => Promise, + host: string, +): OraclePropertyOptions> { + let sequence = 0 + return { + id: + host === 'chromium-opfs' + ? 'query.scope-omission-reacquisition.chromium-opfs' + : 'query.scope-omission-reacquisition', + law: { + statement: + 'A complete scoped omission retires a direct row without invalidating distinct retained Query entries; reacquisition starts a new row lifetime from its retained response.', + authority: + 'Query guide Direct Writes and Query Sync: scope-local omission, retained-cache reacquisition.', + owner: 'Query ownership lifecycle receiving extension', + limits: + 'Two matching scopes, one same-run insert/upsert, complete A response and retained B success, numeric values and successful I/O; no stale requests, window/cursor or concurrent writer.', + }, + histories: { arbitrary, runs: 30, fixedSeeds: [1313], states, constructed }, + model: expected, + driver: (h, observe) => + receive(h, 'scope-omission-' + host + '-' + sequence++, observe), + check: (h, trace) => { + const product = check(h, trace) + const result = + product.kind === 'pass' && + host === 'chromium-opfs' && + !trace.some( + (o) => o.checkpoint === 'browserReceiver' && o.value === true, + ) + ? { + kind: 'violation' as const, + checkpoint: 'browserReceiver', + law: 'receiving completion', + difference: 'browser receiver marker is missing', + } + : product + const errors = trace + .filter((o) => o.checkpoint === 'browserErrors') + .flatMap((o) => o.value as Array) + return result.kind === 'violation' && errors.length + ? { + ...result, + difference: + String(result.difference) + + '; secondary browser diagnostics: ' + + errors.join('; '), + } + : result + }, + calibrations: [ + { + name: 'rejects omitted durable observation', + law: 'receiving completion', + corrupt: (trace: ReadonlyArray) => + trace.filter((o) => o.checkpoint !== 'afterADurable'), + }, + { + name: 'keeps product mismatch on truncated capture', + law: 'scope-local row lifetime', + corrupt: (trace: ReadonlyArray) => { + const at = trace.findIndex((o) => o.checkpoint === 'afterA') + return at < 0 + ? undefined + : trace + .slice(0, at + 1) + .map((o) => + o.checkpoint === 'afterA' + ? { ...o, value: { wrong: 999 } } + : o, + ) + }, + }, + { + name: 'keeps product mismatch before false completion', + law: 'scope-local row lifetime', + corrupt: (trace: ReadonlyArray) => [ + ...trace.map((o) => + o.checkpoint === 'afterA' ? { ...o, value: { wrong: 999 } } : o, + ), + { checkpoint: 'completed', value: false }, + ], + }, + { + name: 'keeps product mismatch before missing browser marker', + law: 'scope-local row lifetime', + corrupt: (trace: ReadonlyArray) => + trace + .filter((o) => o.checkpoint !== 'browserReceiver') + .map((o) => + o.checkpoint === 'afterA' ? { ...o, value: { wrong: 999 } } : o, + ), + }, + { + name: 'reports detached browser error', + law: 'browser diagnostics', + corrupt: (trace: ReadonlyArray) => [ + ...trace, + { checkpoint: 'browserErrors', value: ['scope detached fault'] }, + ], + }, + + ...states.map((state) => ({ + name: 'omits state ' + state, + law: 'constructed reach', + corrupt: (trace: ReadonlyArray, h: History) => + obligations.get(h)?.includes(state) + ? trace.filter((o) => o.checkpoint !== 'reach' || o.value !== state) + : undefined, + })), + { + name: 'loses retained row after omission', + law: 'scope-local row lifetime', + corrupt: (trace: ReadonlyArray, h: History) => + h.includesDirect + ? undefined + : trace.map((o) => + o.checkpoint === 'acquired' ? { ...o, value: {} } : o, + ), + }, + ], + } +} +/** The framework's generated construction objects differ from its seeded + * construction objects. Exercise the same builder, receiving path and check + * with generated inputs, then remove only its required actual reach fact. */ +export async function calibrateGeneratedConstructions( + receive: ( + h: History, + observe: (cut: string, value: unknown) => void, + ) => Promise, +) { + for (const input of fc.sample(arbitrary, { numRuns: 1, seed: 1313 })) { + for (const builder of constructed) { + const h = builder.build(input) + const trace: Array = [] + await receive(h, (checkpoint, value) => trace.push({ checkpoint, value })) + const clean = check(h, trace) + if (clean.kind !== 'pass') + throw new Error('Clean construction failed: ' + JSON.stringify(clean)) + const corrupt = trace.filter( + (o) => + o.checkpoint !== 'reach' || + !builder.enters.includes(o.value as string), + ) + const result = check(h, corrupt) + if (result.kind !== 'violation' || result.law !== 'constructed reach') + throw new Error( + 'Generated missing-state control passed for ' + builder.name, + ) + } + } +} +declareOracle({ + model: [expected], + grammar: [arbitrary], + check: [check, calibrateGeneratedConstructions], +}) diff --git a/packages/query-db-collection/tests/upper-model-checks.test.ts b/packages/query-db-collection/tests/upper-model-checks.test.ts new file mode 100644 index 0000000000..73529de3dd --- /dev/null +++ b/packages/query-db-collection/tests/upper-model-checks.test.ts @@ -0,0 +1,61 @@ +import { expect, it } from 'vitest' +import { + createRefetchCallSettlementModel, + observeHandlerRefetchBoundary, + observeRefetchCallSettlement, + reduceHandlerRefetchBoundary, + reduceRefetchCallSettlement, +} from './ownership-model-oracle' + +// These are the model-only checks formerly embedded in production runs. +// Production law comparisons remain in the framework properties. +it('keeps a rejected refetch rejected while its independent sibling applies', () => { + let model = createRefetchCallSettlementModel(['detail', 'list'], true) + model = reduceRefetchCallSettlement(model, { + type: 'accept-application', + queryId: 'detail', + applicationId: 'detail-result', + }) + model = reduceRefetchCallSettlement(model, { + type: 'accept-application', + queryId: 'list', + applicationId: 'list-result', + }) + for (const queryId of ['detail', 'list']) + model = reduceRefetchCallSettlement(model, { + type: 'settle-fetch', + queryId, + outcome: 'fulfilled', + }) + model = reduceRefetchCallSettlement(model, { + type: 'settle-application', + applicationId: 'detail-result', + outcome: 'rejected', + }) + expect(observeRefetchCallSettlement(model)).toEqual({ refetch: 'rejected' }) + expect(model.applications.get('list-result')?.outcome).toBe('pending') + model = reduceRefetchCallSettlement(model, { + type: 'settle-application', + applicationId: 'list-result', + outcome: 'fulfilled', + }) + expect(observeRefetchCallSettlement(model)).toEqual({ refetch: 'rejected' }) +}) +it('retains successful handler refetch after its publication applies', () => { + let model = reduceHandlerRefetchBoundary( + { fetch: 'pending', application: 'unaccepted', throwOnError: true }, + { type: 'accept-application' }, + ) + model = reduceHandlerRefetchBoundary(model, { + type: 'settle-fetch', + outcome: 'fulfilled', + }) + model = reduceHandlerRefetchBoundary(model, { + type: 'settle-application', + outcome: 'fulfilled', + }) + expect(observeHandlerRefetchBoundary(model)).toEqual({ + refetch: 'resolved', + applicationErrorRecorded: false, + }) +}) diff --git a/packages/query-db-collection/vite.config.ts b/packages/query-db-collection/vite.config.ts index b8c7930f01..a6e6dc95f0 100644 --- a/packages/query-db-collection/vite.config.ts +++ b/packages/query-db-collection/vite.config.ts @@ -12,6 +12,20 @@ const config = defineConfig({ typecheck: { enabled: true, include: [`tests/**/*.test.ts`, `tests/**/*.test-d.ts`], + // oracleProperty registers runtime tests that the type-test collector + // cannot discover. These files remain in the TypeScript program. + exclude: [ + `tests/offline-executor-refresh-oracle.test.ts`, + `tests/ownership-lifecycle.oracle.test.ts`, + `tests/persisted-rejected-write-cutoff-oracle.test.ts`, + `tests/persisted-direct-ownership-oracle.test.ts`, + `tests/immediate-eager-materialization-oracle.test.ts`, + `tests/scope-omission-reacquisition-oracle.test.ts`, + `tests/ownership-settlement.oracle.test.ts`, + `tests/ownership-bottom.oracle.test.ts`, + `tests/ownership-upper.oracle.test.ts`, + `tests/ownership-confirmation.oracle.test.ts`, + ], }, }, }) diff --git a/packages/query-db-collection/vitest.config.ts b/packages/query-db-collection/vitest.config.ts index f867815d48..de9b035892 100644 --- a/packages/query-db-collection/vitest.config.ts +++ b/packages/query-db-collection/vitest.config.ts @@ -7,8 +7,19 @@ export default defineConfig({ enabled: true, include: [`tests/**/*.test.ts`], // oracleProperty registers runtime tests that the type-test collector - // cannot discover. The file remains in the TypeScript program. - exclude: [`tests/offline-executor-refresh-oracle.test.ts`], + // cannot discover. These files remain in the TypeScript program. + exclude: [ + `tests/offline-executor-refresh-oracle.test.ts`, + `tests/ownership-lifecycle.oracle.test.ts`, + `tests/persisted-rejected-write-cutoff-oracle.test.ts`, + `tests/persisted-direct-ownership-oracle.test.ts`, + `tests/immediate-eager-materialization-oracle.test.ts`, + `tests/scope-omission-reacquisition-oracle.test.ts`, + `tests/ownership-settlement.oracle.test.ts`, + `tests/ownership-bottom.oracle.test.ts`, + `tests/ownership-upper.oracle.test.ts`, + `tests/ownership-confirmation.oracle.test.ts`, + ], }, }, }) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index bd1159a187..d20cea04f5 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -135,6 +135,9 @@ export function rxdbCollectionOptions( ? null : validateSyncPersistenceCapability(params.metadata.persistence) + const replacesRetainedRows = + collection.health.reloading && + !collection._state.acceptedSyncedEntries().next().done let initialFetchComplete = false async function initialFetch() { /** @@ -144,13 +147,15 @@ export function rxdbCollectionOptions( */ let cursor: RxDocumentData | undefined = undefined const syncBatchSize = config.syncBatchSize ? config.syncBatchSize : 1000 - // Replace retained rows, including a persisted baseline whose restore - // has not completed. An empty, unwrapped Collection keeps ordinary - // commit ordering with pending mutations. + // Retained reloads replace atomically. First persisted startup keeps + // its one ordinary source commit and deletes only restored omissions. + const listed = + persistence && !replacesRetainedRows ? new Set() : undefined begin({ snapshot: - persistence !== null || - !collection._state.acceptedSyncedEntries().next().done, + persistence === null + ? !collection._state.acceptedSyncedEntries().next().done + : replacesRetainedRows, }) while (!initialFetchComplete) { @@ -193,6 +198,7 @@ export function rxdbCollectionOptions( query, ) const result = await rxCollection.storageInstance.query(preparedQuery) + if (isStopped()) return const docs = result.documents cursor = lastOfArray(docs) @@ -202,13 +208,36 @@ export function rxdbCollectionOptions( } docs.forEach((d) => { + listed?.add(getKey(d)) write({ type: `insert`, value: stripRxdbFields(clone(d)), }) }) } - await commit() + const firstApplied = commit() + if (firstApplied !== true) void firstApplied.catch(() => undefined) + const applied: Array> = [firstApplied] + if (listed && persistence) { + await persistence.hydrateBaseline() + if (isStopped()) return + // Copy omitted keys before committing: synchronous application can + // change the accepted-row iterator's backing map. Buffered RxDB live + // changes still apply after this initial result. + const gone: Array = [] + for (const [key] of collection._state.acceptedSyncedEntries()) { + if (!listed.has(key)) gone.push(key) + } + for (let first = 0; first < gone.length; first += syncBatchSize) { + begin() + for (const key of gone.slice(first, first + syncBatchSize)) + write({ type: `delete`, key }) + const receipt = commit() + if (receipt !== true) void receipt.catch(() => undefined) + applied.push(receipt) + } + } + await Promise.all(applied) } type WriteMessage = Parameters[0] @@ -225,6 +254,7 @@ export function rxdbCollectionOptions( } let sub: Subscription | undefined + const isStopped = () => sub?.closed !== false function stopOngoingFetch() { buffer.length = 0 if (!sub) return @@ -268,11 +298,10 @@ export function rxdbCollectionOptions( } async function start() { - const isCleanedUp = () => collection.status === `cleaned-up` - startOngoingFetch() + if (isStopped()) return await initialFetch() - if (isCleanedUp()) { + if (isStopped()) { return } @@ -286,7 +315,7 @@ export function rxdbCollectionOptions( begin() for (const msg of pending) write(msg) await commit() - if (isCleanedUp()) { + if (isStopped()) { return } } diff --git a/packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts b/packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts index be039e8709..fd82d07e65 100644 --- a/packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts +++ b/packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts @@ -1,85 +1,52 @@ -import { fc } from '@fast-check/vitest' -import { describe, it } from 'vitest' -import { - oracleFixedSeed, - oraclePropertyOptions, - oracleReplayTest, - oracleRoute, - oracleRouteOf, - oracleRuns, -} from '../../db/tests/oracle-config' +/** + * The shared eager-source model independently judges the original status, + * health, rows, field values, event-fold and every publication checkpoint. + * RxDB uses the controlled RxCollection with real RxSchema/query matcher + * and sort comparator. It declares listen-before-fetch and terminal failure + * after ready, with per-change publications and all seven JSON value kinds. + * This receiver has no real RxDB storage claim; rxdb.test.ts separately + * receives the reflected buffered insert on memory storage. + * Five generated constructions enforce the original minimum-five reach. Each + * builder returns fresh identities and preserves generated input variation. + */ +import { oracleProperty } from '../../db/tests/oracle-property' import { declareOracle } from '../../db/tests/oracle-declaration' -import { createOracleReach } from '../../db/tests/oracle-reach' import { checkEagerLiveSourceHistory, - eagerLiveSourceHistoryArb, eagerLiveSourceModel, - eagerLiveSourceProperty, - reachStatesFor, } from '../../db-adapter-conformance/src' +import { eagerLiveSourceCampaign } from '../../db-adapter-conformance/tests/eager-live-source-oracle-suite' +import { constructRxdbEagerHistory } from './eager-live-source-history-oracle' import { rxdbConformanceDriver } from './conformance-driver' -/** - * # RxDB on the shared eager live-source conformance oracle - * - * A receiving driver for `packages/db-adapter-conformance`. The shared - * module states the laws (READY-2/3/4/8/10, HEALTH-1, PUB-1/2/3/5/9, FID-4, - * LIFE-2/5/10, HEALTH-5), the model, the history grammar, and the check. This - * file supplies only the RxDB production driver in `conformance-driver.ts` - * and the campaigns; it states no expected results. - * - * RxDB declares `listen-before-fetch`: it subscribes to `$` before its - * initial fetch and buffers the events that arrive during it. It declares no - * other optional capability: it reads storage once per sync run, its change - * events carry no txid, and it has no transport or capability to renew. The claim is limited to the controlled - * RxCollection; a claim about a real RxDB storage needs a receiving witness - * against it; `rxdb.test.ts` holds one on memory storage for a buffered - * insert that the initial read already reflects. - */ - -const property = `adapter-conformance.eager-live-source.rxdb` -const driver = rxdbConformanceDriver() -const historyArb = eagerLiveSourceHistoryArb(driver) -// `field-removed` needs a held row with a note and a later upsert without -// one; at 400 histories every seed tried reached it at least 11 times. -const runs = 400 -// Startup bursts lengthen histories, so a campaign takes several seconds. -const campaignTimeoutMs = 120_000 - -async function campaign(options: fc.Parameters<[unknown]>) { - const reach = createOracleReach(property, reachStatesFor(driver), { - minimum: 5, - }) - oracleRoute(oracleRouteOf(options)) - await fc.assert(eagerLiveSourceProperty(driver, reach), options) - return reach -} - -describe(`eager live-source conformance: RxDB`, () => { - oracleReplayTest(it)( - `conforms on a fixed-seed campaign`, - async () => { - const reach = await campaign( - oracleFixedSeed(2118, { numRuns: oracleRuns(runs) }), - ) - reach.assertReached() - }, - campaignTimeoutMs, - ) - - oracleReplayTest(it, property)( - `conforms on a random campaign or a replayed seed`, - async () => { - const reach = await campaign(oraclePropertyOptions(runs, property)) - reach.assertReached() - }, - campaignTimeoutMs, - ) +const campaign = eagerLiveSourceCampaign( + rxdbConformanceDriver(), + constructRxdbEagerHistory, +) +oracleProperty({ + id: `adapter-conformance.eager-live-source.rxdb`, + law: { + statement: `Initial readiness, complete row and field fidelity, coherent change publications, failure identity, post-ready health, restart and exactly-once resource release follow the shared eager live-source laws.`, + authority: `db-adapter-conformance/src/eager-live-source-oracle.ts; adapter law set READY-2/3/4/8/10, HEALTH-1/5, PUB-1/2/3/5/9, FID-1/4, LIFE-2/5/10, as supported by this receiver's declarations.`, + owner: `Adapter conformance: eager live sources (#2118), rxdb receiver`, + limits: `Controlled RxCollection with real query semantics, seven JSON value kinds, three startup batches plus twelve steps, eager mode and per-change commits. No remote replication, persisted storage, mutation, reload or on-demand claim.`, + }, + histories: { + arbitrary: campaign.arbitrary, + states: campaign.states, + runs: 400, + fixedSeeds: [2118], + constructed: campaign.constructed, + }, + model: campaign.model, + driver: campaign.driver, + check: campaign.check, + calibrations: campaign.calibrations, + timeout: 120000, }) - declareOracle({ owns: [`src/rxdb.ts#rxdbCollectionOptions`], - check: [checkEagerLiveSourceHistory, eagerLiveSourceProperty], + check: [checkEagerLiveSourceHistory, campaign.check], model: [eagerLiveSourceModel.step, eagerLiveSourceModel.expected], - grammar: [historyArb], + grammar: [campaign.arbitrary], }) diff --git a/packages/rxdb-db-collection/tests/eager-live-source-history-oracle.ts b/packages/rxdb-db-collection/tests/eager-live-source-history-oracle.ts new file mode 100644 index 0000000000..3a4cd84d52 --- /dev/null +++ b/packages/rxdb-db-collection/tests/eager-live-source-history-oracle.ts @@ -0,0 +1,78 @@ +/** + * RxDB constructions retain its three startup batches plus twelve-step + * grammar. Three updates to one key straddle read point two: one read batch + * supersedes the first, and a later batch removes the optional field. Two + * loads and three live batches cover all seven JSON value kinds. The shared + * model still judges every original checkpoint; this code constructs inputs. + */ +import { describeValue, jsonValueKinds } from '../../db-adapter-conformance/src' +import type { + ConformanceRow, + EagerLiveSourceHistory, +} from '../../db-adapter-conformance/src' + +const examples = [ + [``, `plain`, `two\nlines`, `ünïcødé ✓`], + [0, -7, 2 ** 31, Number.MAX_SAFE_INTEGER], + [0.5, -1.25, 1e-7], + [true, false], + [null], + [[], [0, `a`, null], [[1], [true]]], + [{}, { a: 0, b: `x` }, { nested: { deep: [1, { k: null }] } }, { '': false }], +] as const +export function constructRxdbEagerHistory( + generated: EagerLiveSourceHistory, + salt: number, +): EagerLiveSourceHistory { + let hash = salt + 1 + for (const char of describeValue(generated)) + hash = (Math.imul(hash, 31) + char.charCodeAt(0)) >>> 0 + const kinds = jsonValueKinds.map( + (_, index) => (index + salt) % jsonValueKinds.length, + ) + const row = (kind: number, index: number, note: boolean): ConformanceRow => ({ + id: ((hash + index) % 5) + 1, + title: ([`a`, `b`, `c`] as const)[(hash + index) % 3]!, + ...(note ? { note: hash % 2 ? `x` : `y` } : {}), + value: structuredClone( + examples[kind]![(hash + index) % examples[kind]!.length], + ), + }) + const first = row(kinds[0]!, 0, true) + const firstUpdate = { + ...first, + title: ([`a`, `b`, `c`] as const)[(hash + 1) % 3]!, + } + const secondUpdate = { + ...first, + title: ([`a`, `b`, `c`] as const)[(hash + 2) % 3]!, + } + const thirdUpdate = { ...row(kinds[0]!, 0, false) } + const changes = kinds.map((kind, index) => ({ + type: `upsert` as const, + row: row(kind, index, false), + })) + return [ + { kind: `changes`, changes: [{ type: `upsert`, row: firstUpdate }] }, + { kind: `changes`, changes: [{ type: `upsert`, row: secondUpdate }] }, + { kind: `changes`, changes: [{ type: `upsert`, row: thirdUpdate }] }, + { + kind: `snapshot`, + rows: kinds.slice(0, 4).map((kind, index) => row(kind, index, true)), + seen: 2, + }, + { kind: `fail` }, + { kind: `restart` }, + { kind: `fail` }, + { kind: `restart` }, + { + kind: `snapshot`, + rows: kinds.slice(4).map((kind, index) => row(kind, index, true)), + seen: 0, + }, + { kind: `changes`, changes: changes.slice(0, 3) }, + { kind: `changes`, changes: changes.slice(3, 6) }, + { kind: `changes`, changes: changes.slice(6) }, + ...structuredClone(generated.slice(0, 3)), + ] +} diff --git a/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-driver.ts b/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-driver.ts new file mode 100644 index 0000000000..1ceafa7fd7 --- /dev/null +++ b/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-driver.ts @@ -0,0 +1,102 @@ +/** Real RxDB memory storage supplies rows; query settlement is controlled. */ +import { createRxDatabase } from 'rxdb/plugins/core' +import { getRxStorageMemory } from 'rxdb/plugins/storage-memory' +import { Observable } from 'rxjs' +import { baselineGate } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-driver' +import { hydrationLifecycleRows } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' +import { rxdbCollectionOptions } from '../src/rxdb' +import type { RxChangeEvent, RxCollection } from 'rxdb/plugins/core' +import type { Subscriber } from 'rxjs' +import type { BaselineLifecycleAdapter } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-driver' +import type { HydrationLifecycleRow } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' + +let sequence = 0 +export const rxdbBaselineLifecycleAdapter: BaselineLifecycleAdapter = async ( + h, + id, +) => { + const database = await createRxDatabase<{ + rows: RxCollection + }>({ + name: `baselinelifecycle${sequence++}`, + storage: getRxStorageMemory(), + multiInstance: false, + }) + const { rows } = await database.addCollections({ + rows: { + schema: { + version: 0, + type: `object`, + primaryKey: `id`, + properties: { + id: { type: `string`, maxLength: 10 }, + value: { type: `number` }, + }, + required: [`id`, `value`], + }, + }, + }) + const replace = async (values: Array) => { + await rows.find().remove() + const result = await rows.bulkInsert(values) + if (result.error.length) + throw new Error(`RxDB lifecycle source insert failed`) + } + await replace(hydrationLifecycleRows(h.old, 2)) + const source = rows.$ + const sessions: Array<{ + gate: ReturnType> + releases: number + first: boolean + subscriber: Subscriber> + }> = [] + Object.defineProperty(rows, `$`, { + value: new Observable>( + (subscriber) => { + const session = { + gate: baselineGate(), + releases: 0, + first: true, + subscriber, + } + sessions.push(session) + const actual = source.subscribe(subscriber) + return () => { + session.releases++ + actual.unsubscribe() + } + }, + ), + }) + const query = rows.storageInstance.query.bind(rows.storageInstance) + rows.storageInstance.query = async (prepared) => { + const session = sessions.at(-1) + if (!session) throw new Error(`RxDB query has no lifecycle subscription`) + if (session.first) { + session.first = false + await session.gate.promise + } + return query(prepared) + } + return { + options: rxdbCollectionOptions({ + id, + rxCollection: rows, + syncMode: `eager`, + syncBatchSize: h.batchSize, + }), + sessions: () => sessions.length, + deliver: (run) => sessions[run]?.gate.resolve(), + fail: (run, error) => sessions[run]!.gate.reject(error), + streamFail: (run, error) => sessions[run]!.subscriber.error(error), + replace, + event: async (row) => { + await rows.insert(row) + }, + releases: () => sessions.map((session) => session.releases), + hookReleases: () => [], + hookAcquisitions: () => 0, + diagnostics: () => [], + dispose: () => database.remove(), + } +} diff --git a/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts b/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts new file mode 100644 index 0000000000..0f0ec7444f --- /dev/null +++ b/packages/rxdb-db-collection/tests/persisted-baseline-lifecycle-oracle.test.ts @@ -0,0 +1,47 @@ +import { declareOracle } from '../../db/tests/oracle-declaration' +import { baselineLifecycleViolations } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' +import { observeBaselineLifecycle } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-driver' +import { registerBaselineLifecycle } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle-suite' +import { rxdbBaselineLifecycleAdapter } from './persisted-baseline-lifecycle-driver' + +declareOracle({ + owns: [`src/rxdb.ts#rxdbCollectionOptions`], + check: [baselineLifecycleViolations], +}) + +for (const { host, config } of [ + { + host: `controlled`, + config: { + property: `adapter-conformance.persisted-baseline-lifecycle.rxdb.controlled`, + defaultRuns: 24, + streamFailure: true, + }, + }, + { + host: `node-sqlite`, + config: { + property: `adapter-conformance.persisted-baseline-lifecycle.rxdb.node-sqlite`, + defaultRuns: 24, + streamFailure: true, + }, + }, +] as const) { + registerBaselineLifecycle(config, async (h, id) => { + const storage = createStartupHost(host) + return withOracleCleanup( + () => + observeBaselineLifecycle( + rxdbBaselineLifecycleAdapter, + storage.persistence, + storage.holdBaseline, + h, + id, + process.env.TANSTACK_DB_BASELINE_EXPECT_HYDRATE !== `0`, + ), + [() => storage.close()], + ) + }) +} diff --git a/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index 9a65865e93..a2059e6275 100644 --- a/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/rxdb-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -5,13 +5,13 @@ import { describe } from 'vitest' import { declareOracle } from '../../db/tests/oracle-declaration' import { - persistedStartupHistories, persistedStartupHistoryArb, + persistedStartupSingleCommitHistories, } from '../../db-adapter-conformance/tests/persisted-startup-oracle' import { registerPersistedStartupConformance } from '../../db-adapter-conformance/tests/persisted-startup-oracle-suite' import { rxdbStartupAdapter } from './persisted-startup-driver' -const historyArb = persistedStartupHistoryArb(`collection`) +const historyArb = persistedStartupHistoryArb(`collection`, false, true) // The receiving path judges persisted startup through this adapter entry. // The shared model owns replacement; this list lets the gap CLI credit it. declareOracle({ @@ -22,7 +22,7 @@ describe(`persisted startup conformance: RxDB`, () => { registerPersistedStartupConformance({ property: `adapter.rxdb.persisted-startup`, adapter: rxdbStartupAdapter, - histories: persistedStartupHistories, + histories: persistedStartupSingleCommitHistories, arbitrary: historyArb, scopedStartupCommit: true, }) diff --git a/packages/rxdb-db-collection/tests/persisted-startup-driver.ts b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts index 2cde8fd806..a24fffe8c9 100644 --- a/packages/rxdb-db-collection/tests/persisted-startup-driver.ts +++ b/packages/rxdb-db-collection/tests/persisted-startup-driver.ts @@ -88,6 +88,7 @@ export const rxdbStartupAdapter: PersistedStartupAdapter ({ rows: sortedStartupRows(collection.values()) }), deliver: async () => { diff --git a/packages/trailbase-db-collection/package.json b/packages/trailbase-db-collection/package.json index e582016fb1..022ca87134 100644 --- a/packages/trailbase-db-collection/package.json +++ b/packages/trailbase-db-collection/package.json @@ -21,7 +21,7 @@ "dev": "vite build --watch", "lint": "eslint . --fix", "test": "vitest --run", - "test:oracles": "vitest run tests/lifecycle-oracle.property.test.ts tests/persisted-startup-conformance-oracle.property.test.ts", + "test:oracles": "vitest run tests/lifecycle-oracle.property.test.ts tests/eager-live-source-conformance-oracle.property.test.ts tests/persisted-startup-conformance-oracle.property.test.ts", "test:e2e": "vitest --run --config vitest.e2e.config.ts" }, "type": "module", diff --git a/packages/trailbase-db-collection/tests/ORACLE.md b/packages/trailbase-db-collection/tests/ORACLE.md index d3fb302c58..85d0df26e2 100644 --- a/packages/trailbase-db-collection/tests/ORACLE.md +++ b/packages/trailbase-db-collection/tests/ORACLE.md @@ -43,7 +43,7 @@ The same interpreter runs a fixed corpus and generated histories. an event touched. Every page row is written as a full-row `update`, which creates a missing row and replaces a held one, so a page never collides with an event that core accepted but still holds. The page-event grammar enumerates - 15 histories: eager startup or an on-demand load; an update, delete, or + 28 histories: eager startup or an on-demand load; an update, delete, or insert; committed before the list call (on-demand only), while the page is in flight, or after it applies. It compares the Collection's rows with the service after the page settles and after a later change. A generated grammar @@ -51,7 +51,7 @@ The same interpreter runs a fixed corpus and generated histories. pages of 256, cursor or offset paging, one or two overlapping on-demand loads, and up to four inserts, updates, and deletes among page releases. After every step a changed key holds the service's value; after the history drains, every - returned or changed key does. Four fixed histories cover an update between + returned or changed key does. Five fixed histories cover an insert and its update in transit, an update between pages, an update then a delete during a page, overlapping loads around an update, and offset paging with two updates. After the history drains, the Collection holds exactly the rows that a page returned or an event touched and @@ -133,9 +133,10 @@ The same interpreter runs a fixed corpus and generated histories. Histories contain one to three Collection sync runs, eager/on-demand modes, delayed startup and list resolve/reject, zero to eight row edits, five stream -endings, and immediate versus settled cleanup. Thirty-two fixed histories cover -the boundaries; ordinary runs add 50 histories from a fixed-seed campaign and 50 -from a random campaign, with shrinking. Additional controls cover rejecting late +endings, and immediate versus settled cleanup. Thirty-two corpus histories and four additional fixed histories cover +the boundaries and metadata absence. Ordinary runs retain a 50-history fixed-seed +campaign and a 50-history random campaign, with shrinking, plus thirteen +randomized constructed histories per campaign. Additional controls cover rejecting late cancellation in both modes and mutate provider-owned list, event, and buffered rows before parsing. Model rows derive separately from command scalars, so provider mutation cannot change the answer. @@ -154,6 +155,35 @@ separately. The lifecycle production driver calls the core subset boundary; the early-demand grammar also drives a real live query. Service-backed E2E coverage remains separate. +## Framework conversion and enforced reach + +`oracleProperty` now registers the lifecycle and page/event campaigns. Their +run budgets and fixed seeds remain 50 / 714203 and 40 / 205917. The finite +`trailbase.lifecycle.boundaries` property runs all 51 existing boundary histories: +16 early-demand, 28 single-page race, four held-page, two stream-end and one +fresh-run history. All 36 lifecycle and five page/event fixed histories remain. + +The authored service and independent Maps still compute row truth. The +production driver transports those reference facts separately from public +observations. `lifecycle-judgment-oracle.ts` applies the reusable relations at +every original cut. It keeps exact startup-error and resource identity through +opaque tokens. Reports and detached rejections keep append-only events and +opaque reason identity, including DOMException, class instances and symbols; +this law forbids any such event, regardless of its reason's value. + +The lifecycle grammar declares thirteen required states; the page/event grammar +declares seven. Each has a randomized constructed builder that returns a fresh +object. A WeakMap retains each generated construction's required states, so +ordinary generated outputs have the same obligation as named constructions. +Missing-state controls exercise each builder. Separate generated-only controls +use outputs that differ from every named fixed-seed construction. + +The grammar now reconstructs absent metadata fields, rejecting late cancellation +and independently allocated page command objects. The page mapper has a guarded +inverse. These changes expand reconstruction without removing an old history. +The complete same-seed mutation retention gate remains required before claiming +that this pure framework conversion retained every previous kill. + ## Run and replay Run from `packages/trailbase-db-collection`: @@ -166,11 +196,11 @@ TANSTACK_DB_ORACLE_RUNS_MULTIPLIER=10 pnpm test:oracles --coverage.enabled=false For a failing random campaign, use its reported seed and shrink path: ```sh -TANSTACK_DB_ORACLE_SEED=123 TANSTACK_DB_ORACLE_PATH=0:1 TANSTACK_DB_ORACLE_PROPERTY=trailbase.lifecycle node --import tsx ../db/tests/oracle-replay.ts tests/lifecycle-oracle.property.test.ts --coverage.enabled=false -t 'random or replayed' +TANSTACK_DB_ORACLE_SEED=123 TANSTACK_DB_ORACLE_PATH=0:1 TANSTACK_DB_ORACLE_PROPERTY=trailbase.lifecycle node --import tsx ../db/tests/oracle-replay.ts tests/lifecycle-oracle.property.test.ts --coverage.enabled=false -t 'trailbase.lifecycle.*random seed' ``` -Use `TANSTACK_DB_ORACLE_PROPERTY=trailbase.page-event` and `-t 'generated page -histories with a random or replayed seed'` for the page-event grammar. +Use `TANSTACK_DB_ORACLE_PROPERTY=trailbase.page-event` and +`-t 'trailbase.page-event.*random seed'` for the page-event grammar. Replace the example seed/path with the failure's values. The guarded runner requires a witness that the named property executed with this seed and path; diff --git a/packages/trailbase-db-collection/tests/conformance-driver.ts b/packages/trailbase-db-collection/tests/conformance-driver.ts index c3ad1e71ee..419ecce4ba 100644 --- a/packages/trailbase-db-collection/tests/conformance-driver.ts +++ b/packages/trailbase-db-collection/tests/conformance-driver.ts @@ -35,8 +35,7 @@ import { observedView, toSqlRow, } from '../../db-adapter-conformance/src' -import { MockRecordApi } from './mock-record-api' -import type { Event, ListResponse } from 'trailbase' +import type { Event, ListResponse, RecordApi } from 'trailbase' import type { ConformanceRow, ControlledProvider, @@ -68,35 +67,50 @@ interface ProviderSession { } class TrailBaseProvider implements ControlledProvider { - readonly api = new MockRecordApi() + readonly api: RecordApi readonly sessions: Array = [] private rows = new Map() constructor() { - this.api.subscribe.mockImplementation(() => { - let controller!: ReadableStreamDefaultController - const session = { cancels: 0, locked: false } as ProviderSession - const stream = new ReadableStream({ - start: (value) => { - controller = value - }, - cancel: () => { - session.cancels += 1 - }, - }) - session.stream = stream - session.controller = controller - this.sessions.push(session) - // A new sync run lists the source again from its current rows. - this.rows = new Map() - return Promise.resolve(stream) - }) - this.api.list.mockImplementation( - () => + const unsupported = () => { + throw new Error(`unused eager RecordApi operation`) + } + this.api = { + listOp: unsupported, + listGeoOp: unsupported, + read: unsupported, + readOp: unsupported, + create: unsupported, + createBulk: unsupported, + createOp: unsupported, + update: unsupported, + updateOp: unsupported, + delete: unsupported, + deleteOp: unsupported, + subscribeAll: unsupported, + subscribe: () => { + let controller!: ReadableStreamDefaultController + const session = { cancels: 0, locked: false } as ProviderSession + const stream = new ReadableStream({ + start: (value) => { + controller = value + }, + cancel: () => { + session.cancels += 1 + }, + }) + session.stream = stream + session.controller = controller + this.sessions.push(session) + // A new sync run lists the source again from its current rows. + this.rows = new Map() + return Promise.resolve(stream) + }, + list: () => new Promise>((resolve, reject) => { this.latest().list = { resolve, reject } }), - ) + } } private latest(): ProviderSession { diff --git a/packages/trailbase-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts b/packages/trailbase-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts index 3472958170..39e869e3b0 100644 --- a/packages/trailbase-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts +++ b/packages/trailbase-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts @@ -1,23 +1,16 @@ -import { fc } from '@fast-check/vitest' -import { describe, it } from 'vitest' -import { - oracleFixedSeed, - oraclePropertyOptions, - oracleReplayTest, - oracleRoute, - oracleRouteOf, - oracleRuns, -} from '../../db/tests/oracle-config' import { declareOracle } from '../../db/tests/oracle-declaration' -import { createOracleReach } from '../../db/tests/oracle-reach' import { checkEagerLiveSourceHistory, eagerLiveSourceHistoryArb, eagerLiveSourceModel, - eagerLiveSourceProperty, - reachStatesFor, } from '../../db-adapter-conformance/src' import { trailBaseConformanceDriver } from './conformance-driver' +import { + constructTrailBaseEagerHistory, + observeTrailBaseEagerHistory, + trailBaseEagerVerdict, +} from './eager-live-source-framework-oracle' +import { registerTrailBaseEager } from './eager-live-source-oracle-suite' /** * # TrailBase on the shared eager live-source conformance oracle @@ -41,46 +34,17 @@ import { trailBaseConformanceDriver } from './conformance-driver' const property = `adapter-conformance.eager-live-source.trailbase` const driver = trailBaseConformanceDriver() const historyArb = eagerLiveSourceHistoryArb(driver) -// `field-removed` needs a held row with a note and a later upsert without -// one. With value kinds, 400 histories reached it as few as 7 times across -// eight seeds, so TrailBase runs 600. -const runs = 600 -const campaignTimeoutMs = 120_000 - -async function campaign(options: fc.Parameters<[unknown]>) { - const reach = createOracleReach(property, reachStatesFor(driver), { - minimum: 5, - }) - oracleRoute(oracleRouteOf(options)) - await fc.assert(eagerLiveSourceProperty(driver, reach), options) - return reach -} -describe(`eager live-source conformance: TrailBase`, () => { - oracleReplayTest(it)( - `conforms on a fixed-seed campaign`, - async () => { - const reach = await campaign( - oracleFixedSeed(2118, { numRuns: oracleRuns(runs) }), - ) - reach.assertReached() - }, - campaignTimeoutMs, - ) - - oracleReplayTest(it, property)( - `conforms on a random campaign or a replayed seed`, - async () => { - const reach = await campaign(oraclePropertyOptions(runs, property)) - reach.assertReached() - }, - campaignTimeoutMs, - ) -}) +registerTrailBaseEager( + { property, defaultRuns: 600 }, + driver, + (history, corruptValues) => + observeTrailBaseEagerHistory(driver, history, corruptValues), +) declareOracle({ owns: [`src/trailbase.ts#trailBaseCollectionOptions`], - check: [checkEagerLiveSourceHistory, eagerLiveSourceProperty], + check: [checkEagerLiveSourceHistory, trailBaseEagerVerdict], model: [eagerLiveSourceModel.step, eagerLiveSourceModel.expected], - grammar: [historyArb], + grammar: [historyArb, constructTrailBaseEagerHistory], }) diff --git a/packages/trailbase-db-collection/tests/eager-live-source-framework-oracle.ts b/packages/trailbase-db-collection/tests/eager-live-source-framework-oracle.ts new file mode 100644 index 0000000000..aefbc4ac63 --- /dev/null +++ b/packages/trailbase-db-collection/tests/eager-live-source-framework-oracle.ts @@ -0,0 +1,202 @@ +/** + * The shared eager-source checker remains the independent judge at every + * original checkpoint. This bridge records its named mismatch as framework + * comparison evidence; fixture/setup errors still escape as driver errors. + * Five randomized constructions preserve TrailBase's minimum-five reach across + * the same twelve-step grammar. They vary rows, titles, notes, value examples + * and key rotation from the generated history, and return fresh objects. + */ +import { + ConformanceViolation, + checkEagerLiveSourceHistory, + describeValue, + jsonValueKinds, +} from '../../db-adapter-conformance/src' +import type { + ConformanceRow, + EagerLiveSourceDriver, + EagerLiveSourceHistory, + EagerLiveSourceReachState, +} from '../../db-adapter-conformance/src' +import type { Observation, Verdict } from '../../db/tests/oracle-property' + +export type EagerHistoryResult = { + browserErrors?: ReadonlyArray + reached: Array + result?: { accepted: number; skipped: number } + failure?: { + law: string + step: number + expected: unknown + actual: unknown + cleanup: Array + } +} + +// These are members of the unchanged shared grammar's value-example domain. +// They construct inputs only; the shared model owns all expected row truth. +const examples = [ + [``, `plain`, `two\nlines`, `ünïcødé ✓`], + [0, -7, 2 ** 31, Number.MAX_SAFE_INTEGER], + [0.5, -1.25, 1e-7], + [true, false], + [null], + [[], [0, `a`, null], [[1], [true]]], + [{}, { a: 0, b: `x` }, { nested: { deep: [1, { k: null }] } }, { '': false }], +] as const + +export function constructTrailBaseEagerHistory( + generated: EagerLiveSourceHistory, + salt: number, +): EagerLiveSourceHistory { + let hash = salt + 1 + for (const char of describeValue(generated)) + hash = (Math.imul(hash, 31) + char.charCodeAt(0)) >>> 0 + const offset = hash % 5 + const kinds = jsonValueKinds.map( + (_, index) => (index + salt) % jsonValueKinds.length, + ) + const row = (kind: number, index: number, note: boolean): ConformanceRow => ({ + id: ((index + offset) % 5) + 1, + title: ([`a`, `b`, `c`] as const)[(hash + index) % 3]!, + ...(note ? { note: hash % 2 ? `x` : `y` } : {}), + value: structuredClone( + examples[kind]![(hash + index) % examples[kind]!.length], + ), + }) + const initial = kinds.slice(0, 4).map((kind, index) => row(kind, index, true)) + const changes = kinds.map((kind, index) => ({ + type: `upsert` as const, + row: row(kind, index, false), + })) + // Fail before and after readiness, then restart each failed run. The + // malformed first delivery checks duplicate replacement. Two later loads + // partition the seven kinds because a load holds at most four rows. Three + // batches carry all seven change kinds (at most three changes per batch), + // and the first update removes a note from a held row. This uses the + // existing twelve-step bound; no extra grammar length is introduced. + return [ + { kind: `fail` }, + { kind: `restart` }, + { + kind: `malformed`, + rows: [row(kinds[hash % 7]!, 0, true)], + duplicate: hash % 3, + }, + { kind: `fail` }, + { kind: `restart` }, + { kind: `snapshot`, rows: initial }, + { kind: `changes`, changes: changes.slice(0, 3) }, + { kind: `changes`, changes: changes.slice(3, 6) }, + { kind: `changes`, changes: changes.slice(6) }, + { kind: `fail` }, + { kind: `restart` }, + { + kind: `snapshot`, + rows: kinds.slice(4).map((kind, index) => row(kind, index, false)), + }, + ] +} + +export async function observeTrailBaseEagerHistory( + driver: EagerLiveSourceDriver, + history: EagerLiveSourceHistory, + corruptValues = false, +): Promise { + const reached: Array = [] + const actualDriver = corruptValues + ? { + ...driver, + create: () => { + const fixture = driver.create() + const collection = fixture.collection + return { + ...fixture, + collection: { + ...collection, + get status() { + return collection.status + }, + get health() { + return collection.health + }, + *values() { + for (const row of collection.values()) + yield { ...row, value: { incorrect: true } } + }, + }, + } + }, + } + : driver + try { + const result = await checkEagerLiveSourceHistory(actualDriver, history, { + reach: (state) => reached.push(state), + }) + return { reached, result } + } catch (error) { + const primary = error instanceof AggregateError ? error.cause : error + if (!(primary instanceof ConformanceViolation)) throw error + return { + reached, + failure: { + law: primary.law, + step: primary.step, + expected: primary.expected, + actual: primary.actual, + cleanup: + error instanceof AggregateError ? error.errors.map(String) : [], + }, + } + } +} + +export function trailBaseEagerVerdict( + trace: ReadonlyArray, + required: ReadonlyArray, +): Verdict { + const completed = trace.find((entry) => entry.checkpoint === `completed`) + const actual = completed?.value as EagerHistoryResult | undefined + const browserErrors = actual?.browserErrors ?? [] + const secondary = browserErrors.length + ? `; secondary browser errors: ${browserErrors.join(`; `)}` + : `` + if (actual?.failure) { + const { law, step, expected, actual: observed, cleanup } = actual.failure + return { + kind: `violation`, + checkpoint: `step:${step}`, + law, + difference: `${law} at original shared checkpoint ${step}${cleanup.length ? `; secondary cleanup: ${cleanup.join(`; `)}` : ``}${secondary}`, + values: { expected, actual: observed }, + } + } + const reached = new Set( + trace + .filter((entry) => entry.checkpoint === `reach`) + .map((entry) => entry.value), + ) + const missing = required.filter((state) => !reached.has(state)) + if (missing.length) + return { + kind: `violation`, + checkpoint: `reach`, + law: `constructed builder reach`, + difference: `generated construction omitted ${missing.join(`, `)}${secondary}`, + } + if (!actual?.result) + return { + kind: `violation`, + checkpoint: `completed`, + law: `eager source comparison`, + difference: `shared comparison did not complete${secondary}`, + } + if (browserErrors.length) + return { + kind: `violation`, + checkpoint: `browser`, + law: `detached-browser-error`, + difference: browserErrors.join(`; `), + } + return { kind: `pass` } +} diff --git a/packages/trailbase-db-collection/tests/eager-live-source-oracle-suite.ts b/packages/trailbase-db-collection/tests/eager-live-source-oracle-suite.ts new file mode 100644 index 0000000000..df50e2c231 --- /dev/null +++ b/packages/trailbase-db-collection/tests/eager-live-source-oracle-suite.ts @@ -0,0 +1,98 @@ +/** TrailBase retains every shared law and checkpoint with framework campaigns. */ +import { oracleProperty } from '../../db/tests/oracle-property' +import { + eagerLiveSourceHistoryArb, + reachStatesFor, +} from '../../db-adapter-conformance/src' +import { + constructTrailBaseEagerHistory, + trailBaseEagerVerdict, +} from './eager-live-source-framework-oracle' +import type { + EagerLiveSourceDriver, + EagerLiveSourceHistory, +} from '../../db-adapter-conformance/src' +import type { EagerHistoryResult } from './eager-live-source-framework-oracle' +import type { Observation } from '../../db/tests/oracle-property' + +type Receive = ( + history: EagerLiveSourceHistory, + corruptValues?: boolean, +) => Promise +export function registerTrailBaseEager( + campaign: { property: string; defaultRuns: number }, + driver: EagerLiveSourceDriver, + receive: Receive, +) { + const states = reachStatesFor(driver) + const required = new WeakMap< + EagerLiveSourceHistory, + { salt: number; states: ReadonlyArray } + >() + const constructed = Array.from({ length: 5 }, (_, salt) => ({ + name: `all eager states, randomized construction ${salt + 1}`, + enters: states, + build: (generated: EagerLiveSourceHistory) => { + const history = constructTrailBaseEagerHistory(generated, salt) + required.set(history, { salt, states }) + return history + }, + })) + const drive = + (corruptValues = false) => + async ( + history: EagerLiveSourceHistory, + observe: (checkpoint: string, value: unknown) => void, + ) => { + const result = await receive(history, corruptValues) + for (const state of result.reached) observe(`reach`, state) + observe(`completed`, result) + } + oracleProperty({ + id: campaign.property, + law: { + statement: `The eager source's initial gate, complete rows and field values, per-change publications, failure identity, post-ready health, restart and exactly-once resource release follow the shared eager live-source laws.`, + authority: `db-adapter-conformance/src/eager-live-source-oracle.ts, adapter law set READY-2/3/4, HEALTH-1/5, PUB-1/2/3, FID-1/4, LIFE-2/5/10; TrailBase tests/ORACLE.md.`, + owner: `Adapter conformance: eager live sources (#2118), TrailBase receiver`, + limits: `Controlled RecordApi, already decoded events, native ReadableStream; seven JSON value kinds, twelve grammar steps, eager mode and per-change commits. No deployed service, SDK fetch/SSE, mutation acknowledgement, on-demand loading or reload claim.`, + }, + histories: { + arbitrary: eagerLiveSourceHistoryArb(driver), + states, + runs: campaign.defaultRuns, + fixedSeeds: [2118], + constructed, + }, + // The integrated shared checker runs its independent Map/phase model at + // every original checkpoint. This outer model adds only builder premises; + // no expected product result is inferred from production observations. + model: (history) => ({ required: required.get(history)?.states ?? [] }), + driver: drive(), + check: (_history, trace, expected) => + trailBaseEagerVerdict(trace, expected.required), + calibrations: [ + { + name: `shared value judgment rejects wrong public values`, + law: `FID-1 (values)`, + driver: drive(true), + }, + ...constructed.flatMap((_builder, salt) => + states.map((state) => ({ + name: `construction ${salt + 1} omits ${state}`, + law: `constructed builder reach`, + corrupt: ( + trace: ReadonlyArray, + history: EagerLiveSourceHistory, + ) => + required.get(history)?.salt === salt + ? trace.filter( + (entry) => + !(entry.checkpoint === `reach` && entry.value === state), + ) + : undefined, + })), + ), + ], + timeout: 120_000, + }) +} diff --git a/packages/trailbase-db-collection/tests/lifecycle-hydration-oracle-suite.ts b/packages/trailbase-db-collection/tests/lifecycle-hydration-oracle-suite.ts new file mode 100644 index 0000000000..c5a74f7e8b --- /dev/null +++ b/packages/trailbase-db-collection/tests/lifecycle-hydration-oracle-suite.ts @@ -0,0 +1,222 @@ +/** + * Campaign registration for the primary lifecycle owner's persisted receiver. + * The independent row law and comparison stay in lifecycle-hydration-oracle; + * this module supplies generated reach and framework registration only. Both + * Node storage and real Chromium use the same model, grammar, and three cuts. + */ +import { oracleProperty, pass, violation } from '../../db/tests/oracle-property' +import { + hydrationLifecycleHistories, + hydrationLifecycleHistoryArb, + hydrationLifecycleStates, + hydrationLifecycleViolations, +} from './lifecycle-hydration-oracle' +import type { Observation, Verdict } from '../../db/tests/oracle-property' +import type { + HydrationLifecycleHistory, + HydrationLifecycleObservation, +} from './lifecycle-hydration-oracle' + +type State = (typeof hydrationLifecycleStates)[number] +export type HydrationBrowserObservation = HydrationLifecycleObservation & { + browserErrors?: ReadonlyArray +} +type Receiver = ( + history: HydrationLifecycleHistory, + id: string, +) => Promise + +// A builder changes only the premise it promises. Other row sets, the render +// option and the retirement cut remain generated. Rejected lists carry no +// payload. Each builder invocation returns a fresh history object. +const builds: Array<{ + state: State + build: (h: HydrationLifecycleHistory) => HydrationLifecycleHistory +}> = [ + { + state: `hydration-held-cleanup`, + build: (h) => ({ ...h, oldList: `complete` }), + }, + { + state: `late-list-rejection`, + build: (h) => ({ ...h, oldList: `reject`, old: 0 }), + }, + ...([`cleaned-up`, `replacement-loading`, `replacement-ready`] as const).map( + (resumeAt) => ({ + state: `resume-${resumeAt}`, + build: (h: HydrationLifecycleHistory) => ({ ...h, resumeAt }), + }), + ), + { + state: `empty-replacement`, + build: (h) => ({ ...h, replacement: 0 }), + }, + { + state: `nonempty-replacement`, + build: (h) => ({ + ...h, + replacement: h.replacement || 1 << (h.previous % 3), + }), + }, + { + state: `new-row-outside-old-list`, + build: (h) => { + const key = 1 << (h.previous % 3) + return { + ...h, + oldList: `complete`, + old: h.old & ~key, + replacement: h.replacement | key, + } + }, + }, + { + state: `old-row-omitted`, + build: (h) => { + const key = 1 << (h.previous % 3) + return { + ...h, + oldList: `complete`, + old: h.old | key, + replacement: h.replacement & ~key, + } + }, + }, +] + +export function registerHydrationLifecycle( + property: string, + receive: Receiver, +) { + const required = new WeakMap< + HydrationLifecycleHistory, + ReadonlyArray + >() + let sequence = 0 + const constructed = builds.map(({ state, build }) => ({ + name: `generated ${state}`, + enters: [state], + build: (generated: HydrationLifecycleHistory) => { + const history = { ...build(generated) } + required.set(history, [state]) + return history + }, + })) + oracleProperty({ + id: property, + law: { + statement: `Cleanup retires old source authority: a late list or persisted-hydration continuation preserves the cleaned-up or replacement Collection, its durable rows, and its replacement reader.`, + authority: `TrailBase tests/ORACLE.md cleanup law; glossary cleanup and sync run; shared persisted-startup complete eager-result law; SQLite guide Render after SQLite restore.`, + owner: `TrailBase lifecycle oracle, persisted hydration projection`, + limits: `Three string keys and numeric values, one retired run and one replacement, controlled RecordApi/native stream. Network-first remains before its deadline; no remote service, arbitrary I/O failure or concurrent mutation.`, + }, + histories: { + arbitrary: hydrationLifecycleHistoryArb, + runs: 50, + fixedSeeds: [728159], + states: hydrationLifecycleStates, + constructed, + fixed: hydrationLifecycleHistories.map((history) => ({ + name: JSON.stringify(history), + history, + })), + }, + // Row truth remains in the independently specified shared comparison. + // This projection adds only each construction's reach obligation, never a + // production-derived expectation about rows or lifecycle. + model: (history) => ({ required: required.get(history) ?? [] }), + driver: async (history, observe) => { + const actual = await receive(history, `${property}-${sequence++}`) + for (const state of actual.reached) observe(`reach`, state) + observe(`completed`, actual) + }, + check: (history, trace, expected) => + hydrationLifecycleVerdict(history, trace, expected.required), + calibrations: [ + { + name: `late old work erases replacement rows`, + law: `retired source authority`, + corrupt: (trace, history) => + history.resumeAt === `replacement-ready` && history.replacement !== 0 + ? trace.map((entry) => { + if (entry.checkpoint !== `completed`) return entry + const raw = entry.value as HydrationLifecycleObservation + return { + ...entry, + value: { + ...raw, + retiredResume: { ...raw.retiredResume, rows: [] }, + }, + } + }) + : undefined, + }, + ...builds.map(({ state }) => ({ + name: `missing generated builder state ${state}`, + law: `constructed builder reach`, + corrupt: ( + trace: ReadonlyArray, + history: HydrationLifecycleHistory, + ) => + required.get(history)?.includes(state) + ? trace.filter( + (entry) => + !(entry.checkpoint === `reach` && entry.value === state), + ) + : undefined, + })), + ], + timeout: 120000, + }) +} + +/** Product mismatches retain their original law; browser errors are secondary. */ +export function hydrationLifecycleVerdict( + history: HydrationLifecycleHistory, + trace: ReadonlyArray, + required: ReadonlyArray, +): Verdict { + const result = trace.find((entry) => entry.checkpoint === `completed`) + const raw = result?.value as HydrationBrowserObservation | undefined + const browserErrors = raw?.browserErrors ?? [] + const secondary = browserErrors.length + ? `; secondary browser errors: ${browserErrors.join(`; `)}` + : `` + if (result) { + const failures = hydrationLifecycleViolations( + history, + result.value as HydrationBrowserObservation, + ) + if (failures.length) + return violation( + `completed`, + failures.join(`; `) + secondary, + `retired source authority`, + ) + } + const reached = new Set( + trace + .filter((entry) => entry.checkpoint === `reach`) + .map((entry) => entry.value), + ) + const missing = required.filter((state) => !reached.has(state)) + if (missing.length) + return violation( + `reach`, + `generated construction omitted ${missing.join(`, `)}${secondary}`, + `constructed builder reach`, + ) + if (browserErrors.length && result) + return violation( + `browser`, + browserErrors.join(`; `), + `detached-browser-error`, + ) + return result + ? pass + : violation( + `completed`, + `No final observation${secondary}`, + `retired source authority`, + ) +} diff --git a/packages/trailbase-db-collection/tests/lifecycle-judgment-oracle.ts b/packages/trailbase-db-collection/tests/lifecycle-judgment-oracle.ts new file mode 100644 index 0000000000..edfaccb0f8 --- /dev/null +++ b/packages/trailbase-db-collection/tests/lifecycle-judgment-oracle.ts @@ -0,0 +1,237 @@ +/** + * Reference facts and production observations stay in separate trace records. + * The service interpreter computes reference rows from authored commands and + * its independent Map. It never reads adapter rows, errors or verdicts to + * compute them. The sole observational relations are resource release and + * stable error identity: their reference tokens name earlier acquired or + * observed objects, not an expected replacement value. + * + * Tokenizing opaque identities is a capture abstraction. It preserves identity + * while keeping timer handles, streams and Error objects out of serialized + * traces. The framework check applies every relation and retains raw facts at + * the first mismatching cut. No Vitest assertion executes in the driver. + */ +import { isDeepStrictEqual } from 'node:util' +import { declareOracle } from '../../db/tests/oracle-declaration' + +import { pass, violation } from '../../db/tests/oracle-property' +import type { Observation, Verdict } from '../../db/tests/oracle-property' + +type Identity = + | { kind: `symbol`; token: number } + | { kind: `object`; token: number } + | { kind: `primitive`; value: unknown } +type Relation = `equal` | `includes` +export type ReferenceFact = { + checkpoint: string + relation: Relation + expected: unknown +} +type MockCalls = { mock: { calls: ReadonlyArray> } } +type Diagnostic = + | { epoch: number; args: ReadonlyArray } + | { epoch: number; error: unknown } +type Health = { + hasErrors: boolean + failureReason: unknown + error: unknown +} +export type LawRecorder = ReturnType + +export function createLawRecorder( + observe: (checkpoint: string, value: unknown) => void, +) { + let sequence = 0 + const identities = new WeakMap() + const symbols = new Map() + let nextIdentity = 0 + const identity = (value: unknown): Identity => { + if (typeof value === `symbol`) { + let token = symbols.get(value) + if (token === undefined) { + token = nextIdentity++ + symbols.set(value, token) + } + return { kind: `symbol`, token } + } + if ( + (typeof value === `object` && value !== null) || + typeof value === `function` + ) { + const object = value + let token = identities.get(object) + if (token === undefined) { + token = nextIdentity++ + identities.set(object, token) + } + return { kind: `object`, token } + } + return { kind: `primitive`, value } + } + const emit = ( + label: string, + actual: unknown, + expected: unknown, + relation: Relation = `equal`, + ) => { + const checkpoint = `${sequence++}:${label}` + const reference: ReferenceFact = { checkpoint, relation, expected } + observe(`reference`, reference) + observe(checkpoint, actual) + } + return { + equal: (label: string, actual: unknown, expected: unknown) => + emit(label, actual, expected), + same: (label: string, actual: unknown, expected: unknown) => { + // Reserve the reference token first. Its name cannot follow a new wrong + // actual object. Both tokens still refer to this history's identity map. + const reference = identity(expected) + emit(label, identity(actual), reference) + }, + defined: (label: string, actual: unknown) => + emit(label, actual !== undefined, true), + calls: (label: string, mock: MockCalls, count: number) => + emit(label, mock.mock.calls.length, count), + calledWith: (label: string, mock: MockCalls, expected: unknown) => { + const reference = [identity(expected)] + const actual = mock.mock.calls.map((args) => args.map(identity)) + emit(label, actual, reference, `includes`) + }, + errorName: (label: string, actual: unknown, expected: string) => + emit( + label, + typeof actual === `object` && actual !== null && `name` in actual + ? actual.name + : undefined, + expected, + ), + // This law forbids every report and detached rejection. Preserve each + // event, its epoch, argument order and opaque reason identity. A provider + // may reject with any JavaScript value, including DOMException or a class; + // those reasons are not framework observation data and need no value law. + diagnostics: ( + label: string, + actual: ReadonlyArray, + expected: ReadonlyArray, + ) => { + const capture = (entries: ReadonlyArray) => + entries.map((entry) => ({ + epoch: entry.epoch, + ...(`args` in entry + ? { args: entry.args.map(identity) } + : { error: identity(entry.error) }), + })) + const reference = capture(expected) + emit(label, capture(actual), reference) + }, + health: ( + label: string, + health: Health, + requireObject: boolean, + cause?: unknown, + ) => { + const error = health.error + const expected: { + hasErrors: boolean + failureReason: null + errorPresent: boolean + cause?: unknown + } = { + hasErrors: true, + failureReason: null, + errorPresent: true, + } + const actual: { + hasErrors: boolean + failureReason: unknown + errorPresent: boolean + cause?: unknown + } = { + hasErrors: health.hasErrors, + failureReason: health.failureReason, + errorPresent: requireObject + ? typeof error === `object` && error !== null + : error !== undefined && error !== null, + } + if (cause !== undefined) { + expected.cause = cause + actual.cause = + typeof error === `object` && error !== null && `cause` in error + ? error.cause + : undefined + } + emit(label, actual, expected) + }, + reach: (state: string) => observe(`reach`, state), + finish: () => observe(`complete`, true), + } +} + +/** These reference facts are outputs of the independent authored service model, + * carried separately from production observations. The trace is a transport + * for the interleaved interpreter, not authority for the promised answer. */ +export function referenceFacts( + _history: unknown, + trace: ReadonlyArray, +) { + return trace + .filter((entry) => entry.checkpoint === `reference`) + .map((entry) => entry.value as ReferenceFact) +} + +export function checkLawFacts( + _history: unknown, + trace: ReadonlyArray, + references: ReadonlyArray, + law = `TrailBase lifecycle and page convergence`, +): Verdict { + const actual = trace.filter( + (entry) => + entry.checkpoint !== `reference` && + entry.checkpoint !== `reach` && + entry.checkpoint !== `complete`, + ) + for (const [index, reference] of references.entries()) { + const observed = actual[index] + if (!observed) + return violation( + reference.checkpoint, + `production fact was not recorded`, + law, + ) + if (observed.checkpoint !== reference.checkpoint) + return violation( + observed.checkpoint, + `observation order differs from the reference cuts`, + law, + { actual: observed.checkpoint, expected: reference.checkpoint }, + ) + const matches = + reference.relation === `includes` + ? Array.isArray(observed.value) && + observed.value.some((entry: unknown) => + isDeepStrictEqual(entry, reference.expected), + ) + : isDeepStrictEqual(observed.value, reference.expected) + if (!matches) + return violation( + observed.checkpoint, + `production fact differs from the independent reference`, + law, + { actual: observed.value, expected: reference.expected }, + ) + } + if (actual.length !== references.length) + return violation( + actual[references.length]?.checkpoint ?? `complete`, + `production fact has no reference judgment`, + law, + ) + return trace.some( + (entry) => entry.checkpoint === `complete` && entry.value === true, + ) + ? pass + : violation(`complete`, `history did not finish`, law) +} + +declareOracle({ model: [referenceFacts], check: [checkLawFacts] }) diff --git a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts index dcee640bfe..85297990ab 100644 --- a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts +++ b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts @@ -1,39 +1,3 @@ -import { fc, test as fcTest } from '@fast-check/vitest' -import { afterAll, expect, it, vi } from 'vitest' -import { createCollection, createLiveQueryCollection, eq } from '@tanstack/db' -import { - oracleFixedSeed, - oraclePropertyOptions, - oracleRoute, - oracleRouteOf, - oracleRuns, -} from '../../db/tests/oracle-config' -import { createOracleReach } from '../../db/tests/oracle-reach' -import { declareOracle } from '../../db/tests/oracle-declaration' -import { withOracleCleanup } from '../../db/tests/oracle-cleanup' -import { trailBaseCollectionOptions } from '../src/trailbase' -import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' -import { MockRecordApi } from './mock-record-api' -import { observeHydrationLifecycle } from './lifecycle-hydration-driver' -import { - hydrationLifecycleHistories, - hydrationLifecycleHistoryArb, - hydrationLifecycleStates, - hydrationLifecycleViolations, -} from './lifecycle-hydration-oracle' -import { - deferred, - failure, - invalidValue, - observe, - sortedRows, - source, - turn, -} from './lifecycle-support' -import type { Event, ListResponse } from 'trailbase' -import type { RaceRow, Row } from './lifecycle-support' -import type { HydrationLifecycleHistory } from './lifecycle-hydration-oracle' - /** * # Does a TrailBase sync run publish and retire one valid row history? * @@ -54,6 +18,41 @@ import type { HydrationLifecycleHistory } from './lifecycle-hydration-oracle' * See `ORACLE.md` for the full domain, replay commands, mutation evidence, and * the boundary intentionally left to service-backed tests. */ +import { fc } from '@fast-check/vitest' +import { vi } from 'vitest' +import { createCollection, createLiveQueryCollection, eq } from '@tanstack/db' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { createStartupHost } from '../../db-adapter-conformance/tests/persisted-startup-host' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { oracleProperty, pass, violation } from '../../db/tests/oracle-property' +import { trailBaseCollectionOptions } from '../src/trailbase' +import { observeHydrationLifecycle } from './lifecycle-hydration-driver' +import { registerHydrationLifecycle } from './lifecycle-hydration-oracle-suite' +import { + checkLawFacts, + createLawRecorder, + referenceFacts, +} from './lifecycle-judgment-oracle' +import { MockRecordApi } from './mock-record-api' +import { + deferred, + failure, + invalidValue, + observe, + sortedRows, + source, + turn, +} from './lifecycle-support' +import type { LawRecorder } from './lifecycle-judgment-oracle' +import type { + ConstructedHistory, + Observation, +} from '../../db/tests/oracle-property' + + + +import type { Event, ListResponse } from 'trailbase' +import type { RaceRow, Row } from './lifecycle-support' type Change = { operation: `set` | `delete`; id: number; value: number } type Ending = @@ -87,12 +86,11 @@ type Scenario = { * the last rows and reports an error. Cleanup clears rows and retires work. * The model never reads adapter bookkeeping, pending transactions or caches. */ -async function checkLifecycle( +async function observeLifecycle( { mode, syncRuns, metadata }: Scenario, + facts: LawRecorder, providerFault?: `list` | `set` | `buffered`, - metadataReach?: ReturnType, ) { - metadataReach?.beginHistory() const api = new MockRecordApi() let current = source() api.subscribe.mockImplementation(() => current.subscription.promise) @@ -102,7 +100,7 @@ async function checkLifecycle( }) const allSources: Array> = [] const retired: Array<() => void> = [] - const retiredChecks: Array<() => void> = [] + const retiredObservations: Array<() => void> = [] let observationEpoch = -1 const unhandled: Array<{ epoch: number; error: unknown }> = [] const reports: Array<{ epoch: number; args: Array }> = [] @@ -135,7 +133,7 @@ async function checkLifecycle( sync: { ...options.sync, sync: (params) => { - metadataReach?.reach( + facts.reach( metadata === `absent` ? `metadata-absent` : `metadata-present`, ) return options.sync.sync({ @@ -161,26 +159,28 @@ async function checkLifecycle( Array.from(collection.values(), ({ id, value }) => ({ id, value })).sort( (a, b) => a.id - b.id, ) - const assertRows = () => - expect(publicRows(), JSON.stringify({ mode, reached })).toEqual( + const recordRows = () => + facts.equal( + JSON.stringify({ mode, reached }), + publicRows(), [...expected.values()].sort((a, b) => a.id - b.id), ) - const assertNoTimers = () => { + const recordTimers = () => { for (const timer of intervals.mock.results) { if (timer.type === `return`) - expect(cleared).toHaveBeenCalledWith(timer.value) + facts.calledWith('cleared', cleared, timer.value) } } - const assertReports = () => { - expect(reports).toEqual(expectedReports) - expect(unhandled).toEqual([]) + const recordReports = () => { + facts.diagnostics('reports', reports, expectedReports) + facts.diagnostics('unhandled', unhandled, []) } const flushRetired = async () => { retired.splice(0).forEach((settle) => settle()) await turn() - assertRows() - retiredChecks.forEach((check) => check()) - assertReports() + recordRows() + retiredObservations.forEach((recordRetired) => recordRetired()) + recordReports() } const failures: Array = [] try { @@ -194,39 +194,43 @@ async function checkLifecycle( collection.startSyncImmediate() const preload = observe(collection.preload()) await turn() - expect(api.subscribe).toHaveBeenCalledTimes(epoch + 1) - expect(collection.status).toBe(`loading`) - expect(preload.result).toBe(`pending`) - assertRows() + facts.calls('api.subscribe', api.subscribe, epoch + 1) + facts.equal('collection.status', collection.status, `loading`) + facts.equal('preload.result', preload.result, `pending`) + recordRows() if (plan.kind === `cancel-subscribe`) { reached.push(`${epoch}:cancel-subscribe`) if (plan.cancelRejects) active.cancel.mockRejectedValue(new Error(`retired cancel failure`)) await collection.cleanup() - expect(preload.result).toBe(`rejected`) - expect(preload.error).toMatchObject({ name: `AbortError` }) + facts.reach(`cleanup-during-subscribe`) + facts.equal('preload.result', preload.result, `rejected`) + facts.errorName('preload.error', preload.error, `AbortError`) const retiredError = preload.error retired.push(() => plan.late === `resolve` ? active.subscription.resolve(active.stream) : active.subscription.reject(failure), ) - retiredChecks.push(() => { - expect(preload.result).toBe(`rejected`) - expect(preload.error).toBe(retiredError) - expect(active.stream.locked).toBe(false) - expect(active.cancel).toHaveBeenCalledTimes( + retiredObservations.push(() => { + facts.equal('preload.result', preload.result, `rejected`) + facts.same('preload.error', preload.error, retiredError) + facts.equal('active.stream.locked', active.stream.locked, false) + facts.calls( + 'active.cancel', + active.cancel, plan.late === `resolve` ? 1 : 0, ) }) } else if (plan.kind === `reject-subscribe`) { active.subscription.reject(failure) await turn() - expect(preload.result).toBe(`rejected`) - expect(preload.error).toBe(failure) - expect(collection.status).toBe(`error`) - expect(active.listCalls).toBe(0) + facts.reach(`subscribe-rejected`) + facts.equal('preload.result', preload.result, `rejected`) + facts.same('preload.error', preload.error, failure) + facts.equal('collection.status', collection.status, `error`) + facts.equal('active.listCalls', active.listCalls, 0) await collection.cleanup() } else { active.subscription.resolve(active.stream) @@ -236,62 +240,80 @@ async function checkLifecycle( ? preload : observe(Promise.resolve(collection._sync.loadSubset({}))) await turn() - expect(active.listCalls).toBe(1) + facts.equal('active.listCalls', active.listCalls, 1) reached.push(`${epoch}:list-pending`) - expect(load.result).toBe(`pending`) - expect(preload.result).toBe(mode === `eager` ? `pending` : `fulfilled`) + facts.equal('load.result', load.result, `pending`) + facts.equal( + 'preload.result', + preload.result, + mode === `eager` ? `pending` : `fulfilled`, + ) if (plan.kind === `cancel-load`) { reached.push(`${epoch}:cancel-load`) await collection.cleanup() - expect(preload.result).toBe( + facts.reach(`cleanup-during-list`) + facts.equal( + 'preload.result', + preload.result, mode === `eager` ? `rejected` : `fulfilled`, ) if (mode === `eager`) - expect(preload.error).toMatchObject({ name: `AbortError` }) - else expect(load.result).toBe(`pending`) + facts.errorName('preload.error', preload.error, `AbortError`) + else facts.equal('load.result', load.result, `pending`) const retiredError = preload.error retired.push(() => plan.late === `resolve` ? active.list.resolve({ records: [{ id: 99, value: epoch }] }) : active.list.reject(failure), ) - retiredChecks.push(() => { - expect(preload.result).toBe( + retiredObservations.push(() => { + facts.equal( + 'preload.result', + preload.result, mode === `eager` ? `rejected` : `fulfilled`, ) - expect(preload.error).toBe(retiredError) + facts.same('preload.error', preload.error, retiredError) // This is the adapter's direct subset promise, not an unfinished // initial preload: canceled I/O settles as a no-op after delivery. - expect(load.result).toBe( + facts.equal( + 'load.result', + load.result, mode === `eager` ? `rejected` : `fulfilled`, ) - expect(active.stream.locked).toBe(false) - expect(active.cancel).toHaveBeenCalledOnce() + facts.equal('active.stream.locked', active.stream.locked, false) + facts.calls('active.cancel', active.cancel, 1) }) } else if (plan.kind === `reject-load`) { active.list.reject(failure) await turn() - expect(load.result).toBe(`rejected`) - expect(load.error).toBe(failure) - expect(collection.status).toBe(mode === `eager` ? `error` : `ready`) - assertRows() + facts.reach(`list-rejected`) + facts.equal('load.result', load.result, `rejected`) + facts.same('load.error', load.error, failure) + facts.equal( + 'collection.status', + collection.status, + mode === `eager` ? `error` : `ready`, + ) + recordRows() await collection.cleanup() } else { active.list.resolve({ records: [providerRow(0, epoch, `list`)] }) expected.set(0, { id: 0, value: epoch }) await turn() reached.push(`${epoch}:list-delivered`) - expect(load.result).toBe(`fulfilled`) - expect(preload.result).toBe(`fulfilled`) - expect(collection.status).toBe(`ready`) - assertRows() + facts.equal('load.result', load.result, `fulfilled`) + facts.equal('preload.result', preload.result, `fulfilled`) + facts.equal('collection.status', collection.status, `ready`) + recordRows() // Old subscribe/list work finishes only once the replacement owns // a live stream and baseline, not merely after the old cleanup. + if (retired.length > 0) + facts.reach(`replacement-before-retired-delivery`) await flushRetired() - expect(active.stream.locked).toBe(true) - expect(active.cancel).not.toHaveBeenCalled() + facts.equal('active.stream.locked', active.stream.locked, true) + facts.calls('active.cancel', active.cancel, 0) for (const change of plan.changes) { const previous = expected.get(change.id) @@ -310,8 +332,8 @@ async function checkLifecycle( } await turn() reached.push(`${epoch}:${change.operation}:${change.id}`) - assertRows() - expect(collection.status).toBe(`ready`) + recordRows() + facts.equal('collection.status', collection.status, `ready`) } if (plan.ending === `buffered-close`) { @@ -334,40 +356,39 @@ async function checkLifecycle( } await turn() reached.push(`${epoch}:${plan.ending}:immediate=${plan.immediate}`) - assertRows() - expect(active.stream.locked).toBe(false) - assertNoTimers() + facts.reach(`stream-${plan.ending}`) + if (plan.immediate) facts.reach(`stream-immediate-cleanup`) + recordRows() + facts.equal('active.stream.locked', active.stream.locked, false) + recordTimers() const isFailure = plan.ending === `read-error` || plan.ending === `parse-error` if (!plan.immediate && plan.ending !== `cleanup`) { - expect( - collection.health, + facts.health( `${epoch}:${plan.ending} error`, - ).toMatchObject({ - hasErrors: true, - failureReason: null, - error: expect.objectContaining( - isFailure ? { cause: failure } : {}, - ), - }) + collection.health, + true, + isFailure ? failure : undefined, + ) } - assertReports() + recordReports() if (plan.ending === `cleanup` || plan.ending === `parse-error`) - expect(active.cancel).toHaveBeenCalledOnce() + facts.calls('active.cancel', active.cancel, 1) if (!plan.immediate && plan.ending !== `cleanup`) - expect(collection.status).toBe(`ready`) + facts.equal('collection.status', collection.status, `ready`) await collection.cleanup() } } expected.clear() await turn() - assertRows() - expect(collection.status).toBe(`cleaned-up`) - assertReports() - assertNoTimers() + recordRows() + facts.equal('collection.status', collection.status, `cleaned-up`) + recordReports() + recordTimers() } await flushRetired() - for (const active of allSources) expect(active.stream.locked).toBe(false) + for (const active of allSources) + facts.equal('active.stream.locked', active.stream.locked, false) } catch (error) { failures.push(error) } finally { @@ -383,9 +404,10 @@ async function checkLifecycle( } await turn() try { - assertReports() - assertNoTimers() - for (const active of allSources) expect(active.stream.locked).toBe(false) + recordReports() + recordTimers() + for (const active of allSources) + facts.equal('active.stream.locked', active.stream.locked, false) } catch (error) { failures.push(error) } @@ -396,7 +418,7 @@ async function checkLifecycle( } if (failures.length === 1) throw failures[0] if (failures.length > 1) - throw new AggregateError(failures, `Lifecycle and cleanup checks failed`, { + throw new AggregateError(failures, `Lifecycle or cleanup failed`, { cause: failures[0], }) } @@ -407,8 +429,16 @@ const changeArb = fc.record({ value: fc.integer({ min: -20, max: 20 }), }) const syncRunArb: fc.Arbitrary = fc.oneof( + fc.record( + { + kind: fc.constant(`cancel-subscribe` as const), + late: fc.constantFrom(`resolve` as const, `reject` as const), + cancelRejects: fc.boolean(), + }, + { requiredKeys: [`kind`, `late`] }, + ), fc.record({ - kind: fc.constantFrom(`cancel-subscribe` as const, `cancel-load` as const), + kind: fc.constant(`cancel-load` as const), late: fc.constantFrom(`resolve` as const, `reject` as const), }), fc.record({ @@ -427,14 +457,17 @@ const syncRunArb: fc.Arbitrary = fc.oneof( immediate: fc.boolean(), }), ) -const scenarioArb = fc.record({ - metadata: fc.option(fc.constant(`absent` as const), { nil: undefined }), - mode: fc.constantFrom(`eager` as const, `on-demand` as const), - syncRuns: fc.array(syncRunArb, { - minLength: 1, - maxLength: 3, - }), -}) +const scenarioArb = fc.record( + { + metadata: fc.option(fc.constant(`absent` as const), { nil: undefined }), + mode: fc.constantFrom(`eager` as const, `on-demand` as const), + syncRuns: fc.array(syncRunArb, { + minLength: 1, + maxLength: 3, + }), + }, + { requiredKeys: [`mode`, `syncRuns`] }, +) const live = (ending: Ending, immediate = false): SyncRunPlan => ({ kind: `stream`, ending, @@ -471,91 +504,6 @@ const corpus: Array<{ syncRuns: [{ kind }, live(`close`)], })), ] -it.each([`eager`, `on-demand`] as const)( - `preserves lifecycle without a metadata API: %s`, - (mode) => { - oracleRoute(`fixed history`) - return checkLifecycle({ - mode, - metadata: `absent`, - syncRuns: [live(`close`)], - }) - }, -) -it.each( - corpus.flatMap((entry) => - ([`eager`, `on-demand`] as const).map((mode) => ({ ...entry, mode })), - ), -)(`preserves lifecycle laws: $mode / $name`, ({ mode, syncRuns }) => { - oracleRoute(`case`) - return checkLifecycle({ mode, syncRuns }) -}) -it.each([`eager`, `on-demand`] as const)( - `releases a late acquired stream even when native cancellation rejects: %s`, - (mode) => { - oracleRoute(`case`) - return checkLifecycle({ - mode, - syncRuns: [ - { kind: `cancel-subscribe`, late: `resolve`, cancelRejects: true }, - live(`close`), - ], - }) - }, -) -it.each([`list`, `set`, `buffered`] as const)( - `rejects provider-owned row corruption at the %s cut`, - async (cut) => { - oracleRoute(`calibration`) - const scenario: Scenario = { - mode: `eager`, - syncRuns: [live(`buffered-close`)], - } - await checkLifecycle(scenario) - await expect(checkLifecycle(scenario, cut)).rejects.toMatchObject({ - name: `AssertionError`, - message: expect.stringContaining(`deeply equal`), - }) - }, -) -const metadataCampaigns = new Set() -const metadataReaches = { - fixed: createOracleReach(`trailbase.lifecycle.metadata.fixed`, [ - `metadata-absent`, - `metadata-present`, - ]), - random: createOracleReach(`trailbase.lifecycle.metadata.random`, [ - `metadata-absent`, - `metadata-present`, - ]), -} -afterAll(() => { - for (const [campaign, reach] of Object.entries(metadataReaches)) { - if (!metadataCampaigns.has(campaign)) continue - reach.assertReached() - process.stdout.write( - `trailbase.lifecycle.metadata.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, - ) - } -}) -const scenarioFixedSeed = oracleFixedSeed(714_203, { numRuns: oracleRuns(50) }) -fcTest.prop([scenarioArb], scenarioFixedSeed)( - `matches generated lifecycle histories with a fixed seed`, - (scenario) => { - oracleRoute(oracleRouteOf(scenarioFixedSeed)) - metadataCampaigns.add(`fixed`) - return checkLifecycle(scenario, undefined, metadataReaches.fixed) - }, -) -const scenarioCampaign = oraclePropertyOptions(50, `trailbase.lifecycle`) -fcTest.prop([scenarioArb], scenarioCampaign)( - `matches generated lifecycle histories with a random or replayed seed`, - (scenario) => { - oracleRoute(oracleRouteOf(scenarioCampaign)) - metadataCampaigns.add(`random`) - return checkLifecycle(scenario, undefined, metadataReaches.random) - }, -) /** * ## Does an early on-demand load wait for the change listener? @@ -646,7 +594,7 @@ function expectedEarlyDemand(plan: EarlyDemandPlan) { } } -async function checkEarlyDemand(plan: EarlyDemandPlan) { +async function observeEarlyDemand(plan: EarlyDemandPlan, facts: LawRecorder) { const expected = expectedEarlyDemand(plan) const service = new Map(earlyServiceRows.map((row) => [row.id, row])) const listeners: Array> = [] @@ -701,11 +649,11 @@ async function checkEarlyDemand(plan: EarlyDemandPlan) { try { await turn() - expect(state(), `${trace} waiting`).toEqual(expected.waiting) + facts.equal(`${trace} waiting`, state(), expected.waiting) if (plan.serviceChange) { commit({ id: 1, data: `hidden` }) await turn() - expect(state(), `${trace} after service change`).toEqual(expected.waiting) + facts.equal(`${trace} after service change`, state(), expected.waiting) } if (plan.outcome === `listen`) { @@ -727,37 +675,38 @@ async function checkEarlyDemand(plan: EarlyDemandPlan) { await turn() const after = expected.afterOutcome - expect(listCalls, `${trace} list calls`).toBe(after.listCalls) - expect(collection.status, `${trace} status`).toBe(after.status) + facts.equal(`${trace} list calls`, listCalls, after.listCalls) + facts.equal(`${trace} status`, collection.status, after.status) if (query) { - expect(consumer.result, `${trace} query preload`).toBe(after.load) - expect(firstReadyRows, `${trace} first-ready rows`).toEqual( + facts.equal(`${trace} query preload`, consumer.result, after.load) + facts.equal( + `${trace} first-ready rows`, + firstReadyRows, after.firstReadyRows, ) } else { - expect(consumer.result, `${trace} load`).toBe(after.load) + facts.equal(`${trace} load`, consumer.result, after.load) if (after.abortError) - expect(consumer.error, `${trace} abort`).toMatchObject({ - name: `AbortError`, - }) + facts.errorName(`${trace} abort`, consumer.error, `AbortError`) else if (after.load === `rejected`) - expect(consumer.error, `${trace} failure`).toBeDefined() + facts.defined(`${trace} failure`, consumer.error) } if (expected.afterLaterChange && query) { commit({ id: 2, data: `hidden` }) await turn() - expect( - query.toArray.map((row) => row.id).sort((a, b) => a - b), + facts.equal( `${trace} after a later change`, - ).toEqual(expected.afterLaterChange) + query.toArray.map((row) => row.id).sort((a, b) => a - b), + expected.afterLaterChange, + ) } // Late subscription work after abort or cleanup must not fetch. if (plan.outcome === `abort` || plan.outcome === `cleanup`) { subscription.resolve(new TransformStream().readable) await turn() - expect(listCalls, `${trace} late list calls`).toBe(0) + facts.equal(`${trace} late list calls`, listCalls, 0) } } finally { subscription.resolve(new TransformStream().readable) @@ -780,14 +729,6 @@ const earlyDemandPlans: Array = ( ), ) -it.each(earlyDemandPlans)( - `waits for the listener before an early on-demand fetch: $outcome / change=$serviceChange / $consumer`, - (plan) => { - oracleRoute(`case`) - return checkEarlyDemand(plan) - }, -) - /** * ## Does a fetched page keep a newer change that the listener applied? * @@ -860,7 +801,7 @@ function raceChange(change: RacePlan[`change`]): { } } -async function checkPageEventRace(plan: RacePlan) { +async function observePageEventRace(plan: RacePlan, facts: LawRecorder) { const service = new Map(raceServiceRows.map((row) => [row.id, row])) const { events, apply } = raceChange(plan.change) const stream = new TransformStream() @@ -914,21 +855,25 @@ async function checkPageEventRace(plan: RacePlan) { : Promise.resolve(collection._sync.loadSubset({})), ) await turn() - expect(snapshot, `${trace} list called`).toBeDefined() + facts.defined(`${trace} list called`, snapshot) if (plan.timing === `during-fetch`) await commitChange() if (delivery === `during-fetch`) await deliverEvents() page.resolve({ records: snapshot! }) await turn() await turn() - expect(settled.result, `${trace} page settled`).toBe(`fulfilled`) + facts.equal(`${trace} page settled`, settled.result, `fulfilled`) if (delivery !== `after-page`) - expect(rows(), `${trace} rows after the page`).toEqual( + facts.equal( + `${trace} rows after the page`, + rows(), sortedRows(service.values()), ) if (plan.timing === `after-page`) await commitChange() if (delivery === `after-page`) await deliverEvents() - expect(rows(), `${trace} rows after the last event`).toEqual( + facts.equal( + `${trace} rows after the last event`, + rows(), sortedRows(service.values()), ) @@ -936,8 +881,10 @@ async function checkPageEventRace(plan: RacePlan) { const later = { id: 2, data: `later` } service.set(2, later) await send({ Update: later }) - expect(listenerGone, `${trace} listener torn down`).toBe(false) - expect(rows(), `${trace} rows after a later change`).toEqual( + facts.equal(`${trace} listener torn down`, listenerGone, false) + facts.equal( + `${trace} rows after a later change`, + rows(), sortedRows(service.values()), ) } finally { @@ -965,14 +912,6 @@ const racePlans: Array = ([`eager`, `on-demand`] as const).flatMap( ), ) -it.each(racePlans)( - `keeps a newer change over an older page: $mode / $change / $timing / $delivery`, - (plan) => { - oracleRoute(`case`) - return checkPageEventRace(plan) - }, -) - /** * ## Does a page converge while a user mutation holds sync commits? * @@ -1005,7 +944,7 @@ type HeldPlan = { change: `insert` | `update` } -async function checkHeldPage(plan: HeldPlan) { +async function observeHeldPage(plan: HeldPlan, facts: LawRecorder) { const id = plan.change === `insert` ? 3 : 1 const service = new Map(raceServiceRows.map((row) => [row.id, row])) const stream = new TransformStream() @@ -1066,13 +1005,15 @@ async function checkHeldPage(plan: HeldPlan) { const settled = observe(Promise.resolve(collection._sync.loadSubset({}))) await turn() - expect(snapshot, `${trace} list called`).toBeDefined() + facts.defined(`${trace} list called`, snapshot) page.resolve({ records: snapshot! }) await turn() await turn() // Held commits apply only when the mutation settles, and a load waits // until its rows are visible. It must not fail in the meantime. - expect(settled.result, `${trace} load after the page`).toBe( + facts.equal( + `${trace} load after the page`, + settled.result, plan.held ? `pending` : `fulfilled`, ) @@ -1084,12 +1025,16 @@ async function checkHeldPage(plan: HeldPlan) { await turn() await deliver({ Update: confirmed }) await turn() - expect(mutation.result, `${trace} mutation persisted`).toBe(`fulfilled`) + facts.equal(`${trace} mutation persisted`, mutation.result, `fulfilled`) } - expect(settled.result, `${trace} load after everything settles`).toBe( + facts.equal( + `${trace} load after everything settles`, + settled.result, `fulfilled`, ) - expect(rows(), `${trace} rows after everything settles`).toEqual( + facts.equal( + `${trace} rows after everything settles`, + rows(), sortedRows(service.values()), ) } finally { @@ -1105,14 +1050,6 @@ const heldPlans: Array = [true, false].flatMap((held) => ([`insert`, `update`] as const).map((change) => ({ held, change })), ) -it.each(heldPlans)( - `converges while a mutation holds sync commits: held=$held / $change`, - (plan) => { - oracleRoute(`case`) - return checkHeldPage(plan) - }, -) - /** * ## What happens after the change stream ends? * @@ -1140,7 +1077,10 @@ it.each(heldPlans)( * later load. Each compares rows, status, and subscribe calls. After the end, * The Collection's `error` stays, and nothing clears it in this sync run. */ -async function checkStreamEnd(ending: `close` | `read-error`) { +async function observeStreamEnd( + ending: `close` | `read-error`, + facts: LawRecorder, +) { const service = new Map(raceServiceRows.map((row) => [row.id, row])) let controller!: ReadableStreamDefaultController const stream = new ReadableStream({ @@ -1171,51 +1111,38 @@ async function checkStreamEnd(ending: `close` | `read-error`) { await collection._sync.loadSubset({}) await turn() const established = sortedRows(service.values()) - expect(rows(), `${ending} first page`).toEqual(established) + facts.equal(`${ending} first page`, rows(), established) if (ending === `close`) controller.close() else controller.error(failure) await turn() - expect(collection.status, `${ending} status after the end`).toBe(`ready`) - expect(rows(), `${ending} rows after the end`).toEqual(established) - const stopped = { - hasErrors: true, - failureReason: null, - error: expect.anything(), - } - expect(collection.health, `${ending} error after the end`).toMatchObject( - stopped, - ) + facts.equal(`${ending} status after the end`, collection.status, `ready`) + facts.equal(`${ending} rows after the end`, rows(), established) + facts.health(`${ending} error after the end`, collection.health, false) service.set(1, { id: 1, data: `v2` }) await turn() - expect(rows(), `${ending} a later change is not delivered`).toEqual( + facts.equal( + `${ending} a later change is not delivered`, + rows(), established, ) await collection._sync.loadSubset({ limit: 10 } as never) await turn() - expect(api.subscribe, `${ending} subscribe calls`).toHaveBeenCalledTimes(1) - expect(rows(), `${ending} a later load lists current rows`).toEqual( + facts.calls(`${ending} subscribe calls`, api.subscribe, 1) + facts.equal( + `${ending} a later load lists current rows`, + rows(), sortedRows(service.values()), ) - expect(collection.health, `${ending} error after the load`).toMatchObject( - stopped, - ) + facts.health(`${ending} error after the load`, collection.health, false) } finally { await collection.cleanup() reported.mockRestore() } } -it.each([`close`, `read-error`] as const)( - `keeps rows, reports an error, and does not resubscribe after the stream ends: %s`, - (ending) => { - oracleRoute(`case`) - return checkStreamEnd(ending) - }, -) - /** * ## Do several pages, several changes, and overlapping loads still converge? * @@ -1271,6 +1198,26 @@ type PageHistory = { const pageServiceSize = 258 +// Normalize only grammar constraints. This is also the old arbitrary's mapping; +// the inverse below accepts each normalized history as its own legal preimage. +function normalizePage(h: PageHistory): PageHistory { + let changes = 0 + return { + ...h, + steps: h.steps.slice(0, 10).filter((step) => { + if (step.kind !== `change`) return true + if ( + changes >= 4 || + (h.paging === `offset` && + (step.operation === `delete` || step.id === 300)) + ) + return false + changes++ + return true + }), + } +} + const pageHistoryArb: fc.Arbitrary = fc .record({ paging: fc.constantFrom(`cursor`, `offset`), @@ -1288,32 +1235,28 @@ const pageHistoryArb: fc.Arbitrary = fc operation: fc.constantFrom<`set` | `delete`>(`set`, `delete`), delivery: fc.constantFrom<`now` | `later`>(`now`, `later`), }), - fc.constant({ kind: `deliver` as const }), - fc.constant({ kind: `start-second` as const }), + fc.record({ kind: fc.constant(`deliver` as const) }), + fc.record({ kind: fc.constant(`start-second` as const) }), ), { maxLength: 10 }, ), }) - .map((history) => { - let changes = 0 - return { - ...history, - steps: history.steps.filter((step) => { - if (step.kind !== `change`) return true - if (changes >= 4) return false - // Offset paging: only updates of rows the service already holds. - if ( - history.paging === `offset` && - (step.operation === `delete` || step.id === 300) - ) - return false - changes++ - return true - }), - } + .map(normalizePage, (value) => { + const history = value as PageHistory + if ( + history.steps.filter((step) => step.kind === `change`).length > 4 || + (history.paging === `offset` && + history.steps.some( + (step) => + step.kind === `change` && + (step.operation === `delete` || step.id === 300), + )) + ) + throw new Error(`Not a normalized page history`) + return history }) -async function checkPageHistory(history: PageHistory) { +async function observePageHistory(history: PageHistory, facts: LawRecorder) { const service = new Map() for (let id = 1; id <= pageServiceSize; id++) service.set(id, { id, data: `v0` }) @@ -1322,6 +1265,7 @@ async function checkPageHistory(history: PageHistory) { // Each load's list calls, oldest first, with the answer captured at the call. type PendingPage = { answer: ListResponse; resolve: () => void } const pending: [Array, Array] = [[], []] + const releasedPages: [number, number] = [0, 0] const returnedKeys = new Set() const changedKeys = new Set() // Events in transit, in commit order. @@ -1338,6 +1282,7 @@ async function checkPageHistory(history: PageHistory) { const api = new MockRecordApi() api.subscribe.mockImplementation(() => Promise.resolve(stream.readable)) api.list.mockImplementation((opts) => { + facts.reach(`${history.paging}-paging`) const rows = [...service.values()].sort((a, b) => a.id - b.id) const limit = opts?.pagination?.limit ?? 256 const after = @@ -1381,16 +1326,22 @@ async function checkPageHistory(history: PageHistory) { loadOfNextCall = load loads[load] = observe(Promise.resolve(collection._sync.loadSubset(options))) await turn() + if ( + load === 1 && + loads[0]?.result === `pending` && + loads[1]?.result === `pending` + ) + facts.reach(`overlapping-loads`) } const value = (id: number) => { const row = collection.get(id) return row ? { id: row.id, data: row.data } : undefined } const trace = JSON.stringify(history) - const checkChangedKeys = (label: string) => { + const recordChangedKeys = (label: string) => { for (const id of changedKeys) { if (inTransit.some((delivery) => delivery.id === id)) continue - expect(value(id), `${trace} ${label}: key ${id}`).toEqual(service.get(id)) + facts.equal(`${trace} ${label}: key ${id}`, value(id), service.get(id)) } } const deliverNext = async () => { @@ -1401,9 +1352,12 @@ async function checkPageHistory(history: PageHistory) { const next = pending[load].shift() if (!next) return loadOfNextCall = load + const hasInTransitEvent = inTransit.length > 0 next.resolve() + releasedPages[load]++ await turn() await turn() + if (hasInTransitEvent) facts.reach(`event-in-transit-across-page`) } try { @@ -1427,11 +1381,20 @@ async function checkPageHistory(history: PageHistory) { let event: Event | undefined if (step.operation === `delete`) { if (existing) { + if ( + step.id === 1 && + releasedPages[0] === 0 && + pending[0].length > 0 + ) + facts.reach(`delete-first-page-in-flight`) service.delete(step.id) event = { Delete: existing } } } else { const row = { id: step.id, data: `v${++version}` } + if (!existing && step.id === 300) facts.reach(`insert-new-key`) + if (existing && releasedPages[0] > 0 && pending[0].length > 0) + facts.reach(`update-between-pages`) service.set(step.id, row) event = existing ? { Update: row } : { Insert: row } } @@ -1443,7 +1406,7 @@ async function checkPageHistory(history: PageHistory) { else await send(event) } } - checkChangedKeys(`after step ${index}`) + recordChangedKeys(`after step ${index}`) } if (history.loads === 2 && !secondStarted) await startLoad(1) @@ -1454,23 +1417,22 @@ async function checkPageHistory(history: PageHistory) { } while (inTransit.length > 0) await deliverNext() for (const load of loads) - expect(load.result, `${trace} load settled`).toBe(`fulfilled`) + facts.equal(`${trace} load settled`, load.result, `fulfilled`) const reached = new Set([...returnedKeys, ...changedKeys]) for (const id of reached) - expect(value(id), `${trace} drained: key ${id}`).toEqual(service.get(id)) + facts.equal(`${trace} drained: key ${id}`, value(id), service.get(id)) // No other row exists: every visible key came from a page or an event. - expect( - [...collection.keys()].sort((a, b) => Number(a) - Number(b)), + facts.equal( `${trace} drained: key set`, - ).toEqual( + [...collection.keys()].sort((a, b) => Number(a) - Number(b)), [...reached].filter((id) => service.has(id)).sort((a, b) => a - b), ) // The listener is still alive: a later change to an unrelated row arrives. const later = { id: 2, data: `later` } service.set(2, later) await send({ Update: later }) - expect(listenerGone, `${trace} listener torn down`).toBe(false) - expect(value(2), `${trace} a later change arrives`).toEqual(later) + facts.equal(`${trace} listener torn down`, listenerGone, false) + facts.equal(`${trace} a later change arrives`, value(2), later) } finally { for (const queue of pending) for (const next of queue.splice(0)) next.resolve() @@ -1553,29 +1515,6 @@ const pageFixedHistories: Array<[string, PageHistory]> = [ ], ] -it.each(pageFixedHistories)(`converges: %s`, (_name, history) => { - oracleRoute(`fixed history`) - return checkPageHistory(history) -}) -const pageHistoryFixedSeed = oracleFixedSeed(205_917, { - numRuns: oracleRuns(40), -}) -fcTest.prop([pageHistoryArb], pageHistoryFixedSeed)( - `converges across generated page histories with a fixed seed`, - (history) => { - oracleRoute(oracleRouteOf(pageHistoryFixedSeed)) - return checkPageHistory(history) - }, -) -const pageHistoryCampaign = oraclePropertyOptions(40, `trailbase.page-event`) -fcTest.prop([pageHistoryArb], pageHistoryCampaign)( - `converges across generated page histories with a random or replayed seed`, - (history) => { - oracleRoute(oracleRouteOf(pageHistoryCampaign)) - return checkPageHistory(history) - }, -) - /** * ## Does a fresh sync run load a row an earlier run's event touched? * @@ -1585,7 +1524,7 @@ fcTest.prop([pageHistoryArb], pageHistoryCampaign)( * clock makes the old run's event and the new run's list call share a * millisecond, the case a wall-clock fence cannot order. */ -async function checkFreshRunAfterEvent() { +async function observeFreshRunAfterEvent(facts: LawRecorder) { const service = new Map(raceServiceRows.map((row) => [row.id, row])) const streams: Array> = [] const api = new MockRecordApi() @@ -1617,17 +1556,18 @@ async function checkFreshRunAfterEvent() { service.set(1, changed) await streams[0]!.write({ Update: changed }) await turn() - expect(collection.get(1)?.data, `first run`).toBe(`v2`) + facts.equal(`first run`, collection.get(1)?.data, `v2`) await collection.cleanup() await collection.preload() await turn() await collection._sync.loadSubset({}) await turn() - expect( - sortedRows(collection.toArray), + facts.equal( `fresh run after an event in the same millisecond`, - ).toEqual(sortedRows(service.values())) + sortedRows(collection.toArray), + sortedRows(service.values()), + ) } finally { now.mockRestore() await collection.cleanup() @@ -1636,100 +1576,526 @@ async function checkFreshRunAfterEvent() { } } -it(`loads a row in a fresh run that an earlier run's event touched`, () => { - oracleRoute(`case`) - return checkFreshRunAfterEvent() -}) - -/** - * The persisted receiver crosses the same cleanup law with an old completed - * list waiting at hydrateBaseline, or a late rejected list. Fixed histories - * and both generated campaigns use the same judgment on controlled and real - * SQLite hosts. The browser receiver imports the model and driver directly. +/** The authored service and independent Map interpreters above are the trusted + * reference boundary. Their facts are transported separately from production + * facts; only checkLawFacts judges them, at every original observation cut. + * This conversion preserves all 36 lifecycle and five page fixed histories. */ -async function checkHydrationLifecycle( - history: HydrationLifecycleHistory, - host: `controlled` | `node-sqlite`, - reach?: ReturnType, +const lifecycleStates = [ + `metadata-absent`, + `metadata-present`, + `cleanup-during-subscribe`, + `cleanup-during-list`, + `subscribe-rejected`, + `list-rejected`, + `replacement-before-retired-delivery`, + `stream-close`, + `stream-buffered-close`, + `stream-read-error`, + `stream-parse-error`, + `stream-cleanup`, + `stream-immediate-cleanup`, +] as const + +type LifecycleState = (typeof lifecycleStates)[number] +const lifecycleRequired = new WeakMap>() +const streamFrom = ( + h: Scenario, + ending: Ending, + immediate = false, +): SyncRunPlan => ({ + kind: `stream`, + ending, + immediate, + changes: + h.syncRuns + .find((run) => run.kind === `stream`) + ?.changes.map((change) => ({ ...change })) ?? [], +}) +const lifecycleBuilds: Array<{ + state: LifecycleState + build: (h: Scenario) => Scenario +}> = [ + ...([`absent`, undefined] as const).map((metadata) => ({ + state: + metadata === `absent` + ? (`metadata-absent` as const) + : (`metadata-present` as const), + build: (h: Scenario): Scenario => ({ ...h, metadata }), + })), + ...( + [ + `cancel-subscribe`, + `cancel-load`, + `reject-subscribe`, + `reject-load`, + ] as const + ).map((kind) => ({ + state: ( + { + 'cancel-subscribe': `cleanup-during-subscribe`, + 'cancel-load': `cleanup-during-list`, + 'reject-subscribe': `subscribe-rejected`, + 'reject-load': `list-rejected`, + } as const + )[kind], + build: (h: Scenario): Scenario => ({ + ...h, + syncRuns: [ + kind === `cancel-subscribe` || kind === `cancel-load` + ? { kind, late: h.syncRuns.length % 2 === 0 ? `reject` : `resolve` } + : { kind }, + ...h.syncRuns.slice(0, 2), + ], + }), + })), + { + state: `replacement-before-retired-delivery`, + build: (h) => ({ + ...h, + syncRuns: [ + { + kind: + h.syncRuns.length % 2 === 0 ? `cancel-subscribe` : `cancel-load`, + late: h.metadata === `absent` ? `reject` : `resolve`, + }, + streamFrom(h, `close`), + ...h.syncRuns.slice(0, 1), + ], + }), + }, + ...( + [`close`, `buffered-close`, `read-error`, `parse-error`, `cleanup`] as const + ).map((ending) => ({ + state: `stream-${ending}` as const, + build: (h: Scenario): Scenario => ({ + ...h, + syncRuns: [streamFrom(h, ending), ...h.syncRuns.slice(0, 2)], + }), + })), + { + state: `stream-immediate-cleanup`, + build: (h) => ({ + ...h, + syncRuns: [streamFrom(h, `close`, true), ...h.syncRuns.slice(0, 2)], + }), + }, +] +function constructedWithRequired( + builds: ReadonlyArray<{ state: string; build: (h: T) => T }>, + required: WeakMap>, +): Array> { + return builds.map(({ state, build }) => ({ + name: `generated ${state}`, + enters: [state], + build: (generated) => { + const history = { ...build(generated) } + required.set(history, [state]) + return history + }, + })) +} +function checkConstructed( + h: T, + trace: ReadonlyArray, + required: WeakMap>, ) { - reach?.beginHistory() - const storage = createStartupHost(host) - return withOracleCleanup(async () => { - const actual = await observeHydrationLifecycle( - storage.persistence, - history, - `trailbase-hydration-${host}`, - ) - for (const state of actual.reached) reach?.reach(state) - expect(hydrationLifecycleViolations(history, actual)).toEqual([]) - return actual - }, [() => storage.close()]) + const reached = new Set( + trace + .filter((entry) => entry.checkpoint === `reach`) + .map((entry) => entry.value), + ) + const missing = required.get(h)?.filter((state) => !reached.has(state)) ?? [] + return missing.length + ? violation( + `complete`, + `generated construction omitted ${missing.join(`, `)}`, + `constructed builder reach`, + ) + : pass } -for (const host of [`controlled`, `node-sqlite`] as const) { - it.each(hydrationLifecycleHistories)( - `retires persisted hydration through ${host}: %j`, - async (history) => { - oracleRoute(`fixed history`) - await checkHydrationLifecycle(history, host) +function missingStateControls( + builds: ReadonlyArray<{ state: string }>, + required: WeakMap>, +) { + return builds.map(({ state }) => ({ + name: `missing generated builder state ${state}`, + law: `constructed builder reach`, + corrupt: (trace: ReadonlyArray, h: T) => + required.get(h)?.includes(state) + ? trace.filter( + (entry) => !(entry.checkpoint === `reach` && entry.value === state), + ) + : undefined, + })) +} +const lifecycleFixed = [ + ...corpus.flatMap(({ name, syncRuns }) => + ([`eager`, `on-demand`] as const).map((mode) => ({ + name: `${mode} / ${name}`, + history: { mode, syncRuns }, + })), + ), + ...([`eager`, `on-demand`] as const).map((mode) => ({ + name: `${mode} / absent metadata`, + history: { mode, metadata: `absent` as const, syncRuns: [live(`close`)] }, + })), + ...([`eager`, `on-demand`] as const).map((mode) => ({ + name: `${mode} / rejecting late native cancellation`, + history: { + mode, + syncRuns: [ + { + kind: `cancel-subscribe` as const, + late: `resolve` as const, + cancelRejects: true, + }, + live(`close`), + ], }, + })), +] +function corruptFirstFact( + trace: ReadonlyArray, + accepts: (entry: Observation) => boolean, + replacement: (value: unknown) => unknown, +) { + const at = trace.findIndex( + (entry) => entry.checkpoint !== `reference` && accepts(entry), ) - for (const campaign of [`fixed`, `random`] as const) { - const property = `trailbase.lifecycle.persisted-hydration.${host}` - const settings = - campaign === `fixed` - ? oracleFixedSeed(728_159, { numRuns: oracleRuns(50) }) - : oraclePropertyOptions(50, property) - const reach = createOracleReach( - `${property}.${campaign}`, - hydrationLifecycleStates, - ) - let ran = false - let obsoleteBegins = 0 - afterAll(() => { - if (!ran) return - reach.assertReached() - process.stdout.write( - `${property}.${campaign} ${JSON.stringify(Object.fromEntries(reach.counts()))} obsoleteBegins=${obsoleteBegins}\n`, + return at < 0 + ? undefined + : trace.map((entry, index) => + index === at ? { ...entry, value: replacement(entry.value) } : entry, ) - }) - fcTest.prop([hydrationLifecycleHistoryArb], settings)( - `retires generated persisted hydration through ${host} with ${campaign} seed`, - async (history) => { - oracleRoute(oracleRouteOf(settings)) - ran = true - const actual = await checkHydrationLifecycle(history, host, reach) - obsoleteBegins += actual.obsoleteBegins +} +const lifecycleLaw = `TrailBase lifecycle` +const lifecycleDriver = async ( + h: Scenario, + record: (checkpoint: string, value: unknown) => void, + fault?: `list` | `set` | `buffered`, +) => { + const facts = createLawRecorder(record) + await observeLifecycle(h, facts, fault) + facts.finish() +} +oracleProperty({ + id: `trailbase.lifecycle`, + law: { + statement: `A sync run publishes the independent service row history, settles startup and subset loads at their promised cuts, and cleanup retires its late work and resources.`, + authority: `TrailBase tests/ORACLE.md; Collection lifecycle readiness, cleanup, health, and exact reported-error contracts.`, + owner: `TrailBase lifecycle oracle`, + limits: `Controlled RecordApi/native streams, one to three sync runs, zero to eight scalar changes per stream. No deployed server, network retries, persistence or arbitrary scheduler.`, + }, + histories: { + arbitrary: scenarioArb, + runs: 50, + fixedSeeds: [714203], + states: lifecycleStates, + constructed: constructedWithRequired(lifecycleBuilds, lifecycleRequired), + fixed: lifecycleFixed, + }, + model: referenceFacts, + driver: lifecycleDriver, + check: (h, trace, expected) => { + const facts = checkLawFacts(h, trace, expected, lifecycleLaw) + return facts.kind === `violation` + ? facts + : checkConstructed(h, trace, lifecycleRequired) + }, + calibrations: [ + ...([`list`, `set`, `buffered`] as const).map((cut) => ({ + name: `provider-owned ${cut} row corruption`, + law: lifecycleLaw, + driver: ( + h: Scenario, + record: (checkpoint: string, value: unknown) => void, + ) => lifecycleDriver(h, record, cut), + })), + { + name: `Collection status changes before its promised cut`, + law: lifecycleLaw, + corrupt: (trace) => + corruptFirstFact( + trace, + (entry) => entry.checkpoint.endsWith(`:collection.status`), + () => `wrong-status`, + ), + }, + { + name: `a later observation replaces the exact startup error`, + law: lifecycleLaw, + corrupt: (trace) => + corruptFirstFact( + trace, + (entry) => + entry.checkpoint.endsWith(`.error`) && + typeof entry.value === `object` && + entry.value !== null && + `token` in entry.value, + () => ({ kind: `object`, token: -1 }), + ), + }, + { + name: `a retired reader remains owned`, + law: lifecycleLaw, + corrupt: (trace) => + corruptFirstFact( + trace, + (entry) => entry.checkpoint.endsWith(`:active.stream.locked`), + (value) => !value, + ), + }, + { + name: `cleanup leaves an acquired timer uncleared`, + law: lifecycleLaw, + corrupt: (trace) => + corruptFirstFact( + trace, + (entry) => entry.checkpoint.endsWith(`:cleared`), + () => [], + ), + }, + ...([`DOMException`, `class instance`, `symbol`] as const).map((kind) => ({ + name: `detached ${kind} retains a lifecycle violation`, + law: lifecycleLaw, + driver: async ( + h: Scenario, + record: (checkpoint: string, value: unknown) => void, + ) => { + const facts = createLawRecorder(record) + await observeLifecycle(h, facts) + class ProviderFailure {} + const error = + kind === `DOMException` + ? new DOMException(`retired work`, `AbortError`) + : kind === `symbol` + ? Symbol(`provider failure`) + : new ProviderFailure() + facts.diagnostics( + `detached diagnostic capture`, + [{ epoch: 0, error }], + [], + ) + facts.finish() }, - ) - } - it(`rejects old-work row corruption through ${host}`, async () => { - oracleRoute(`calibration`) - const history = hydrationLifecycleHistories.find( - (h) => h.resumeAt === `replacement-ready` && h.replacement !== 0, - )! - const actual = await checkHydrationLifecycle(history, host) - expect( - hydrationLifecycleViolations(history, { - ...actual, - retiredResume: { ...actual.retiredResume, rows: [] }, + })), + ...missingStateControls(lifecycleBuilds, lifecycleRequired), + ], +}) + +const pageStates = [ + `cursor-paging`, + `offset-paging`, + `overlapping-loads`, + `event-in-transit-across-page`, + `update-between-pages`, + `delete-first-page-in-flight`, + `insert-new-key`, +] as const +const pageRequired = new WeakMap>() +const pageBuilds: Array<{ + state: (typeof pageStates)[number] + build: (h: PageHistory) => PageHistory +}> = [ + ...([`cursor`, `offset`] as const).map((paging) => ({ + state: `${paging}-paging` as const, + build: (h: PageHistory) => normalizePage({ ...h, paging }), + })), + { + state: `overlapping-loads`, + build: (h) => ({ ...h, loads: 2, secondAtOnce: true }), + }, + { + state: `event-in-transit-across-page`, + build: (h) => + normalizePage({ + ...h, + steps: [ + { + kind: `change`, + id: h.steps.length % 2 === 0 ? 1 : 258, + operation: `set`, + delivery: `later`, + }, + { kind: `release`, load: 0 }, + { kind: `deliver` }, + ...h.steps.slice(0, 7), + ], }), - ).not.toEqual([]) - }) -} + }, + { + state: `update-between-pages`, + build: (h) => + normalizePage({ + ...h, + steps: [ + { kind: `release`, load: 0 }, + { kind: `change`, id: 258, operation: `set`, delivery: `now` }, + { kind: `release`, load: 0 }, + ...h.steps.slice(0, 7), + ], + }), + }, + { + state: `delete-first-page-in-flight`, + build: (h) => + normalizePage({ + ...h, + paging: `cursor`, + steps: [ + { + kind: `change`, + id: 1, + operation: `delete`, + delivery: h.steps.length % 2 === 0 ? `now` : `later`, + }, + ...h.steps.slice(0, 9), + ], + }), + }, + { + state: `insert-new-key`, + build: (h) => + normalizePage({ + ...h, + paging: `cursor`, + steps: [ + { + kind: `change`, + id: 300, + operation: `set`, + delivery: h.steps.length % 2 === 0 ? `now` : `later`, + }, + ...h.steps.slice(0, 9), + ], + }), + }, +] +const pageLaw = `TrailBase page/event convergence` +oracleProperty({ + id: `trailbase.page-event`, + law: { + statement: `Pages and change events converge to the independent service map at every cut after the row's pending events arrive; cleanup fences an earlier sync run.`, + authority: `TrailBase tests/ORACLE.md page/event and fresh-run laws; subscribe-before-list boundary.`, + owner: `TrailBase lifecycle page/event oracle`, + limits: `258 service rows, cursor or offset paging, one or two loads, up to ten steps and four scalar changes. Offset paging updates existing rows only; no live deployed server.`, + }, + histories: { + arbitrary: pageHistoryArb, + runs: 40, + fixedSeeds: [205917], + states: pageStates, + constructed: constructedWithRequired(pageBuilds, pageRequired), + fixed: pageFixedHistories.map(([name, history]) => ({ name, history })), + }, + model: (h: PageHistory, trace) => referenceFacts(h, trace), + driver: async (h, record) => { + const facts = createLawRecorder(record) + await observePageHistory(h, facts) + facts.finish() + }, + check: (h, trace, expected) => { + const facts = checkLawFacts(h, trace, expected, pageLaw) + return facts.kind === `violation` + ? facts + : checkConstructed(h, trace, pageRequired) + }, + calibrations: [ + { + name: `row result omitted`, + law: pageLaw, + corrupt: (trace) => { + const at = trace.findIndex((entry) => + entry.checkpoint.includes(`drained: key set`), + ) + return at < 0 + ? undefined + : trace.map((entry, index) => + index === at ? { ...entry, value: [] } : entry, + ) + }, + }, + ...missingStateControls(pageBuilds, pageRequired), + ], +}) + +type BoundaryHistory = + | { kind: `early-demand`; plan: EarlyDemandPlan } + | { kind: `page-race`; plan: RacePlan } + | { kind: `held-page`; plan: HeldPlan } + | { kind: `stream-end`; ending: `close` | `read-error` } + | { kind: `fresh-run` } +const boundaryHistories: Array = [ + ...earlyDemandPlans.map((plan): BoundaryHistory => ({ + kind: `early-demand`, + plan, + })), + ...racePlans.map((plan): BoundaryHistory => ({ kind: `page-race`, plan })), + ...heldPlans.map((plan): BoundaryHistory => ({ kind: `held-page`, plan })), + ...([`close`, `read-error`] as const).map((ending): BoundaryHistory => ({ + kind: `stream-end`, + ending, + })), + { kind: `fresh-run` }, +] +oracleProperty({ + id: `trailbase.lifecycle.boundaries`, + law: { + statement: `Early demand waits for the listener; page/event overlap and held optimistic work converge to the service; stream end retains rows and health; a new sync run ignores old event fences.`, + authority: `TrailBase tests/ORACLE.md early demand, page/event, held-page, stream-end and fresh-run laws.`, + owner: `TrailBase lifecycle oracle`, + limits: `All 51 cases of the original finite grammars: 16 early demand, 28 one-page race, four held-page, two stream-end, one fresh-run. Controlled provider boundary only.`, + }, + histories: boundaryHistories, + model: (h: BoundaryHistory, trace) => referenceFacts(h, trace), + driver: async (h, record) => { + const facts = createLawRecorder(record) + if (h.kind === `early-demand`) await observeEarlyDemand(h.plan, facts) + else if (h.kind === `page-race`) await observePageEventRace(h.plan, facts) + else if (h.kind === `held-page`) await observeHeldPage(h.plan, facts) + else if (h.kind === `stream-end`) await observeStreamEnd(h.ending, facts) + else await observeFreshRunAfterEvent(facts) + facts.finish() + }, + check: (h, trace, expected) => + checkLawFacts(h, trace, expected, `TrailBase lifecycle boundary`), + calibrations: [ + { + name: `early fetch becomes visible before the listener`, + law: `TrailBase lifecycle boundary`, + corrupt: (trace) => + trace.some((entry) => entry.checkpoint.includes(`waiting`)) + ? trace.map((entry) => + entry.checkpoint.includes(`waiting`) && + entry.checkpoint !== `reference` + ? { ...entry, value: { listCalls: 1, settled: false } } + : entry, + ) + : undefined, + }, + ], +}) -// What this oracle owns: the gap report judges these entries, and their -// reference trees within the package, by this file's tests. +// The reusable judgments own this production boundary. The framework handles +// campaign registration; this declaration retains gap-report discovery. declareOracle({ owns: [`src/trailbase.ts#trailBaseCollectionOptions`], - check: [ - checkLifecycle, - checkEarlyDemand, - checkPageEventRace, - checkHeldPage, - checkStreamEnd, - checkPageHistory, - checkFreshRunAfterEvent, - checkHydrationLifecycle, - ], - model: [expectedEarlyDemand], + check: [checkLawFacts], + model: [referenceFacts, expectedEarlyDemand], + grammar: [scenarioArb, pageHistoryArb], }) + +/** The same cleanup law receives controlled and real Node SQLite hydration. + * The shared hydration suite owns its model, generated grammar and cuts. */ +for (const host of [`controlled`, `node-sqlite`] as const) + registerHydrationLifecycle( + `trailbase.lifecycle.persisted-hydration.${host}`, + async (history, id) => { + const storage = createStartupHost(host) + return withOracleCleanup( + () => observeHydrationLifecycle(storage.persistence, history, id), + [() => storage.close()], + ) + }, + ) diff --git a/scripts/bench/persisted-startup-2026-11-10.csv b/scripts/bench/persisted-startup-2026-11-10.csv index 4d07b6e9ea..0ff7d4d163 100644 --- a/scripts/bench/persisted-startup-2026-11-10.csv +++ b/scripts/bench/persisted-startup-2026-11-10.csv @@ -175,3 +175,67 @@ final-large,12,prior-selection,natural,1,False,powersync,50000,True,1000,256,def final-large,13,current,natural,1,False,powersync,50000,True,1000,256,default,0,0,56.62091700000019,193.9167499999994,1148.2332079999996,376.027333,1148.073625,50000,1,1,1,50000,1,49236288,299601144,299600240,662847488,751009792,751009792,6902117,6901949,688160,733408 final-large,14,current,natural,2,False,powersync,50000,True,1000,256,default,0,0,52.12916700000005,180.27387499999986,1001.3796249999996,369.4567500000003,1001.2657499999996,50000,1,1,1,50000,1,49241256,298967856,298966952,657424384,745357312,745357312,6902117,6901949,682864,727888 final-large,15,prior-selection,natural,2,False,powersync,50000,True,1000,256,default,0,0,55.567167000000154,185.98524999999972,853.1055420000002,192.6263339999996,853.0315,50000,51,0,50,50000,0,49238400,218935624,224459640,657539072,706691072,706691072,6901957,6901789,683072,690128 +repaired-final,0,prior-selection,natural,-1,True,rxdb,10000,True,1000,256,default,0,0,32.868791999999985,88.25529199999994,167.503292,63.501125,167.40912500000002,10000,1,0,1,10000,0,50211056,151035808,151034952,272285696,349372416,349356032,6920484,6920524,274704,341184 +repaired-final,1,current,natural,-1,True,rxdb,10000,True,1000,256,default,0,0,42.50866700000006,102.57120800000007,185.681917,76.54008299999998,185.578667,10000,1,0,1,10000,0,50222488,153692248,153633016,274186240,356253696,356253696,6929231,6929271,276640,347904 +repaired-final,2,current,natural,0,False,rxdb,10000,True,1000,256,default,0,0,32.81949999999995,89.035166,172.64404099999996,64.26304099999999,172.533791,10000,1,0,1,10000,0,50220720,91651520,124219336,273137664,356990976,356990976,6929231,6929271,274512,348624 +repaired-final,3,prior-selection,natural,0,False,rxdb,10000,True,1000,256,default,0,0,34.30008399999997,91.66024999999996,173.47125,66.23379199999994,173.36650000000003,10000,1,0,1,10000,0,50211848,150989608,150988752,272400384,350191616,350175232,6920484,6920524,274608,341984 +repaired-final,4,prior-selection,natural,1,False,rxdb,10000,True,1000,256,default,0,0,32.28520800000001,88.41066699999993,165.374167,63.51595800000001,165.28574999999995,10000,1,0,1,10000,0,50209360,150714232,150802928,272662528,355483648,355483648,6920484,6920524,275360,347152 +repaired-final,5,current,natural,1,False,rxdb,10000,True,1000,256,default,0,0,34.52158300000002,92.26820800000002,176.68683300000004,66.48979200000002,176.58887499999997,10000,1,0,1,10000,0,50226728,154116880,153793792,275529728,356204544,356204544,6929231,6929271,276384,347856 +repaired-final,6,current,natural,2,False,rxdb,10000,True,1000,256,default,0,0,31.738749999999982,88.23199999999997,171.64908300000002,62.467083,171.5475,10000,1,0,1,10000,0,50225224,154126216,153629272,274137088,351420416,351420416,6929231,6929271,275616,343184 +repaired-final,7,prior-selection,natural,2,False,rxdb,10000,True,1000,256,default,0,0,31.67750000000001,86.35825000000006,165.465125,61.29862499999996,165.34891599999997,10000,1,0,1,10000,0,50209232,150501160,150777272,273006592,350519296,350519296,6920484,6920524,274896,342304 +repaired-final,8,prior-selection,fallback,-1,True,rxdb,10000,True,1000,256,network-first,0,350,10.26724999999999,27.914333,30.195666000000017,385.682833,490.31708299999997,10000,1,0,1,10000,0,50212960,146563992,146796616,273874944,354959360,354959360,6920484,6920524,276336,346640 +repaired-final,9,current,fallback,-1,True,rxdb,10000,True,1000,256,network-first,0,350,9.278208000000006,24.35749999999996,26.430207999999993,406.216542,510.956375,10000,1,0,1,10000,0,50227560,150033008,150032248,273285120,350683136,350683136,6929231,6929271,275184,342464 +repaired-final,10,current,fallback,0,False,rxdb,10000,True,1000,256,network-first,0,350,9.47425000000004,25.159583999999995,27.249000000000024,414.3025420000001,519.2719589999999,10000,1,0,1,10000,0,50231352,149879688,149878928,272236544,351584256,351584256,6929231,6929271,274816,343344 +repaired-final,11,prior-selection,fallback,0,False,rxdb,10000,True,1000,256,network-first,0,350,9.600583000000029,24.90195799999998,26.96358300000003,408.46466699999996,510.612292,10000,1,0,1,10000,0,50217768,146136656,146445512,271482880,355631104,355631104,6920484,6920524,274080,347296 +repaired-final,12,prior-selection,fallback,1,False,rxdb,10000,True,1000,256,network-first,0,350,10.03158400000001,26.632416999999975,28.949749999999995,391.49533399999996,495.56766700000003,10000,1,0,1,10000,0,50211808,146723704,146747880,272728064,354975744,354975744,6920484,6920524,275216,346656 +repaired-final,13,current,fallback,1,False,rxdb,10000,True,1000,256,network-first,0,350,9.763958000000002,25.23758300000003,27.519416999999976,393.71170800000004,504.802875,10000,1,0,1,10000,0,50225752,150075624,150074864,271974400,351518720,351518720,6929231,6929271,274096,343280 +repaired-final,14,current,fallback,2,False,rxdb,10000,True,1000,256,network-first,0,350,10.314291000000026,27.759541000000013,29.984290999999985,391.771208,497.88983299999995,10000,1,0,1,10000,0,50225680,149918040,149917280,272728064,356319232,356319232,6929231,6929271,275152,347968 +repaired-final,15,prior-selection,fallback,2,False,rxdb,10000,True,1000,256,network-first,0,350,10.065458999999976,26.842084,29.15491700000001,392.527167,494.40912499999996,10000,1,0,1,10000,0,50215936,146584680,146895944,273186816,354451456,354451456,6920484,6920524,274752,346144 +repaired-final,16,prior-selection,natural,-1,True,rxdb,50000,True,1000,256,default,0,0,69.11275,178.95549999999992,1072.004958,577.3557500000001,1071.895458,50000,1,0,1,50000,0,98667640,352295216,352138880,537640960,706625536,706625536,6920692,6920524,578528,690064 +repaired-final,17,current,natural,-1,True,rxdb,50000,True,1000,256,default,0,0,73.34466599999996,192.74508300000002,1121.0087500000002,616.330125,1120.897541,50000,1,0,1,50000,0,98648464,370792752,370437000,538607616,714145792,714145792,6929439,6929271,579328,697408 +repaired-final,18,current,natural,0,False,rxdb,50000,True,1000,256,default,0,0,72.85183300000006,179.42449999999997,1118.8473329999997,600.35225,1118.7309169999999,50000,1,0,1,50000,0,98585256,370512792,370439632,537968640,713179136,713179136,6929439,6929271,577984,696464 +repaired-final,19,prior-selection,natural,0,False,rxdb,50000,True,1000,256,default,0,0,71.03429199999994,189.07829200000003,1114.9639590000002,619.9457919999999,1114.8555420000002,50000,1,0,1,50000,0,98666480,357689008,357390624,536068096,707411968,707411968,6920692,6920524,577104,690832 +repaired-final,20,prior-selection,natural,1,False,rxdb,50000,True,1000,256,default,0,0,71.8213750000001,198.035209,1122.3186249999999,623.889542,1122.2100419999997,50000,1,0,1,50000,0,98603272,357740104,357448304,539197440,712523776,712523776,6920692,6920524,579616,695824 +repaired-final,21,current,natural,1,False,rxdb,50000,True,1000,256,default,0,0,71.256125,184.5613330000001,1092.466417,594.5388330000001,1092.35875,50000,1,0,1,50000,0,98648400,366827152,366660880,540491776,713392128,713392128,6929439,6929271,581424,696672 +repaired-final,22,current,natural,2,False,rxdb,50000,True,1000,256,default,0,0,66.36987500000009,169.39212500000008,1045.4537090000001,568.347334,1045.3450420000001,50000,1,0,1,50000,0,98609584,368592912,368560064,539688960,717242368,717242368,6929439,6929271,580096,700432 +repaired-final,23,prior-selection,natural,2,False,rxdb,50000,True,1000,256,default,0,0,66.0145,168.5663750000001,1048.233625,551.845041,1048.127541,50000,1,0,1,50000,0,98580896,351669336,351499288,538918912,710803456,710803456,6920692,6920524,579344,694144 +repaired-final,24,prior-selection,fallback,-1,True,rxdb,50000,True,1000,256,network-first,0,350,42.930665999999974,108.74583299999995,115.71362499999998,831.8889999999999,1334.1121660000001,50000,1,0,1,50000,0,98636040,359253872,359792096,537247744,708247552,708247552,6920692,6920524,577856,691648 +repaired-final,25,current,fallback,-1,True,rxdb,50000,True,1000,256,network-first,0,350,43.446957999999995,114.61212499999988,121.72470799999996,825.8062499999999,1306.098416,50000,1,0,1,50000,0,98679424,369742960,369742200,539770880,716947456,716947456,6929439,6929271,580736,700144 +repaired-final,26,current,fallback,0,False,rxdb,50000,True,1000,256,network-first,0,350,41.797250000000076,114.31033400000001,121.67729200000008,808.4929999999999,1275.6054589999999,50000,1,0,1,50000,0,98587384,369065352,369064592,539492352,715816960,715816960,6929439,6929271,579632,699040 +repaired-final,27,prior-selection,fallback,0,False,rxdb,50000,True,1000,256,network-first,0,350,42.99350000000004,108.96383300000002,115.88999999999999,837.404333,1324.3568750000002,50000,1,0,1,50000,0,98605520,358343304,358342544,539885568,708034560,708034560,6920692,6920524,580704,691440 +repaired-final,28,prior-selection,fallback,1,False,rxdb,50000,True,1000,256,network-first,0,350,42.78520900000001,111.21299999999997,118.82633399999997,811.792292,1297.875959,50000,1,0,1,50000,0,98600656,359214616,359213856,538886144,712622080,712622080,6920692,6920524,579168,695920 +repaired-final,29,current,fallback,1,False,rxdb,50000,True,1000,256,network-first,0,350,43.44500000000005,108.44804199999999,115.65033300000005,817.9628329999999,1301.925542,50000,1,0,1,50000,0,98619352,370024656,370023896,537559040,711311360,711311360,6929439,6929271,577872,694640 +repaired-final,30,current,fallback,2,False,rxdb,50000,True,1000,256,network-first,0,350,45.70945800000004,117.63687499999992,125.08408299999996,834.481458,1386.359833,50000,1,0,1,50000,0,98683416,370230784,370230024,538132480,712376320,712376320,6929439,6929271,579152,695680 +repaired-final,31,prior-selection,fallback,2,False,rxdb,50000,True,1000,256,network-first,0,350,43.814167,117.52658400000018,125.00800000000004,838.9228750000002,1355.249417,50000,1,0,1,50000,0,98791808,361372400,361371640,538804224,714686464,714686464,6920692,6920524,579072,697936 +repaired-final,32,prior-selection,natural,-1,True,powersync,10000,True,1000,256,default,0,0,9.76908299999991,24.20875000000001,140.7087919999999,27.075666999999953,140.6523749999999,10000,11,0,10,10000,0,39873200,67051320,122690960,384155648,431177728,431177728,6902025,6901857,375152,423248 +repaired-final,33,current,natural,-1,True,powersync,10000,True,1000,256,default,0,0,9.865957999999978,24.606042000000002,135.393417,27.464042000000063,135.334208,10000,11,0,10,10000,0,39884840,70290016,123270360,383483904,431046656,431046656,6910772,6910604,374704,423152 +repaired-final,34,current,natural,0,False,powersync,10000,True,1000,256,default,0,0,10.126458999999954,25.016917000000035,137.781292,27.868124999999964,137.720459,10000,11,0,10,10000,0,39876104,70526344,123493352,384401408,431423488,431423488,6910772,6910604,375392,423584 +repaired-final,35,prior-selection,natural,0,False,powersync,10000,True,1000,256,default,0,0,10.453957999999943,29.059833000000026,159.204791,32.46858299999997,159.11191599999995,10000,11,0,10,10000,0,39871616,67494928,123157288,385007616,434454528,434454528,6902025,6901857,375984,426432 +repaired-final,36,prior-selection,natural,1,False,powersync,10000,True,1000,256,default,0,0,9.795875000000024,24.383207999999968,134.94324999999992,27.287500000000023,134.88529200000005,10000,11,0,10,10000,0,39865792,67102464,122853416,382926848,431341568,431341568,6902025,6901857,373952,423392 +repaired-final,37,current,natural,1,False,powersync,10000,True,1000,256,default,0,0,9.95562499999994,24.314916999999923,135.58837499999993,27.165874999999915,135.53258399999993,10000,11,0,10,10000,0,39879400,70213168,123339992,386613248,435601408,435601408,6910772,6910604,377728,427664 +repaired-final,38,current,natural,2,False,powersync,10000,True,1000,256,default,0,0,9.960708999999952,24.442000000000007,135.554167,27.32245899999998,135.49745899999994,10000,11,0,10,10000,0,39883880,70279176,123336264,385204224,434847744,434847744,6910772,6910604,376176,426976 +repaired-final,39,prior-selection,natural,2,False,powersync,10000,True,1000,256,default,0,0,10.009999999999991,24.30054199999995,133.19841699999995,27.072667000000024,133.14645799999994,10000,11,0,10,10000,0,39865840,66811216,122547832,382828544,430833664,430833664,6902025,6901857,373856,422928 +repaired-final,40,prior-selection,fallback,-1,True,powersync,10000,True,1000,256,network-first,0,350,9.611708000000021,24.049082999999996,25.982666999999992,380.488792,483.168417,10000,1,1,1,10000,1,39866224,112946176,112945272,383598592,430145536,430145536,6902025,6901857,374848,420064 +repaired-final,41,current,fallback,-1,True,powersync,10000,True,1000,256,network-first,0,350,9.757916000000023,25.18566599999997,25.965374999999995,356.1683330000001,478.00925000000007,10000,11,0,10,10000,0,39881424,70820848,123880368,386121728,431472640,431472640,6910772,6910604,377264,423728 +repaired-final,42,current,fallback,0,False,powersync,10000,True,1000,256,network-first,0,350,12.864791000000082,33.74304100000006,34.56087500000001,356.0307500000001,467.115458,10000,11,0,10,10000,0,39887160,70464272,123581800,386039808,435191808,435191808,6910772,6910604,376992,427312 +repaired-final,43,prior-selection,fallback,0,False,powersync,10000,True,1000,256,network-first,0,350,9.712750000000028,24.159874999999943,24.757292000000007,387.443,492.2980419999999,10000,1,1,1,10000,1,39875296,112663504,112662600,381829120,425181184,425181184,6902025,6901857,372880,415216 +repaired-final,44,prior-selection,fallback,1,False,powersync,10000,True,1000,256,network-first,0,350,9.670417000000043,24.306542000000036,24.934750000000008,386.7711250000001,511.85637499999996,10000,1,1,1,10000,1,39869464,112952520,112951616,385613824,429735936,429735936,6902025,6901857,376768,419664 +repaired-final,45,current,fallback,1,False,powersync,10000,True,1000,256,network-first,0,350,10.298457999999982,25.99000000000001,26.597624999999994,359.8800000000001,473.3362910000001,10000,11,0,10,10000,0,39886672,70692088,123755632,385007616,434716672,434716672,6910772,6910604,376192,426832 +repaired-final,46,current,fallback,2,False,powersync,10000,True,1000,256,network-first,0,350,13.555791999999997,33.44504200000006,34.50516700000003,359.3310419999999,487.97958299999993,10000,11,0,10,10000,0,39872424,69257448,122024928,386924544,434651136,434651136,6910772,6910604,378064,426432 +repaired-final,47,prior-selection,fallback,2,False,powersync,10000,True,1000,256,network-first,0,350,11.429916000000048,32.250999999999976,33.67887500000006,378.8932910000001,493.56658300000004,10000,1,1,1,10000,1,39871016,113530672,113529768,387760128,431407104,431407104,6902025,6901857,378848,421296 +repaired-final,48,prior-selection,natural,-1,True,powersync,50000,True,1000,256,default,0,0,46.0827919999997,139.81812499999978,735.8787919999995,147.536834,735.7945840000002,50000,51,0,50,50000,0,49241576,218954056,223972072,658243584,705495040,705478656,6902025,6901857,683664,688960 +repaired-final,49,current,natural,-1,True,powersync,50000,True,1000,256,default,0,0,46.50704200000018,128.14470900000015,663.8213340000002,133.0696250000001,663.7507500000002,50000,51,0,50,50000,0,49249760,172577224,226822312,656637952,704675840,707280896,6910772,6910604,682224,690736 +repaired-final,50,current,natural,0,False,powersync,50000,True,1000,256,default,0,0,44.47237499999983,118.91883400000006,619.2493340000001,123.36349999999993,619.1844999999998,50000,51,0,50,50000,0,49254536,173029024,227293088,656424960,704380928,706953216,6910772,6910604,682080,690448 +repaired-final,51,prior-selection,natural,0,False,powersync,50000,True,1000,256,default,0,0,49.35270799999989,174.51820799999996,785.9484159999997,183.19091599999956,785.879958,50000,51,0,50,50000,0,49241888,219689960,224701752,653852672,702988288,702988288,6902025,6901857,680112,686512 +repaired-final,52,prior-selection,natural,1,False,powersync,50000,True,1000,256,default,0,0,49.629249999999956,172.19145799999978,772.5474579999996,179.86662499999966,772.4738329999996,50000,51,0,50,50000,0,49241144,218696088,224294360,651640832,699957248,699957248,6902025,6901857,676944,683552 +repaired-final,53,current,natural,1,False,powersync,50000,True,1000,256,default,0,0,43.493083999999726,117.01833399999987,679.9052919999999,121.92812499999991,679.8359999999998,50000,51,0,50,50000,0,49254752,172758688,226757248,656556032,705904640,708509696,6910772,6910604,682208,691936 +repaired-final,54,current,natural,2,False,powersync,50000,True,1000,256,default,0,0,44.81254099999978,120.89908299999979,679.246208,125.49387500000012,679.181583,50000,51,0,50,50000,0,49254064,172792936,227029832,653918208,700841984,703430656,6910772,6910604,679632,686992 +repaired-final,55,prior-selection,natural,2,False,powersync,50000,True,1000,256,default,0,0,44.1667080000002,130.9908330000003,658.0135,136.39191600000004,657.9451250000002,50000,51,0,50,50000,0,49238720,218415704,224183616,652148736,698630144,698630144,6902025,6901857,677808,682256 +repaired-final,56,prior-selection,fallback,-1,True,powersync,50000,True,1000,256,network-first,0,350,42.11604199999965,124.27437499999996,126.8037089999998,481.35354199999983,977.8281669999997,50000,1,1,1,50000,1,49237872,284045424,297218320,652902400,679182336,738050048,6902025,6901857,685168,721120 +repaired-final,57,current,fallback,-1,True,powersync,50000,True,1000,256,network-first,0,350,44.57699999999977,128.51866699999982,132.1092920000001,359.365417,898.2489169999999,50000,51,0,50,50000,0,49257800,172661464,226916000,656506880,705069056,707641344,6910772,6910604,681968,691120 +repaired-final,58,current,fallback,0,False,powersync,50000,True,1000,256,network-first,0,350,43.477499999999964,127.23929099999987,130.17358299999978,359.8292080000001,914.6600410000001,50000,51,0,50,50000,0,49256984,165914144,222712152,655147008,696827904,699351040,6910772,6910604,680640,682960 +repaired-final,59,prior-selection,fallback,0,False,powersync,50000,True,1000,256,network-first,0,350,42.86295800000016,124.09704099999999,127.25004100000024,480.9310830000004,1055.5470410000003,50000,1,1,1,50000,1,49240720,246382536,258243232,653852672,672956416,734789632,6902025,6901857,679472,726256 +repaired-final,60,prior-selection,fallback,1,False,powersync,50000,True,1000,256,network-first,0,350,43.157042000000274,127.93012499999986,131.89183300000013,494.1883750000002,958.3735420000003,50000,1,1,1,50000,1,49241072,246277280,257857824,656949248,677314560,738312192,6902025,6901857,682400,730512 +repaired-final,61,current,fallback,1,False,powersync,50000,True,1000,256,network-first,0,350,42.21483399999988,115.84712499999978,118.37649999999985,361.3002919999999,880.9809999999998,50000,51,0,50,50000,0,49252896,172916464,226946424,657162240,705462272,708067328,6910772,6910604,683328,691504 +repaired-final,62,current,fallback,2,False,powersync,50000,True,1000,256,network-first,0,350,43.550416999999925,132.6935840000001,137.0905419999999,358.64029200000004,884.1494590000002,50000,51,0,50,50000,0,49253816,172596152,226873528,653623296,700153856,702742528,6910772,6910604,679248,686304 +repaired-final,63,prior-selection,fallback,2,False,powersync,50000,True,1000,256,network-first,0,350,42.10658300000023,116.05754200000001,119.03137500000003,506.2604580000002,1008.0146250000003,50000,1,1,1,50000,1,49243432,246668424,257899568,655179776,674119680,735756288,6902025,6901857,680672,727376 diff --git a/scripts/bench/persisted-startup.mjs b/scripts/bench/persisted-startup.mjs index 0ba02169f4..4558f569ef 100644 --- a/scripts/bench/persisted-startup.mjs +++ b/scripts/bench/persisted-startup.mjs @@ -1,6 +1,8 @@ -// Process-isolated, paired measurements of the PR's persisted snapshot choice. +// Process-isolated, paired measurements of persisted eager startup. The prior +// selection uses immutable adapter sources, so it stays valid after a repair. // Example: node scripts/bench/persisted-startup.mjs --rows=1000,10000 --repetitions=5 --out=.review-2367/startup-perf/results.json import { spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' import { readFile, mkdir, writeFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import path from 'node:path' @@ -26,6 +28,46 @@ const out = path.resolve( const directory = path.dirname(out) await mkdir(directory, { recursive: true }) const variants = ['prior-selection', 'current'] +const baselineRef = + args.baselineRef ?? '0e86c1fdae85520aff142f141f71ccf46a8ee8bd' +const sourceControls = [ + { + file: 'packages/rxdb-db-collection/src/rxdb.ts', + sha256: '2dca19cff8bb602b2aa59f46c7635035e49d0f1cda0a2222e1917f6d2eb8aa62', + }, + { + file: 'packages/powersync-db-collection/src/powersync.ts', + sha256: 'd945eb99bff416fc9c7ba041a802fa842072e5cf03a71d02dd2ed214610df408', + }, +] +const sha256 = (value) => createHash('sha256').update(value).digest('hex') +const sourceInputs = new Map() +for (const control of sourceControls) { + const original = spawnSync( + 'git', + ['show', `${baselineRef}:${control.file}`], + { + cwd: root, + encoding: 'utf8', + }, + ) + if (original.status !== 0) throw new Error(original.stderr) + if (sha256(original.stdout) !== control.sha256) + throw new Error(`Immutable baseline source guard failed: ${control.file}`) + const prior = original.stdout.replace( + /persistence !== null \|\|\s*!collection\._state\.acceptedSyncedEntries\(\)\.next\(\)\.done/, + '!collection._state.acceptedSyncedEntries().next().done', + ) + if (prior === original.stdout) + throw new Error(`Snapshot source control did not match ${control.file}`) + sourceInputs.set(path.join(root, control.file), { + file: control.file, + baselineSourceSha256: control.sha256, + 'prior-selection': prior, + current: await readFile(path.join(root, control.file), 'utf8'), + }) +} +const bundleHashes = {} const workspace = { '@tanstack/db': path.join(root, 'packages/db/src/index.ts'), '@tanstack/db-ivm': path.join(root, 'packages/db-ivm/src/index.ts'), @@ -65,34 +107,30 @@ for (const variant of variants) { if (resolved.errors.length) return { errors: resolved.errors } return { path: resolved.path, external: true } }) - if (variant === 'prior-selection') - bundle.onLoad( - { - filter: - /packages\/(rxdb|powersync)-db-collection\/src\/(rxdb|powersync)\.ts$/, - }, - async ({ path: source }) => { - const current = await readFile(source, 'utf8') - const original = current.replace( - /persistence !== null \|\|\s*!collection\._state\.acceptedSyncedEntries\(\)\.next\(\)\.done/, - '!collection._state.acceptedSyncedEntries().next().done', - ) - if (original === current) - throw new Error( - `Snapshot source control did not match ${source}`, - ) - changed++ - return { contents: original, loader: 'ts' } - }, - ) + bundle.onLoad( + { + filter: + /packages\/(rxdb|powersync)-db-collection\/src\/(rxdb|powersync)\.ts$/, + }, + async ({ path: source }) => { + const input = sourceInputs.get(source) + if (!input) + throw new Error(`Uncontrolled adapter source ${source}`) + changed++ + return { contents: input[variant], loader: 'ts' } + }, + ) }, }, ], }) - if (variant === 'prior-selection' && changed !== 2) + if (changed !== 2) throw new Error( `Expected two controlled snapshot selections, got ${changed}`, ) + bundleHashes[variant] = sha256( + await readFile(path.join(directory, `${variant}.mjs`)), + ) } const sourceHead = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, @@ -101,7 +139,15 @@ const sourceHead = spawnSync('git', ['rev-parse', 'HEAD'], { const report = { sourceHead, baseline: - 'Current source/dependencies with only the persisted-capability OR operand removed from RxDB/PowerSync eager snapshot selection, matching origin/main selection. Not a full origin/main checkout.', + 'Pinned adapter source with only the persisted-capability OR operand removed from RxDB/PowerSync eager snapshot selection, matching prior ordinary startup selection. Both variants share current core/dependencies. Not a full origin/main checkout; omitted-row correctness is judged separately by the oracle.', + baselineRef, + sourceInputs: [...sourceInputs.values()].map((input) => ({ + file: input.file, + baselineSourceSha256: input.baselineSourceSha256, + priorSelectionSha256: sha256(input['prior-selection']), + currentSha256: sha256(input.current), + })), + bundleHashes, runtime: { node: process.version, platform: process.platform, @@ -122,6 +168,7 @@ const report = { }, observations: [], } +await writeFile(out, `${JSON.stringify(report, null, 2)}\n`) let pair = 0 for (const adapter of adapters) for (const count of rows) diff --git a/scripts/oracle-lint-settings.mjs b/scripts/oracle-lint-settings.mjs index 54639222d8..eb35711bf0 100644 --- a/scripts/oracle-lint-settings.mjs +++ b/scripts/oracle-lint-settings.mjs @@ -635,10 +635,6 @@ export const oracleLintSettings = { file: `packages/offline-transactions/tests/transaction-settlement-oracle.property.test.ts`, reason: `manual reach with fixed histories; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, }, - { - file: `packages/powersync-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts`, - reason: `manual reach; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, - }, { file: `packages/query-db-collection/tests/cursor-pagination-oracle.integration.test.ts`, reason: `raw campaign; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, @@ -663,10 +659,6 @@ export const oracleLintSettings = { file: `packages/query-db-collection/tests/optimistic-ordered-refetch-oracle.test.ts`, reason: `manual reach; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, }, - { - file: `packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts`, - reason: `manual reach; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, - }, { file: `packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts`, reason: `manual reach with fixed histories; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, @@ -723,10 +715,6 @@ export const oracleLintSettings = { file: `packages/react-db/tests/window-render-oracle.test.tsx`, reason: `manual reach with fixed histories; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, }, - { - file: `packages/rxdb-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts`, - reason: `manual reach; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, - }, { file: `packages/solid-db/tests/row-materialization-oracle.test.tsx`, reason: `manual reach; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, @@ -755,14 +743,6 @@ export const oracleLintSettings = { file: `packages/svelte-db/tests/ssr-snapshot-handoff-oracle.svelte.test.ts`, reason: `manual reach with fixed histories; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, }, - { - file: `packages/trailbase-db-collection/tests/eager-live-source-conformance-oracle.property.test.ts`, - reason: `manual reach; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, - }, - { - file: `packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts`, - reason: `raw campaign; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, - }, { file: `scripts/lint-scope-oracle.property.test.js`, reason: `manual reach; preserve its laws, declared states, and run budgets during oracleProperty conversion.`, From 5bb88d40ec355ca972ea241311ce3b8b1510bca8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:39:37 +0000 Subject: [PATCH 19/24] ci: apply automated fixes --- .../tests/lifecycle-oracle.property.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts index 85297990ab..f1b8875fd9 100644 --- a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts +++ b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts @@ -49,8 +49,6 @@ import type { Observation, } from '../../db/tests/oracle-property' - - import type { Event, ListResponse } from 'trailbase' import type { RaceRow, Row } from './lifecycle-support' From 735dc8a1962fa00df22e9c66cea01a658650a16a Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 17:11:10 -0700 Subject: [PATCH 20/24] test: complete RxDB startup fixture and record scoped gap allowance --- .../2026-11-10-pr-2394-final-validation.md | 26 +++++++++++++++++++ .../rxdb-db-collection/tests/rxdb.test.ts | 1 + 2 files changed, 27 insertions(+) diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md index 8cf6a865d2..758c84ae19 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md @@ -131,3 +131,29 @@ pass (119 tests). The integrated RxDB eager/startup/lifecycle run passes 281 tests across three files. PowerSync and pushed-head integration checks remain in progress at this record's checkpoint. Earlier finite coverage and the explicit mutation-decision limits above remain applicable. + +## Maintainer allowance and CI fixture correction + +Decision `PR2394-GAP-ALLOWANCE-2026-11-11`: Kyle Mathews explicitly answered +"Grant this PR-specific allowance" to the merge-specific question identifying +the 27 exact surviving mutations in this record and the linked inventories. +The scope is six Query survivors, TrailBase M151, eighteen automatic +PowerSync/RxDB survivors and two authored live-iterator alternatives. Their +exact source anchors, replacements and hashes remain in the linked reports and +audit JSON. The allowance permits this PR to merge after green CI and the +other readiness checks. It does not claim those mutations were killed or +that the oracle gaps were repaired. + +The same answer accepts the supplementary sweep's partial discovery limits: +twelve inconclusive outcomes and fifty-eight unrun or interrupted operators. +Those outcomes retain their original classifications. They are not oracle +kills or demonstrated product defects. + +The later local PowerSync integration run completed successfully: four files, +387 tests. The actual Chromium TrailBase receiver also passed 119 tests. CI +then exposed one ordinary RxDB unit-test fixture that omitted the Collection's +required `health` object. Its isolated local run reproduced the exact +`health.reloading` TypeError. Adding `health: { reloading: false }` describes +the fixture's existing first-load state without changing its assertions or +production behavior. The full `tests/rxdb.test.ts` file then passed all nineteen +tests. Final pushed-head CI remains pending at this checkpoint. diff --git a/packages/rxdb-db-collection/tests/rxdb.test.ts b/packages/rxdb-db-collection/tests/rxdb.test.ts index 53314f4542..d8ed7841b1 100644 --- a/packages/rxdb-db-collection/tests/rxdb.test.ts +++ b/packages/rxdb-db-collection/tests/rxdb.test.ts @@ -525,6 +525,7 @@ describe(`RxDB Integration`, () => { // A first load: the Collection holds no synced rows. collection: { status: `loading`, + health: { reloading: false }, _state: { acceptedSyncedEntries: () => new Map().entries() }, }, } as never) From fb3f13949837c67236fc8ab7d21e76e93e91e37b Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 18:15:13 -0700 Subject: [PATCH 21/24] Fix startup receiving fixture cleanup and collection state --- .../2026-11-10-pr-2394-final-validation.md | 38 +++++++++++++++++++ .../e2e/eager-gap-conformance.opfs.ts | 10 ++++- ...ed-startup-conformance-oracle.opfs.spec.ts | 31 +++++++++++++-- .../e2e/persisted-startup-conformance.opfs.ts | 15 ++++++-- .../tests/on-demand-sync.test.ts | 6 ++- 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md index 758c84ae19..af0f86d42a 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md @@ -157,3 +157,41 @@ required `health` object. Its isolated local run reproduced the exact the fixture's existing first-load state without changing its assertions or production behavior. The full `tests/rxdb.test.ts` file then passed all nineteen tests. Final pushed-head CI remains pending at this checkpoint. + +## Browser cleanup and PowerSync fixture corrections + +CI on `735dc8a1962fa00df22e9c66cea01a658650a16a` exposed two further test +fixture defects. The ordinary PowerSync on-demand fixture also omitted +`collection.health`. Its isolated unchanged run failed with the same +`health.reloading` TypeError as CI. Supplying `health: { reloading: false }` +states its existing first-load condition. It changes no production behavior, +model, history, comparison or budget. +The isolated rerun passed its selected test with no type errors. + +The OPFS startup receiver attempted file removal immediately after awaited +worker close. Chromium can release the native handle in a later task. Both +startup cleanup sites and the eager-gap cleanup site now use the existing +bounded `removeClosedArtifacts` helper. Only `NoModificationAllowedError` +retries; other errors and permanent busy handles still fail. Primary comparison +failures remain visible when cleanup also fails. + +The actual Chromium receiver now injects one busy-file rejection after the +first database closes, on each of two runs using the same browser profile. +With the original cleanup restored, that history failed with +`NoModificationAllowedError: fixture handle release pending`. With the retry +restored, both OPFS receiver specs passed in 3.9 minutes. Each profile run +recorded the injected rejection and one retry, then completed the unchanged +shared row, ownership, durability and artifact comparisons. The existing +cleanup calibration also passed all three selected checks: transient busy +success, permanent busy failure and preservation of a primary mismatch. + +Commands from the repository root: + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/browser-db-sqlite-persistence exec playwright test --config playwright.opfs.config.ts persisted-startup-conformance-oracle.opfs.spec.ts eager-gap-conformance-oracle.opfs.spec.ts +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/powersync-db-collection exec vitest run tests/on-demand-sync.test.ts --maxWorkers=1 --coverage.enabled=false -t 'does not publish a provisional or rejected subset' +``` + +The full PowerSync package run remains in progress. Its earlier CI timeouts +are not treated as collateral failures without a passing rerun. Final-head CI +and integration with later main remain separate gates. diff --git a/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.ts index 1cc90a4708..1b9dac814a 100644 --- a/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.ts +++ b/packages/browser-db-sqlite-persistence/e2e/eager-gap-conformance.opfs.ts @@ -47,6 +47,7 @@ import { removeStartupOPFSArtifacts, startupOPFSArtifacts, } from './persisted-startup-opfs-cleanup' +import { removeClosedArtifacts } from './query-rejected-write-cutoff-cleanup' import type { PersistedCollectionPersistence } from '../../db-sqlite-persistence-core/src' export type EagerGapResult = { @@ -76,7 +77,14 @@ async function database( const db = await openBrowserWASQLiteOPFSDatabase({ databaseName: name }) return withOracleCleanup( () => body(createBrowserWASQLitePersistence({ database: db })), - [() => db.close?.(), () => removeStartupOPFSArtifacts(name)], + [ + () => db.close?.(), + () => + removeClosedArtifacts( + () => removeStartupOPFSArtifacts(name), + (cut, value) => console.info(`eager-gap.opfs ${cut}`, value), + ), + ], ) } async function removeReloadFiles() { diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts index b7c69a96b1..5232b5b289 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance-oracle.opfs.spec.ts @@ -41,11 +41,34 @@ test(`complete source results replace persisted rows across OPFS startup`, async page, }) => { test.setTimeout(600_000) - page.on(`console`, (message) => - console.info(`browser ${message.type()}: ${message.text()}`), - ) + let busyResponses = 0 + page.on(`console`, (message) => { + console.info(`browser ${message.type()}: ${message.text()}`) + if (message.text() === `startup fixture busy response injected`) + busyResponses++ + }) const errors: Array = [] page.on(`pageerror`, (error) => errors.push(error.message)) + // Native worker termination can release an OPFS handle after close returns. + // Inject that exact cleanup condition once, after the first database closes. + // Both profile runs must still complete the same shared row/durability checks. + await page.addInitScript(() => { + const removeEntry = FileSystemDirectoryHandle.prototype.removeEntry + let firstFileRemovals = 0 + FileSystemDirectoryHandle.prototype.removeEntry = function (name, options) { + if (name === `ps-0.sqlite` && ++firstFileRemovals === 2) { + FileSystemDirectoryHandle.prototype.removeEntry = removeEntry + console.info(`startup fixture busy response injected`) + return Promise.reject( + new DOMException( + `fixture handle release pending`, + `NoModificationAllowedError`, + ), + ) + } + return removeEntry.call(this, name, options) + } + }) await page.goto(`/e2e/persisted-startup-conformance.opfs.html`) await page.waitForFunction( () => window.__persistedStartupConformance !== undefined, @@ -53,6 +76,7 @@ test(`complete source results replace persisted rows across OPFS startup`, async const result = await page.evaluate( () => window.__persistedStartupConformance!, ) + expect(busyResponses).toBe(1) expect(errors).toEqual([]) checkStartupConformance(result) // Reuse the same origin and file names. Successful cleanup must keep the @@ -64,4 +88,5 @@ test(`complete source results replace persisted rows across OPFS startup`, async checkStartupConformance( await page.evaluate(() => window.__persistedStartupConformance!), ) + expect(busyResponses).toBe(2) }) diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts index 786c72042f..047a0f31a8 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-startup-conformance.opfs.ts @@ -38,6 +38,7 @@ import { removeStartupOPFSArtifacts, startupOPFSArtifacts, } from './persisted-startup-opfs-cleanup' +import { removeClosedArtifacts } from './query-rejected-write-cutoff-cleanup' import type { PersistedStartupHistory } from '../../db-adapter-conformance/tests/persisted-startup-oracle' import type { PersistedStartupAdapter } from '../../db-adapter-conformance/tests/persisted-startup-driver' @@ -119,7 +120,11 @@ async function run() { } }, [ () => database.close?.(), - () => removeStartupOPFSArtifacts(databaseName), + () => + removeClosedArtifacts( + () => removeStartupOPFSArtifacts(databaseName), + (cut, value) => console.info(`${name}.opfs ${cut}`, value), + ), ]) } for (const [state, count] of Object.entries(reach)) @@ -167,7 +172,11 @@ async function run() { }) }, [ () => database.close?.(), - () => removeStartupOPFSArtifacts(databaseName), + () => + removeClosedArtifacts( + () => removeStartupOPFSArtifacts(databaseName), + (cut, value) => console.info(`${name}.opfs ${cut}`, value), + ), ]) } } @@ -180,7 +189,7 @@ async function run() { artifacts, error: error instanceof Error - ? `${error.stack} ${JSON.stringify(error, Object.getOwnPropertyNames(error))}` + ? `${error.stack ?? error.toString()} ${JSON.stringify(error, Object.getOwnPropertyNames(error))}` : String(error), } } diff --git a/packages/powersync-db-collection/tests/on-demand-sync.test.ts b/packages/powersync-db-collection/tests/on-demand-sync.test.ts index 631b195fe8..8a449fe5ae 100644 --- a/packages/powersync-db-collection/tests/on-demand-sync.test.ts +++ b/packages/powersync-db-collection/tests/on-demand-sync.test.ts @@ -2750,7 +2750,11 @@ describe(`On-Demand Sync Mode`, () => { syncBatchSize: settings.syncBatchSize, }) const sync = config.sync.sync({ - collection: { status: `ready`, has: () => false }, + collection: { + status: `ready`, + health: { reloading: false }, + has: () => false, + }, begin, write, commit, From 8feb587ca1688bc7330d7d95f2c9dd99bcbc1e87 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 19:17:30 -0700 Subject: [PATCH 22/24] Allow PowerSync startup campaigns to finish on CI --- .../2026-11-10-pr-2394-final-validation.md | 37 +++++++++++++++++++ .../tests/persisted-startup-oracle-suite.ts | 4 +- ...tartup-conformance-oracle.property.test.ts | 4 ++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md index ac4ab97fb0..cbc8e0603c 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md @@ -247,3 +247,40 @@ HTTP 403 installation recovery remains stashed through the merge commit; the committed dependency pins are preserved. The full PowerSync run has progressed through both startup hosts into eager conformance but has no final verdict at this checkpoint. Final-head CI remains a separate gate. + +### Final-head CI campaign deadline correction + +The full local PowerSync run subsequently passed all 671 tests in 18 files, +with no type errors, in 931.75 seconds. On head +`331bde897d1a1ea5e688379f73a989fe93aa117e`, GitHub Collections job +`130255720337` instead reported 13 startup tests exceeding their 60-second +test deadlines. All other Collections packages passed. All E2E jobs, including +both Chromium receiving suites, and the other CI checks passed. CodeRabbit +remained paused at its previously recorded reviewed head. + +The 13 failures were the controlled host's fixed/random campaigns, nine +missing-state calibrations, and determinism on both hosts. The log reported +only deadline failures, with no model comparison failure. Neighboring +calibrations finished in 56.7–59.7 seconds. Individual fixed histories finished +in approximately 0.3–0.7 seconds. An 80-history campaign plus constructed +witnesses, and especially its repeated determinism execution, can exceed +60 seconds on that runner. + +The shared registrar now accepts a test deadline, defaulting to the existing +60 seconds. Only PowerSync startup selects 180 seconds. This changes no +production code, law, model, generator, run budget, comparison, calibration, +receiving path or product deadline. It does not classify CI timeouts as kills. + +Local command after that change: + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/powersync-db-collection exec vitest run tests/persisted-startup-conformance-oracle.property.test.ts --maxWorkers=2 --fsModuleCache=false -t 'fixed seed 2367|random seed|omits constructed response-before-restore|determinism' +``` + +Result: eight selected tests pass across both hosts, no type errors, 107.18 +seconds. The remaining 246 tests were outside this targeted selection. Each +fixed/random campaign reports all fourteen required states. The missing-reach +calibration rejects its corrupted trace, and both determinism tests pass. +`pnpm --config.verifyDepsBeforeRun=false lint:changed` also passes. New-head +CI remains required before merge. The existing maintainer allowance does not +waive that gate. diff --git a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts index 15f520a17a..42e8821ab7 100644 --- a/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts +++ b/packages/db-adapter-conformance/tests/persisted-startup-oracle-suite.ts @@ -50,12 +50,14 @@ export function registerPersistedStartupConformance< origins = [], origin = () => undefined, scopedStartupCommit = false, + timeout = 60_000, }: { property: string adapter: PersistedStartupAdapter histories: Array arbitrary: Arbitrary scopedStartupCommit?: boolean + timeout?: number origins?: Array origin?: (history: H) => string | undefined }) { @@ -506,7 +508,7 @@ export function registerPersistedStartupConformance< }), ), ], - timeout: 60_000, + timeout, }) }) } diff --git a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts index 27eab097b8..1c0ef10707 100644 --- a/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts +++ b/packages/powersync-db-collection/tests/persisted-startup-conformance-oracle.property.test.ts @@ -25,5 +25,9 @@ describe(`persisted startup conformance: PowerSync`, () => { histories: persistedStartupBatchHistories, arbitrary: historyArb, scopedStartupCommit: true, + // Each history opens a real PowerSync database. CI needs more than 60s + // for 80 histories plus constructed reach, and for their determinism replay. + // Keep every history, calibration and comparison in the shared suite. + timeout: 180_000, }) }) From f6cee8966e20460c900f5159fdc508f7d6495adc Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 19:41:13 -0700 Subject: [PATCH 23/24] Retry post-close OPFS cleanup in remaining receivers --- .../2026-11-10-pr-2394-final-validation.md | 34 +++++++ .../e2e/persisted-baseline-lifecycle.ts | 10 +- .../e2e/query-ownership.opfs.ts | 7 +- .../e2e/trailbase-lifecycle.ts | 10 +- .../tests/opfs-fixture-cleanup.test.ts | 99 +++++++++++++++++++ .../vitest.adapter-oracles.config.ts | 1 + 6 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 packages/browser-db-sqlite-persistence/tests/opfs-fixture-cleanup.test.ts diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md index cbc8e0603c..c71a513198 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md @@ -284,3 +284,37 @@ calibration rejects its corrupted trace, and both determinism tests pass. `pnpm --config.verifyDepsBeforeRun=false lint:changed` also passes. New-head CI remains required before merge. The existing maintainer allowance does not waive that gate. + +### Remaining OPFS receiver cleanup sites + +On `8feb587ca1688bc7330d7d95f2c9dd99bcbc1e87`, adapter-browser CI job +`130275725937` passed 403 checks and failed one TrailBase hydration history. +The error was `NoModificationAllowedError` from fixture file deletion, after +database close. It was the same native handle-release condition previously +corrected in the startup and eager-gap pages, not a row comparison failure. + +An audit of the shared deletion helper's callers found three remaining direct +database-close cleanup sites without its bounded retry: TrailBase hydration, +Query direct ownership, and the shared RxDB/PowerSync baseline lifecycle page. +All three now call the existing `removeClosedArtifacts` after awaited close. +It retries only `NoModificationAllowedError`, at most 100 times. Existing +cleanup aggregation, permanent failures and primary product verdicts stay. +No production code, model, history grammar or product comparison changes. + +A supplemental fixture calibration injects that exact error on the post-close +removal, separately on all four receiving paths. Before the cleanup edits, +actual Chromium fails all four checks with the injected error. Afterward, +each path retries successfully and completes another history in the same +browser origin. This calibration tests cleanup mechanics, not another product +model or an added claim of oracle closure. + +```sh +pnpm --config.verifyDepsBeforeRun=false --filter @tanstack/browser-db-sqlite-persistence test:oracles:adapters-browser tests/opfs-fixture-cleanup.test.ts tests/trailbase-hydration-oracle.test.ts tests/query-rejected-write-cutoff-browser-fidelity.test.ts +``` + +GREEN: 52 tests in three files, 86.00 seconds. This includes all 37 TrailBase +hydration checks, four new receiver cleanup calibrations, and eleven existing +transport/cleanup calibrations. Permanent busy errors still fail and a prior +product mismatch remains primary. Fixed and random hydration campaigns each +reach all nine required states. Changed-file lint and the browser package's +`tsc --noEmit` pass. New-head CI remains required before merge. diff --git a/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.ts b/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.ts index 2b35fe23ab..53c9aa57a4 100644 --- a/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.ts +++ b/packages/browser-db-sqlite-persistence/e2e/persisted-baseline-lifecycle.ts @@ -9,6 +9,7 @@ import { powerSyncBrowserBaselineLifecycleAdapter } from '../../powersync-db-col import { inProcessSeams } from '../../db-sqlite-persistence-core/tests/reload-class-oracle' import { withOracleCleanup } from '../../db/tests/oracle-cleanup' import { removeStartupOPFSArtifacts } from './persisted-startup-opfs-cleanup' +import { removeClosedArtifacts } from './query-rejected-write-cutoff-cleanup' import type { BaselineLifecycleHistory } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle' async function runBaselineLifecycle( @@ -32,7 +33,14 @@ async function runBaselineLifecycle( id, candidate, ), - [() => database.close?.(), () => removeStartupOPFSArtifacts(databaseName)], + [ + () => database.close?.(), + () => + removeClosedArtifacts( + () => removeStartupOPFSArtifacts(databaseName), + (cut, value) => console.info(`${adapter}.opfs ${cut}`, value), + ), + ], ) } Object.assign(window, { runBaselineLifecycle }) diff --git a/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts index 91b8da5dd8..8bbb90b148 100644 --- a/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts +++ b/packages/browser-db-sqlite-persistence/e2e/query-ownership.opfs.ts @@ -10,6 +10,7 @@ import { startupOPFSArtifacts, } from './persisted-startup-opfs-cleanup' import { recordQueryHistory } from './query-ownership-browser-support' +import { removeClosedArtifacts } from './query-rejected-write-cutoff-cleanup' import type { DirectOwnershipHistory } from '../../query-db-collection/tests/persisted-direct-ownership-oracle' import type { ImmediateMaterializationHistory } from '../../query-db-collection/tests/immediate-eager-materialization-oracle' @@ -30,7 +31,11 @@ async function runQueryPersistedDirectOwnership( id, observe, ) - }, [() => database.close?.(), () => removeStartupOPFSArtifacts(name)]) + }, [ + () => database.close?.(), + () => + removeClosedArtifacts(() => removeStartupOPFSArtifacts(name), observe), + ]) const artifacts = await startupOPFSArtifacts() if (artifacts.length) throw new Error( diff --git a/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.ts b/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.ts index b4d15e8bc6..690c0cf955 100644 --- a/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.ts +++ b/packages/browser-db-sqlite-persistence/e2e/trailbase-lifecycle.ts @@ -10,6 +10,7 @@ import { startupOPFSArtifacts, } from './persisted-startup-opfs-cleanup' import { runTrailBaseEager } from './trailbase-eager' +import { removeClosedArtifacts } from './query-rejected-write-cutoff-cleanup' import type { HydrationLifecycleHistory } from '../../trailbase-db-collection/tests/lifecycle-hydration-oracle' export async function runTrailBaseHydration( @@ -26,7 +27,14 @@ export async function runTrailBaseHydration( history, id, ), - [() => database.close?.(), () => removeStartupOPFSArtifacts(databaseName)], + [ + () => database.close?.(), + () => + removeClosedArtifacts( + () => removeStartupOPFSArtifacts(databaseName), + (cut, value) => console.info(`trailbase.opfs ${cut}`, value), + ), + ], ) const artifacts = await startupOPFSArtifacts() if (artifacts.length) diff --git a/packages/browser-db-sqlite-persistence/tests/opfs-fixture-cleanup.test.ts b/packages/browser-db-sqlite-persistence/tests/opfs-fixture-cleanup.test.ts new file mode 100644 index 0000000000..a6b33a59a2 --- /dev/null +++ b/packages/browser-db-sqlite-persistence/tests/opfs-fixture-cleanup.test.ts @@ -0,0 +1,99 @@ +// @vitest-environment node +/** Cleanup calibration only. Product results retain their shared oracle checks. */ +import { expect, it } from 'vitest' +import { withOracleCleanup } from '../../db/tests/oracle-cleanup' +import { directOwnershipHistories } from '../../query-db-collection/tests/persisted-direct-ownership-oracle' +import { baselineLifecycleHistories } from '../../db-adapter-conformance/tests/persisted-baseline-lifecycle-oracle' +import { startAdapterChromium } from './adapter-chromium-host' + +const receivers = [ + { + name: `TrailBase hydration`, + entry: `trailbase-lifecycle.html`, + ready: `runTrailBaseHydration`, + databaseName: `ps-trailbase-converted.sqlite`, + args: [ + { + previous: 3, + old: 0, + replacement: 6, + oldList: `reject`, + resumeAt: `replacement-ready`, + initialRender: `default`, + }, + `cleanup-calibration`, + ], + }, + { + name: `Query ownership`, + entry: `query-ownership.opfs.html`, + ready: `runQueryPersistedDirectOwnership`, + databaseName: `ps-query-direct-ownership.sqlite`, + args: [directOwnershipHistories[0]!, `cleanup-calibration`], + }, + ...([`rxdb`, `powersync`] as const).map((adapter) => ({ + name: `${adapter} baseline lifecycle`, + entry: `persisted-baseline-lifecycle.html`, + ready: `runBaselineLifecycle`, + databaseName: `ps-baseline-lifecycle.sqlite`, + args: [ + adapter, + baselineLifecycleHistories[0]!, + `cleanup-calibration`, + false, + ], + })), +] + +it.each(receivers)( + `retries a transient post-close busy handle in $name`, + async ({ entry, ready, databaseName, args }) => { + const host = await startAdapterChromium({ + entry: `/packages/browser-db-sqlite-persistence/e2e/${entry}`, + ready, + }) + await withOracleCleanup(async () => { + const result = await host.page.evaluate( + async ({ ready: method, databaseName: file, args: input }) => { + const removeEntry = FileSystemDirectoryHandle.prototype.removeEntry + let removals = 0 + let injected = 0 + FileSystemDirectoryHandle.prototype.removeEntry = function ( + name, + options, + ) { + if (name === file && ++removals === 2) { + injected++ + return Promise.reject( + new DOMException( + `fixture handle release pending`, + `NoModificationAllowedError`, + ), + ) + } + return removeEntry.call(this, name, options) + } + try { + const receive = ( + window as unknown as Record< + string, + (...values: Array) => Promise + > + )[method]! + const first = await receive(...input) + const second = await receive(...input) + return { injected, first, second } + } finally { + FileSystemDirectoryHandle.prototype.removeEntry = removeEntry + } + }, + { ready, databaseName, args }, + ) + expect(result.injected).toBe(1) + expect(result.first).not.toHaveProperty(`failure`) + expect(result.second).not.toHaveProperty(`failure`) + expect(host.errors).toEqual([]) + }, [() => host.close()]) + }, + 120_000, +) diff --git a/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts b/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts index 601d6ed0b2..82e2810255 100644 --- a/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts +++ b/packages/browser-db-sqlite-persistence/vitest.adapter-oracles.config.ts @@ -28,6 +28,7 @@ export default defineConfig({ `tests/trailbase-hydration-oracle.test.ts`, `tests/trailbase-eager-live-source-oracle.test.ts`, `tests/trailbase-browser-fidelity.test.ts`, + `tests/opfs-fixture-cleanup.test.ts`, `tests/query-rejected-write-cutoff-browser-fidelity.test.ts`, `tests/persisted-baseline-lifecycle-oracle.test.ts`, `e2e/query-*-oracle.browser.test.ts`, From 9fd2ba81dc3fca5af93314dc42e5dad1040e7bd4 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 10 Nov 2026 19:56:57 -0700 Subject: [PATCH 24/24] Route OPFS cleanup calibration only to Chromium CI --- .../2026-11-10-pr-2394-final-validation.md | 16 ++++++++++++++++ .../browser-db-sqlite-persistence/vite.config.ts | 1 + 2 files changed, 17 insertions(+) diff --git a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md index c71a513198..66c6cb9a51 100644 --- a/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md +++ b/docs/contributing/oracle-reviews/2026-11-10-pr-2394-final-validation.md @@ -318,3 +318,19 @@ transport/cleanup calibrations. Permanent busy errors still fail and a prior product mismatch remains primary. Fixed and random hydration campaigns each reach all nine required states. Changed-file lint and the browser package's `tsc --noEmit` pass. New-head CI remains required before merge. + +### Chromium calibration routing correction + +On `f6cee8966e20460c900f5159fdc508f7d6495adc`, the SQLite-adapter job +`130284780161` also selected the new cleanup calibration through the package's +default `tests/**/*.test.ts` pattern. Its runner does not install Chromium. +The four new checks failed at browser launch with a missing executable; +the other browser-package runtime checks passed (1,349 passed, two expected +failures, one skipped). This is a test-registration mistake, not a product +comparison failure. + +The default Vite configuration now lists this file beside the existing +Chromium-only exclusions. The dedicated adapter-browser configuration still +includes it. `vitest list --filesOnly --json` verifies that the default +configuration excludes the file and the Chromium configuration selects it. +No test is disabled in its intended receiving job. Changed-file lint passes. diff --git a/packages/browser-db-sqlite-persistence/vite.config.ts b/packages/browser-db-sqlite-persistence/vite.config.ts index 1fa6729350..d2fd228966 100644 --- a/packages/browser-db-sqlite-persistence/vite.config.ts +++ b/packages/browser-db-sqlite-persistence/vite.config.ts @@ -11,6 +11,7 @@ const adapterBrowserTests = [ `tests/trailbase-hydration-oracle.test.ts`, `tests/trailbase-eager-live-source-oracle.test.ts`, `tests/trailbase-browser-fidelity.test.ts`, + `tests/opfs-fixture-cleanup.test.ts`, `tests/query-rejected-write-cutoff-browser-fidelity.test.ts`, `tests/persisted-baseline-lifecycle-oracle.test.ts`, ]