diff --git a/.changeset/fix-client-pages-and-suspense-ownership.md b/.changeset/fix-client-pages-and-suspense-ownership.md new file mode 100644 index 0000000000..56cb0d8dac --- /dev/null +++ b/.changeset/fix-client-pages-and-suspense-ownership.md @@ -0,0 +1,8 @@ +--- +'@tanstack/db': patch +'@tanstack/react-db': patch +'@tanstack/preact-db': patch +'@tanstack/svelte-db': patch +--- + +Keep live-query Collections across finite page changes under a DbClient, including React Suspense and Svelte, while preserving page-specific preload and hydration snapshots. Give independent React Suspense queries separate successful Collections so one query cannot move another query's page. Surface a private Suspense output's failed first load while it is showing copied ready rows, and keep its health independent of a peer's page-request failures. Fix initially disabled keyed query admission in Preact and Svelte, and finish hydration seed handoff when subset work settles without a row or status change. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index da2b6fbbbe..0478d310db 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -2029,7 +2029,8 @@ rendering before flush or reactive adapters' option updates. Its exhausts two initial bindings across config pairs, derived/same/different keys, unscoped/client caches, and distinct same-name constructor references. It checks both admissions, each admitted output's settings, retained first settings, and -compatible sharing and forbidden sharing between two admitted outputs. +independent output identity between two admitted bindings, including equal +declarations (maintainer decision, 2026-11-03, superseding compatible sharing). Its 582 histories retain the original 534 cases and add 24 omitted/explicit- undefined cases and 24 pending-source cases. They check first-option retention on rerender (528 admitted-first histories) and application after an actual @@ -2067,8 +2068,9 @@ The local owner uses equal/different callback and scalar declarations; the indexing owner crosses mode and constructor equality. Both hold source writes, require two distinct visible Suspense fallbacks before release, then use their existing output comparisons after delivery. Per-scope aggregate reach requires -both-suspended, shared and separate outputs, plus each representative option -dimension. A mutant that reuses and memoizes an unqualified pending entry passes +both-suspended, equal and different option declarations, plus each +representative option dimension. As of 2026-11-03, both cases require distinct +outputs; the original sharing outcomes remain historical evidence only. A mutant that reuses and memoizes an unqualified pending entry passes the old 882 histories and fails 36 extended histories after the pending premise. These controlled eager-source schedules do not establish abandonment, rejection while pending, cleanup races, hydration, or arbitrary Suspense scheduling. @@ -6785,13 +6787,158 @@ The separate Index structural-collision obligation above remains open. See the appended [review evidence](oracle-reviews/2026-10-30-distinct-collisions.md). +### React Suspense initial-view lifecycle + +[The lifecycle oracle](../../packages/react-db/tests/suspense-lifecycle-oracle.test.tsx) +owns the React boundary observation of current initial-view obligations. Its +small model keeps only the current obligation, public pending/ready/error view, +rows, boundary cause policy, and stopped-source health. It has no retry cache, +observer, or source lifecycle implementation. Authority is the error-handling +guide's Suspense contract, hook JSDoc, the shared conformance contract's retry +progress and exclusive output laws, and the established cleanup/request recovery +contract examples. + +Fixed and fresh campaigns generate source fail/crash/abandon, pending rerenders, +boundary reset/root remount, retired rejection, successful peers, and source +failure after readiness. Derived/keyed queries run without a client, with a +provider, and with an explicit client. Supplied Collections receive query-only +preload rejection before/after cleanup without a client or under a provider. +After a late rejection, a forced render checks the replacement while it remains +pending; readiness cannot hide a stale wrapper error. Four legal client request +schedules cover observed failure, replacement in the old promise's rejection +reaction, cleanup before rejection, and switching clients. A pending client +record deduplicates work, so registration while it remains pending is not +modeled as replacement. + +The co-mounted sibling grammar places both hooks before their first successful +commit. Shared/separate boundaries, equal/different queries, staggered/same-pass +readiness, and ready/lagging source Collections cross every client/key mode. +The staggered path drains only the selected query's acquisitions until its hook +returns successfully. The peer's hook has not returned and its demand stays +held. Shared-boundary histories also require that neither layout effect commits. +Each receiving mode declares this successful-candidate state separately from +source-release ordering. +Each generated family guarantees the reported shared-boundary shape. Every +successful layout commit checks distinct output Collections. After both commit, +a held private startup crosses a peer page move and an unchanged-input render; +the copied ready rows stay on their own page until private startup settles. +Render-time retry-cache eviction, an effect-only ownership fix, and copying a +peer's moving rows all fail this receiver. These are finite React 19/jsdom +histories with controlled on-demand requests, not arbitrary scheduler coverage. + +Copied ready rows do not establish the private output's initial-query readiness. +The sibling grammar also rejects its held first acquisition before or after a +peer page move, with Error, record and undefined provider causes. Each client/key +mode declares and measures both rejection schedules. The existing initial-view +model distinguishes a provisional seed from owned readiness: a first-load error +ends the seed and reaches the real React ErrorBoundary; a source failure after +owned readiness keeps data and appears in health. This receives the observer +hydration owner's failure-ending-seed law. It reads the private Collection's +public normalized failure as input and checks the boundary's cause; it does not +reimplement core's provider-error normalization. Both the original hidden error +and a repair that clears the seed without resetting readiness fail this receiver. +The first error comparison runs before any application rerender, with declared +reach before and after the peer move. A later reread checks the same error +again. Suppressing only the private initial-error notification fails the first +comparison; a forced host reread cannot supply the missing notification. + +The sibling grammar also discards candidates after their hook returns and +lets a peer commit first under a separate boundary. The mounted replacement +receiver admits a peer's pending target plan before the mounted binding changes +to it, and crosses before/after-hook holds. Both receivers compare progress, +exclusive outputs and subsequent retained page moves. Diagnostic acquisition +counts expose finite retry work; no exact request budget or retention of +uncommitted Collections is promised. + +While private startup remains held, source report/recovery histories compare +committed health before and after a peer page move without a host rerender. +A peer's page-request rejection then crosses an unchanged-input read. Its +query-local problem must not appear on the seeded output or the unchanged +source. The seed takes health from its own observer. Restoring peer health +copying fails request isolation; freezing seed health fails source notification. +These are query-local and source-health receiving laws, not provider transport +or garbage-collection claims. + +Each receiving campaign declares reach and replays fixed examples through the +same model/checker. Five hostile mutations fail at retry progress, successful +peer ownership, replacement pending, restart pending, and late preload rejection. +These laws previously depended on ordinary fixed recovery tests; the generated +owner adds adjacent histories and direct seed/path replay. The core source-health +and observer owners retain their status, health-tree, and transport contracts; +the page owner retains page mutation and peer isolation. Limits: finite one-source +queries, real React boundaries in jsdom, controlled subset work and a public +query-preload seam. This does not establish native hydration, provider behavior, +React 18 scheduling, arbitrary concurrent rendering, exact GC timing, or delayed +failure-retirement timers. Error message text remains with the existing supplied +query boundary examples; this owner checks Error identity or wrapped cause. +The [review record](oracle-reviews/2026-11-03-client-suspense-page-ownership.md) +contains per-mode reach, calibration, replay and grammar accounting. + ### Adapter-owned page windows The framework owner includes [window-history-oracle.ts](../../packages/db/tests/conformance/window-history-oracle.ts), registered through the shared conformance suite in React, Preact, Vue, Solid, Angular and Svelte. Maintainer decision 2026-10-30 for #2291: changing only `.limit()`/`.offset()` moves the existing outer window; it adds no hook option. The independent sort/filter/slice model checks ordered selected rows and keyed-map values after each framework flush, Collection retention for page moves, replacement for source/filter/order/projection/nested-window changes, and retained upstream ownership. Direct and nested paths have separate reach, as do changing/stable/opaque keys where the driver supports keys. Keyed source swaps include distinct source objects under one Collection ID. Generated source updates check liveness after reuse. The bare-callback lanes also vary a Collection option together with a page change and check its applied value. The pending lane observes a move during initial loading, oldest settlement while the latest remains pending, initial readiness, overlapping held acquisitions, final latest rows, and unmount before settlement. The publication-callback lane changes offset, limit or both from a public change callback, then checks the retained Collection after the host settles. It controls demand settlement over already installed source rows; the core pagination owner continues to own source delivery, failures and ordered repair. -The [React receiving witness](../../packages/react-db/tests/window-render-oracle.test.tsx) reuses this model/checker at suspended, cancelled and committed render cuts for eager and finite on-demand sources, with derived and explicit keys. It checks the captured committed Collection and provider acquisition count while a render is abandoned. The [Preact receiving witness](../../packages/preact-db/tests/window-render-oracle.test.tsx) checks an aborted render under a real error boundary while a public subscription keeps the committed Collection active. Both run separate offset-only, limit-only and combined-move campaigns with declared reach per category. These do not establish arbitrary concurrent framework schedules. The retained-work observation checks continuous upstream ownership and every public subscription-count transition during a retained move; add-before-remove replacement fails, while retired Collections may complete GC. This is not a bound on CPU time or every possible internal compilation strategy. DbClient/hydration and Suspense shared-result caches, supplied Collections, opaque-plan optimization, queries without an outer limit and findOne retain their existing contracts and paths; this fix does not retarget their shared Collections. The generated retention guarantee covers finite bounds. An explicit Infinity limit can also move to a finite target under the resolver guard, but this evidence does not establish that additional domain or promise replacement outside the finite domain. The existing identity, deferred-acquisition, window-controller and includes owners remain authoritative for their neighboring laws. - -Evidence, per-path reach counts, reviewer reconciliation and mutation outcomes are recorded in [the page-window review](oracle-reviews/2026-10-30-adapter-page-windows.md). +The [React receiving witness](../../packages/react-db/tests/window-render-oracle.test.tsx) reuses this model/checker at suspended, cancelled and committed render cuts for eager and finite on-demand sources, with derived and explicit keys. It checks the captured committed Collection and provider acquisition count while a render is abandoned. The [Preact receiving witness](../../packages/preact-db/tests/window-render-oracle.test.tsx) checks an aborted render under a real error boundary while a public subscription keeps the committed Collection active. Both run separate offset-only, limit-only and combined-move campaigns with declared reach per category. These do not establish arbitrary concurrent framework schedules. The retained-work observation checks continuous upstream ownership and every public subscription-count transition during a retained move; add-before-remove replacement fails, while retired Collections may complete GC. This is not a bound on CPU time or every possible internal compilation strategy. The page law now runs through client-backed React and Svelte drivers, and React Suspense both with and without a client (maintainer decision, 2026-11-03, issue #2345). DbClient is a driver mode, not a React exception; future client-backed adapters register the same histories and comparisons. Each mode receives direct/nested, applicable keyed/derived, pending, publication-callback, changed-plan and source-write histories. Every adapter also runs the independent-peer histories: equal queries yield distinct outputs; declarative and imperative moves preserve the peer page; source writes update both; retiring one leaves its peer usable. Before initial Suspense readiness, the pending comparison observes the actual fallback and absence of a successful result. It starts retention comparisons at the first successful return. + +The React render receiver crosses ordinary/Suspense hooks, provider/explicit-client/descriptor inputs, eager/on-demand sources, derived/keyed identities, and offset/limit/combined moves. A separate receiving schedule in that same file blocks a mounted replacement before its hook while a peer finishes the same preload, then resumes it. Its rows and identity use the shared page comparison; a missing exclusive-ownership check fails after the peer has rendered first. The [client page receiver](../../packages/react-db/tests/client-window-oracle.test.tsx) checks JSON-transported server preloads at the first browser hook render, a move before local readiness, local handoff, peer isolation, return to the first page, and replacement on a client switch over the same concrete source. Keyed preload identity includes the outer page. This receiver does not run hydrateRoot or a real Electric provider. Suspense retry work may remain unclaimed across discarded renders, but a successful binding takes exclusive ownership; returned query Collections are never shared implicitly. Supplied Collections, opaque-plan optimization, queries without an outer limit and findOne retain their existing contracts; no transported snapshot is retargeted. The generated retention guarantee covers finite bounds. An explicit Infinity limit can also move to a finite target under the resolver guard, but this evidence does not establish that additional domain or promise replacement outside the finite domain. The existing identity, deferred-acquisition, window-controller and includes owners remain authoritative for their neighboring laws. + +The React client page receiver also crosses the held-seed history with a +speculative offset-only, limit-only, or combined move and cancellation. Each +keyed/derived × ordinary/Suspense × provider/explicit-client campaign proves +React ran the post-hook suspension, checks the original committed seed while +suspended, then checks data, keyed state and Collection identity after the +original page renders again. Local readiness remains held through both cuts. +This distinguishes a wrong observer transport identity that correct local rows +would conceal. All 48 fixed/fresh campaigns reject the original render-time +observer mutation. The source remains a controlled finite Collection; native +hydration and arbitrary React schedules remain outside this receiver. + +The mounted replacement/borrower receiver also generates both co-ready and +borrower-first schedules. It moves the borrower after the original compiler +retries, then moves the original binding and checks both pages. Reusing the +compiler's resolver token fails borrower Collection retention. This adds the +next-use direction the earlier receiver omitted; the initial successful +Collections remain distinct in the reviewed implementation's co-ready lane. +Before the original binding moves, every mode also checks its unchanged rows, +keyed state and previously captured Collection's rows. Both bindings' keyed +state is checked after each move. A wrong original page cannot disappear behind +the original binding's next action before it is observed. + +Svelte client recovery histories cross a committed declarative move, an +imperative move, a throwing key getter, repeated public rejection, the throwing +native pre-effect, and recovery. Equal keys retain the authored input and its +imperative page despite changed body/dependency proposals; changed keys admit +the new plan. The existing independent window judgment compares data, keyed +state and Collection retention after native settlement. Removing stable-key +retention fails this receiver. This does not add disabled config fields to the +typed API or promise in-place key mutation. + +The shared owner also crosses initially disabled inputs with stable keys through real ordinary React, Preact and client-backed Svelte bindings. A disabled input admits no plan; first enablement admits one, an enabled unchanged key retains it, and a changed key admits the changed plan. This distinguishes the enablement fix from deleting stable-key retention. Suspense has no disabled-input overload and does not receive this lane. + +The [Svelte client page receiver](../../packages/svelte-db/tests/client-window-oracle.svelte.test.ts) and React client receiver share [transported-page histories](../../packages/db/tests/conformance/client-window-history-oracle.ts) and the existing window judgment. Every keyed/derived receiving campaign mounts one or two fresh old-page readers after a move while local readiness is held. It compares their first seed and distinct Collection, then the moved binding and existing peer through local handoff and return. Svelte uses rune construction, native `flushSync` for held-readiness page changes, and a bounded settled cut for the asynchronous observer handoff; it does not claim native `hydrate`. React's client-switch subhistory now actually executes ordinary/Suspense × keyed/derived hooks, replacing its Collection for both same-page and moved-page switches through explicit clients. Those per-mode results are later evidence, not retroactive credit for the original pre-edit gate's ordinary unkeyed switch. + +Both transported-page receivers compare the existing peer's keyed state at +every peer row comparison. Each receiving mode declares separate reach while +the seed is held and after local handoff. A wrong keyed map fails even when +the peer's data array and Collection identity remain correct. + +The ready-output/subset-settlement handoff found by this receiving extension is owned by the existing [observer hydration oracle](../../packages/db/tests/live-query-observer-hydration-oracle.property.test.ts). Its new fixed/fresh grammar holds subset work on a ready Collection before seeded observer attachment, then settles it without any row or status change. Both granular and wholesale consumers must keep the seed while held, then observe and receive the local snapshot, including empty local rows. The original missing subset notification and a premature handoff each fail their intended comparison. The Svelte page receiver supplies the same premise through retained live-query Collections. These controlled histories do not establish arbitrary provider schedules. + +The same shared window owner enforces immutable explicit-key values (maintainer +decision, 2026-11-03). React, React DbClient, ordinary/client Suspense, Preact and +Svelte DbClient receive equal-key retention and changed-key admission histories. +Each fixed/fresh campaign declares separate reach for the same key object, a +fresh outer array, fresh equal nested values, reordered record properties, +independent record/array leaf replacements, equality after replacement and a +return to an earlier key. Ordered data, keyed state and Collection identity are +compared after native host settlement. Keys are frozen plain records/arrays +with JSON scalar leaves. In-place mutation is unsupported; its behavior is +unconstrained. Rich native keys, speculative cuts and transported seeds remain +outside this lane. Reactive bindings that compare no query identity retain +their reactive input contract. Future identity-comparing adapters register the +same key histories through `mountKeyedControllable`. + +Evidence, per-path reach counts, reviewer reconciliation and mutation outcomes are recorded in [the page-window review](oracle-reviews/2026-10-30-adapter-page-windows.md) and the [client/Suspense extension](oracle-reviews/2026-11-03-client-suspense-page-ownership.md). The [fresh independent review](oracle-reviews/2026-10-30-adapter-page-windows-round-2.md) records observation and failure-fidelity repairs. Shared and receiving campaigns preserve the first violated law, checkpoint and input trace through shrinking. Later action and cleanup failures remain distinguishable; non-reproducing shrink candidates supply no successful product evidence. The original late-coverage process gap remains documented. diff --git a/docs/contributing/oracle-reviews/2026-11-03-client-suspense-page-ownership-manifest.json b/docs/contributing/oracle-reviews/2026-11-03-client-suspense-page-ownership-manifest.json new file mode 100644 index 0000000000..c1256bbe06 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-03-client-suspense-page-ownership-manifest.json @@ -0,0 +1,1138 @@ +{ + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "four oracle enforcement repairs on42514b45f; no production edits; digest excludes report and manifest", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "reviewedHead": "42514b45f3e8d63f3bb4000280d915ab4079d41c", + "pullRequest": "https://github.com/TanStack/db/pull/2348", + "combinedSha256": "2324d2e46262f0e5a63498bc53824e44aa33db945904159727bd8905792cc5ad", + "files": [ + { + "path": ".changeset/fix-client-pages-and-suspense-ownership.md", + "sha256": "bb2841a86bccfbc4f2310ffa33e4e8f63918119ca1b2045684b59258906439a7" + }, + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "5fca4d39566093542765264a5e524f001f2e60854e6283ef724df54277594402" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "fec4e59f6ce4507df1819913963390977e2981f31b44c49a3055238f2dbfe663" + }, + { + "path": "docs/guides/live-queries.md", + "sha256": "a46cb2dba26ded881b62a2d94f335a786e06b3240209c6b3ceeedc4f23f41af2" + }, + { + "path": "packages/db/src/live-query-observer.ts", + "sha256": "ddfc273f9206ad97f7bfd0152fa953188fa92440b053777c1d90c8c23e06c885" + }, + { + "path": "packages/db/src/live-query-options.ts", + "sha256": "41e653166c7daf5d4d422be294f1a98c2d50fc9200042637032d2932c692e532" + }, + { + "path": "packages/db/tests/conformance/client-window-history-oracle.ts", + "sha256": "bf0377d06a0069708a03f476ecda63719c0464588dd912cc096472df0caad3ee" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "94fbf8fa38608747b10330c3a307887c73393b0a64ff2157b824a9ff2e54afb9" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "4d1b78fd610044d318a1cee7762930c7dece0b709df19b6bdc3322c252c0542f" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "f273af8197be4afd9cb4dad7fa17d07cdc15671b82f1704e0d7e12ea5cf0e702" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "e5b26430f3f6e1cf82f19e6a1e9fdedf4553810dd88baae731033be4de860e92" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/live-query-observer-hydration-oracle.property.test.ts", + "sha256": "b2ae5fb670e0d52a6eec0821217d01698e71e9358d1ead71e38cc4e4bbdfe22b" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "497c18823374a7d0e769559f16499c7f90f76e2c99edf5fd6df7ace48a3ca6c0" + }, + { + "path": "packages/db/tests/oracle-replay-manifest.ts", + "sha256": "e0480f10c1861a7960bae3100f06673a5e978ee95ee646d2e9d30937dd421327" + }, + { + "path": "packages/db/tests/oracle-replay.test.ts", + "sha256": "7bf2f6e2f73858bf9dd2b61b1e86fd382943b89aa6038ea1f0e453ffcfae08f5" + }, + { + "path": "packages/preact-db/src/useLiveQuery.ts", + "sha256": "6fc91226b13950bd033ba5e7955b4b4427e4c3be1445074cfee19d25d531b986" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "5aab14a8d8f1b8bac879773029bad3b13c49a36ccefc23732c69178f5fe28a49" + }, + { + "path": "packages/react-db/src/useLiveSuspenseQuery.ts", + "sha256": "3a7dffba5ce2493ae190b4e82462c78c4e79233ddcb7299b2498d3d8f56d3366" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "2ca4e03e3f6b152c10613e3b486731dab2f6900a3b75e271050f784b98d672b7" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "10c13bc46724c093d5caf77e3f14bb4325604ca1016ff13b1f7c608b0dcc14b0" + }, + { + "path": "packages/react-db/tests/suspense-lifecycle-oracle.test.tsx", + "sha256": "9821b2ee8636c88ddeac17543ba56e4528b2975c228211734a05e3a1a80a7aad" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "138ce6f5153b03236cb1c8505d012ffea24b25f76f58961755911a0084c7a266" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "c4c0a0b27cbe57946417d108007cf8469963732cef489a29ddb2a010a3f67e3d" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "33bfce543dd29ee44f4485769af0021b76bc7e6649e3fc370877efdaee173b28" + }, + { + "path": "packages/svelte-db/tests/client-window-oracle.svelte.test.ts", + "sha256": "d66ae788fc6abbc7fa088dab6bd7bb66c9c3dac985eb6c99327d23b127fa7075" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ], + "priorManifests": [ + { + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "uncommitted; no PR", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "combinedSha256": "1972e2bfdc64b61c11996f50db5c0afc5433cdb50b028220a1d59166dccb1e55", + "files": [ + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "b1429ec2b6806bd7924111df9180d4b85b3f0015532cba8a1fd416dcc6b4723d" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "8d7ce71adac30864b49a9f0f46f78afabfe2184f1a5de39537452820c75622ed" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "f3b5644aff82a12eec0bc1d8676e373f70fa23bfba5ab90cca06233628c1dd4e" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "0aba35b33bd3e2c656e04b65406368c8ad40bb3ae0879a0dc2976a4720f763ed" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "c5a7fd8da6ed46b9096962167e62f3ed08ea21e25de23712b1831549bfa05011" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "c6a2b2c7b4cdda177d7d51a536707758030eaed75fb6a8e9e17c667d0355bf5f" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "3547b4b78fad2f00a4fa2fac9f01e87cf9e734ec06d84f73091f953214c94a0e" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "2f26818a251aa498c7edcaea7fa855fbd836f2aa1cc2c6750dcc20d24358c15f" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "a645c6d892485b8abe4f918d6341ab374a0d8af05196f2ebba0dd63adec3ed5b" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "6e9e933c0d4778eb07761aac6aa6cafd954f0528349153fd61b8082a18bd52e6" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "0fc239c55e3db14034ab075b9f93e2faadb60e267ec7449c2fe31d0abcf40e07" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "6750c9412be8995ebf942b2c4af2ad043917cc34fa2a88e4f17a77b944fad527" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "95a0c3af45bc6978480b84451be7cc74289e2f214264655a034afa10912b7f23" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ] + }, + { + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "uncommitted; no PR", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "combinedSha256": "efd5bb4df0db5f86e9c169439835202d11ef15466dfff01ccb8a21d4ce3e2c79", + "files": [ + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "b1429ec2b6806bd7924111df9180d4b85b3f0015532cba8a1fd416dcc6b4723d" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "8d7ce71adac30864b49a9f0f46f78afabfe2184f1a5de39537452820c75622ed" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "f3b5644aff82a12eec0bc1d8676e373f70fa23bfba5ab90cca06233628c1dd4e" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "0aba35b33bd3e2c656e04b65406368c8ad40bb3ae0879a0dc2976a4720f763ed" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "c5a7fd8da6ed46b9096962167e62f3ed08ea21e25de23712b1831549bfa05011" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "c6a2b2c7b4cdda177d7d51a536707758030eaed75fb6a8e9e17c667d0355bf5f" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "3547b4b78fad2f00a4fa2fac9f01e87cf9e734ec06d84f73091f953214c94a0e" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "7170c6e9447a73ef3bc392b05a54da5b2fd50bbb5a369fe1bcf242a920c13bed" + }, + { + "path": "packages/react-db/src/useLiveSuspenseQuery.ts", + "sha256": "0e52d7f4a5f8644de1fdc2f959e2ab20209dcef3f43619b6377ed6db1b8d0836" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "a645c6d892485b8abe4f918d6341ab374a0d8af05196f2ebba0dd63adec3ed5b" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "6e9e933c0d4778eb07761aac6aa6cafd954f0528349153fd61b8082a18bd52e6" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "0fc239c55e3db14034ab075b9f93e2faadb60e267ec7449c2fe31d0abcf40e07" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "6750c9412be8995ebf942b2c4af2ad043917cc34fa2a88e4f17a77b944fad527" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "821f6d40246121e65e7c2658e3a80d66d2d0962fc13621cb697bce1491845f11" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ] + }, + { + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "uncommitted; no PR", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "combinedSha256": "2c1e7f6caaacc77e2e7c51edf177820466b1cf15723e460116399fcb95517fdf", + "files": [ + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "3c9b9629c8247bc182ccd6700e1624d341fde19e1f98e0db09a1d610b099469a" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "a385d3b261cbf5405b5f5ee3fb700d181937c3ab3ae4a0700662aebec669a0a6" + }, + { + "path": "packages/db/src/live-query-observer.ts", + "sha256": "ddfc273f9206ad97f7bfd0152fa953188fa92440b053777c1d90c8c23e06c885" + }, + { + "path": "packages/db/tests/conformance/client-window-history-oracle.ts", + "sha256": "3a25d706ca2c25eb78679468ef3c2ee8485d8dbd1053e076bb817415c457a2a3" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "0951d628168d86deadd9393312047abd654347ce7a26ce5d355effa65cb34f6d" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "0aba35b33bd3e2c656e04b65406368c8ad40bb3ae0879a0dc2976a4720f763ed" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "c5a7fd8da6ed46b9096962167e62f3ed08ea21e25de23712b1831549bfa05011" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "bc8f2fc3c9c920d2226572e6eeb1d216a2edd3719e62a6c597cdd6c1534ad2ce" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/live-query-observer-hydration-oracle.property.test.ts", + "sha256": "b2ae5fb670e0d52a6eec0821217d01698e71e9358d1ead71e38cc4e4bbdfe22b" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "dd288f2d9c552f82cdc818a04edc187b39389385bd6dc45032c9a549c1dbf9ca" + }, + { + "path": "packages/preact-db/src/useLiveQuery.ts", + "sha256": "6fc91226b13950bd033ba5e7955b4b4427e4c3be1445074cfee19d25d531b986" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "7170c6e9447a73ef3bc392b05a54da5b2fd50bbb5a369fe1bcf242a920c13bed" + }, + { + "path": "packages/react-db/src/useLiveSuspenseQuery.ts", + "sha256": "0e52d7f4a5f8644de1fdc2f959e2ab20209dcef3f43619b6377ed6db1b8d0836" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "084c8c82227827a4bd8357f8b8d0d300a9ccfa64da6eab29be4d56e9ed850b9f" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "6e9e933c0d4778eb07761aac6aa6cafd954f0528349153fd61b8082a18bd52e6" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "0fc239c55e3db14034ab075b9f93e2faadb60e267ec7449c2fe31d0abcf40e07" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "6750c9412be8995ebf942b2c4af2ad043917cc34fa2a88e4f17a77b944fad527" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "33bfce543dd29ee44f4485769af0021b76bc7e6649e3fc370877efdaee173b28" + }, + { + "path": "packages/svelte-db/tests/client-window-oracle.svelte.test.ts", + "sha256": "eb436bf1ae6fb5bda8d9a7a4c19463fbb7893c778348a6d84a71f1805c64c62c" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ] + }, + { + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "uncommitted; no PR", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "combinedSha256": "d4899416d330bf345599932475fd273aa124e4ecb0f51ecf508162d87edc27d2", + "files": [ + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "5f87d96768813ce8a58346b4d49b099a3a7c2b975f0ddf1af73cf101ffbac194" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "a385d3b261cbf5405b5f5ee3fb700d181937c3ab3ae4a0700662aebec669a0a6" + }, + { + "path": "packages/db/src/live-query-observer.ts", + "sha256": "ddfc273f9206ad97f7bfd0152fa953188fa92440b053777c1d90c8c23e06c885" + }, + { + "path": "packages/db/tests/conformance/client-window-history-oracle.ts", + "sha256": "3a25d706ca2c25eb78679468ef3c2ee8485d8dbd1053e076bb817415c457a2a3" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "0951d628168d86deadd9393312047abd654347ce7a26ce5d355effa65cb34f6d" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "0aba35b33bd3e2c656e04b65406368c8ad40bb3ae0879a0dc2976a4720f763ed" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "c5a7fd8da6ed46b9096962167e62f3ed08ea21e25de23712b1831549bfa05011" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "bc8f2fc3c9c920d2226572e6eeb1d216a2edd3719e62a6c597cdd6c1534ad2ce" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/live-query-observer-hydration-oracle.property.test.ts", + "sha256": "b2ae5fb670e0d52a6eec0821217d01698e71e9358d1ead71e38cc4e4bbdfe22b" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "723c7dac2a6ec786f329aa9a5fa391e94870ed1dc24e6607d807ec375bb6714d" + }, + { + "path": "packages/db/tests/oracle-replay-manifest.ts", + "sha256": "c6ad530ffc17eedcd06a76271ab5dbfa3ee648c0d0ec1d933e6104be7f207d3f" + }, + { + "path": "packages/preact-db/src/useLiveQuery.ts", + "sha256": "6fc91226b13950bd033ba5e7955b4b4427e4c3be1445074cfee19d25d531b986" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "7170c6e9447a73ef3bc392b05a54da5b2fd50bbb5a369fe1bcf242a920c13bed" + }, + { + "path": "packages/react-db/src/useLiveSuspenseQuery.ts", + "sha256": "0e52d7f4a5f8644de1fdc2f959e2ab20209dcef3f43619b6377ed6db1b8d0836" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "084c8c82227827a4bd8357f8b8d0d300a9ccfa64da6eab29be4d56e9ed850b9f" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "6e9e933c0d4778eb07761aac6aa6cafd954f0528349153fd61b8082a18bd52e6" + }, + { + "path": "packages/react-db/tests/suspense-lifecycle-oracle.test.tsx", + "sha256": "a2ae9626f8df30774db0f9be923bce7f7a4d7c8e244645a6177542a57cb84a5c" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "0fc239c55e3db14034ab075b9f93e2faadb60e267ec7449c2fe31d0abcf40e07" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "6750c9412be8995ebf942b2c4af2ad043917cc34fa2a88e4f17a77b944fad527" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "33bfce543dd29ee44f4485769af0021b76bc7e6649e3fc370877efdaee173b28" + }, + { + "path": "packages/svelte-db/tests/client-window-oracle.svelte.test.ts", + "sha256": "eb436bf1ae6fb5bda8d9a7a4c19463fbb7893c778348a6d84a71f1805c64c62c" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ] + }, + { + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "post-review repair on 8cf448391; PR #2348", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "reviewedHead": "8cf448391e59c0ebdf6a05c2fa2fa7f8dadae711", + "pullRequest": "https://github.com/TanStack/db/pull/2348", + "combinedSha256": "a431a728438cb265f55001ef18d214794c6d78a543db18285b42b6f4a481e1e1", + "files": [ + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "7049350ed863c3f4541d86936ac60ef49bee8ef2010a46632aaf68e4d11059b4" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "a385d3b261cbf5405b5f5ee3fb700d181937c3ab3ae4a0700662aebec669a0a6" + }, + { + "path": "packages/db/src/live-query-observer.ts", + "sha256": "ddfc273f9206ad97f7bfd0152fa953188fa92440b053777c1d90c8c23e06c885" + }, + { + "path": "packages/db/tests/conformance/client-window-history-oracle.ts", + "sha256": "3a25d706ca2c25eb78679468ef3c2ee8485d8dbd1053e076bb817415c457a2a3" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "0951d628168d86deadd9393312047abd654347ce7a26ce5d355effa65cb34f6d" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "0aba35b33bd3e2c656e04b65406368c8ad40bb3ae0879a0dc2976a4720f763ed" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "c5a7fd8da6ed46b9096962167e62f3ed08ea21e25de23712b1831549bfa05011" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "bc8f2fc3c9c920d2226572e6eeb1d216a2edd3719e62a6c597cdd6c1534ad2ce" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/live-query-observer-hydration-oracle.property.test.ts", + "sha256": "b2ae5fb670e0d52a6eec0821217d01698e71e9358d1ead71e38cc4e4bbdfe22b" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "723c7dac2a6ec786f329aa9a5fa391e94870ed1dc24e6607d807ec375bb6714d" + }, + { + "path": "packages/db/tests/oracle-replay-manifest.ts", + "sha256": "50a51b4c745fef7490ee46f280a3035bdff268dcc59565dc3959a2352c284ee0" + }, + { + "path": "packages/db/tests/oracle-replay.test.ts", + "sha256": "535e7c58194cd25f1a6a27b01c6bcb3b0f4217eb040d0d431156d5c4cbe262ea" + }, + { + "path": "packages/preact-db/src/useLiveQuery.ts", + "sha256": "6fc91226b13950bd033ba5e7955b4b4427e4c3be1445074cfee19d25d531b986" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "4ed56e4a554254bad590882ad12954a123ce178a6825d32fcd67927e49e35299" + }, + { + "path": "packages/react-db/src/useLiveSuspenseQuery.ts", + "sha256": "0e52d7f4a5f8644de1fdc2f959e2ab20209dcef3f43619b6377ed6db1b8d0836" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "ced00d99ca8c7bfb89efc78ca7369b2ef77ad3f669de4efdf63d91e8b05fa881" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "6e9e933c0d4778eb07761aac6aa6cafd954f0528349153fd61b8082a18bd52e6" + }, + { + "path": "packages/react-db/tests/suspense-lifecycle-oracle.test.tsx", + "sha256": "a2ae9626f8df30774db0f9be923bce7f7a4d7c8e244645a6177542a57cb84a5c" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "0fc239c55e3db14034ab075b9f93e2faadb60e267ec7449c2fe31d0abcf40e07" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "6750c9412be8995ebf942b2c4af2ad043917cc34fa2a88e4f17a77b944fad527" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "33bfce543dd29ee44f4485769af0021b76bc7e6649e3fc370877efdaee173b28" + }, + { + "path": "packages/svelte-db/tests/client-window-oracle.svelte.test.ts", + "sha256": "eb436bf1ae6fb5bda8d9a7a4c19463fbb7893c778348a6d84a71f1805c64c62c" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ] + }, + { + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "immutable explicit-key enforcement on 69664f1d9; production unchanged; PR #2348", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "reviewedHead": "69664f1d9215bfdbe7fb2d31e7e5ee0661506f20", + "pullRequest": "https://github.com/TanStack/db/pull/2348", + "combinedSha256": "0247b20dac86a5f8da38769860bf7c4d78c2baefab61c87c2b2bb06796f8ef3f", + "files": [ + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "df58c69c97da0fd3f124748523b30cd4cddf0127744e1b79c95644f60d460f43" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "fec4e59f6ce4507df1819913963390977e2981f31b44c49a3055238f2dbfe663" + }, + { + "path": "docs/guides/live-queries.md", + "sha256": "a46cb2dba26ded881b62a2d94f335a786e06b3240209c6b3ceeedc4f23f41af2" + }, + { + "path": "packages/db/src/live-query-observer.ts", + "sha256": "ddfc273f9206ad97f7bfd0152fa953188fa92440b053777c1d90c8c23e06c885" + }, + { + "path": "packages/db/tests/conformance/client-window-history-oracle.ts", + "sha256": "3a25d706ca2c25eb78679468ef3c2ee8485d8dbd1053e076bb817415c457a2a3" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "94fbf8fa38608747b10330c3a307887c73393b0a64ff2157b824a9ff2e54afb9" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "0aba35b33bd3e2c656e04b65406368c8ad40bb3ae0879a0dc2976a4720f763ed" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "c5a7fd8da6ed46b9096962167e62f3ed08ea21e25de23712b1831549bfa05011" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "21e167b8ef3d62a212c9bd9784a49eff34f18db8eac1fb2e70f1d7687b7289fd" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/live-query-observer-hydration-oracle.property.test.ts", + "sha256": "b2ae5fb670e0d52a6eec0821217d01698e71e9358d1ead71e38cc4e4bbdfe22b" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "52562ef41c12463593f8f9be5a39ace850515f8b49a22181d536c1f5b80cdc99" + }, + { + "path": "packages/db/tests/oracle-replay-manifest.ts", + "sha256": "18bfcba4421f3aab17e187921633082f13bc8500f95bb050109b99793e1505cf" + }, + { + "path": "packages/db/tests/oracle-replay.test.ts", + "sha256": "7bf2f6e2f73858bf9dd2b61b1e86fd382943b89aa6038ea1f0e453ffcfae08f5" + }, + { + "path": "packages/preact-db/src/useLiveQuery.ts", + "sha256": "6fc91226b13950bd033ba5e7955b4b4427e4c3be1445074cfee19d25d531b986" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "4ed56e4a554254bad590882ad12954a123ce178a6825d32fcd67927e49e35299" + }, + { + "path": "packages/react-db/src/useLiveSuspenseQuery.ts", + "sha256": "0e52d7f4a5f8644de1fdc2f959e2ab20209dcef3f43619b6377ed6db1b8d0836" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "ced00d99ca8c7bfb89efc78ca7369b2ef77ad3f669de4efdf63d91e8b05fa881" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "6e9e933c0d4778eb07761aac6aa6cafd954f0528349153fd61b8082a18bd52e6" + }, + { + "path": "packages/react-db/tests/suspense-lifecycle-oracle.test.tsx", + "sha256": "a2ae9626f8df30774db0f9be923bce7f7a4d7c8e244645a6177542a57cb84a5c" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "0fc239c55e3db14034ab075b9f93e2faadb60e267ec7449c2fe31d0abcf40e07" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "6750c9412be8995ebf942b2c4af2ad043917cc34fa2a88e4f17a77b944fad527" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "33bfce543dd29ee44f4485769af0021b76bc7e6649e3fc370877efdaee173b28" + }, + { + "path": "packages/svelte-db/tests/client-window-oracle.svelte.test.ts", + "sha256": "eb436bf1ae6fb5bda8d9a7a4c19463fbb7893c778348a6d84a71f1805c64c62c" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ] + }, + { + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "high-effort review repair on d8bf49f2e; source digest excludes report and manifest", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "reviewedHead": "d8bf49f2e2457dcd6da5977570e4bb83afd73eb4", + "pullRequest": "https://github.com/TanStack/db/pull/2348", + "combinedSha256": "d70fdf29acd5de0c9e24322f6a5e271a93e35d08ac1300657101c3be7781d657", + "files": [ + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "b0c5967274542cd1612742993e045d15126e0eef068550cde2975444e3579cb3" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "fec4e59f6ce4507df1819913963390977e2981f31b44c49a3055238f2dbfe663" + }, + { + "path": "docs/guides/live-queries.md", + "sha256": "a46cb2dba26ded881b62a2d94f335a786e06b3240209c6b3ceeedc4f23f41af2" + }, + { + "path": "packages/db/src/live-query-observer.ts", + "sha256": "ddfc273f9206ad97f7bfd0152fa953188fa92440b053777c1d90c8c23e06c885" + }, + { + "path": "packages/db/src/live-query-options.ts", + "sha256": "41e653166c7daf5d4d422be294f1a98c2d50fc9200042637032d2932c692e532" + }, + { + "path": "packages/db/tests/conformance/client-window-history-oracle.ts", + "sha256": "3a25d706ca2c25eb78679468ef3c2ee8485d8dbd1053e076bb817415c457a2a3" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "94fbf8fa38608747b10330c3a307887c73393b0a64ff2157b824a9ff2e54afb9" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "4d1b78fd610044d318a1cee7762930c7dece0b709df19b6bdc3322c252c0542f" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "f273af8197be4afd9cb4dad7fa17d07cdc15671b82f1704e0d7e12ea5cf0e702" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "21e167b8ef3d62a212c9bd9784a49eff34f18db8eac1fb2e70f1d7687b7289fd" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/live-query-observer-hydration-oracle.property.test.ts", + "sha256": "b2ae5fb670e0d52a6eec0821217d01698e71e9358d1ead71e38cc4e4bbdfe22b" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "497c18823374a7d0e769559f16499c7f90f76e2c99edf5fd6df7ace48a3ca6c0" + }, + { + "path": "packages/db/tests/oracle-replay-manifest.ts", + "sha256": "e0480f10c1861a7960bae3100f06673a5e978ee95ee646d2e9d30937dd421327" + }, + { + "path": "packages/db/tests/oracle-replay.test.ts", + "sha256": "7bf2f6e2f73858bf9dd2b61b1e86fd382943b89aa6038ea1f0e453ffcfae08f5" + }, + { + "path": "packages/preact-db/src/useLiveQuery.ts", + "sha256": "6fc91226b13950bd033ba5e7955b4b4427e4c3be1445074cfee19d25d531b986" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "63f89e146567e69cf7ee9d51f9ba1481c83aa9bbf09ff390abd00b2928bb2826" + }, + { + "path": "packages/react-db/src/useLiveSuspenseQuery.ts", + "sha256": "a72a250b94d6038b222c10c470ec37d3909f0ab12a92d9977c30a579c3c383a4" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "ced00d99ca8c7bfb89efc78ca7369b2ef77ad3f669de4efdf63d91e8b05fa881" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "10c13bc46724c093d5caf77e3f14bb4325604ca1016ff13b1f7c608b0dcc14b0" + }, + { + "path": "packages/react-db/tests/suspense-lifecycle-oracle.test.tsx", + "sha256": "3394c4501cd00f66afe9c1766dd689673a8e2c4eb263dd208020b99a758dbc12" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "138ce6f5153b03236cb1c8505d012ffea24b25f76f58961755911a0084c7a266" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "e63fb6d7a09d41a7d9641b8aa1b46c96de71a3790806ba58362dde26bb201e4e" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "33bfce543dd29ee44f4485769af0021b76bc7e6649e3fc370877efdaee173b28" + }, + { + "path": "packages/svelte-db/tests/client-window-oracle.svelte.test.ts", + "sha256": "a036b27849ede46ec6aa1bd5111552df9580e29b64607e492f0e080119cf5b5d" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ] + }, + { + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "CodeRabbit 5756985798 private startup failure repair on 2c605f92b; source digest excludes report and manifest", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "reviewedHead": "2c605f92bd26ce722448446f6102b5e19d7f6919", + "pullRequest": "https://github.com/TanStack/db/pull/2348", + "combinedSha256": "c970caa0f5dc74b6fbc04da7c1092cf57ea7d2ec2fe7c05cbf195534bf3f79f5", + "files": [ + { + "path": ".changeset/fix-client-pages-and-suspense-ownership.md", + "sha256": "e3a128c939fc118783be50ef03a0517de371d6aa2b25f5509125fff7889445dd" + }, + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "cb02e32e49b08f3dc7b3eb8c83292996a1fac308afaea2ff48b9bc3ac202fb30" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "fec4e59f6ce4507df1819913963390977e2981f31b44c49a3055238f2dbfe663" + }, + { + "path": "docs/guides/live-queries.md", + "sha256": "a46cb2dba26ded881b62a2d94f335a786e06b3240209c6b3ceeedc4f23f41af2" + }, + { + "path": "packages/db/src/live-query-observer.ts", + "sha256": "ddfc273f9206ad97f7bfd0152fa953188fa92440b053777c1d90c8c23e06c885" + }, + { + "path": "packages/db/src/live-query-options.ts", + "sha256": "41e653166c7daf5d4d422be294f1a98c2d50fc9200042637032d2932c692e532" + }, + { + "path": "packages/db/tests/conformance/client-window-history-oracle.ts", + "sha256": "3a25d706ca2c25eb78679468ef3c2ee8485d8dbd1053e076bb817415c457a2a3" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "94fbf8fa38608747b10330c3a307887c73393b0a64ff2157b824a9ff2e54afb9" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "4d1b78fd610044d318a1cee7762930c7dece0b709df19b6bdc3322c252c0542f" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "f273af8197be4afd9cb4dad7fa17d07cdc15671b82f1704e0d7e12ea5cf0e702" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "21e167b8ef3d62a212c9bd9784a49eff34f18db8eac1fb2e70f1d7687b7289fd" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/live-query-observer-hydration-oracle.property.test.ts", + "sha256": "b2ae5fb670e0d52a6eec0821217d01698e71e9358d1ead71e38cc4e4bbdfe22b" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "497c18823374a7d0e769559f16499c7f90f76e2c99edf5fd6df7ace48a3ca6c0" + }, + { + "path": "packages/db/tests/oracle-replay-manifest.ts", + "sha256": "e0480f10c1861a7960bae3100f06673a5e978ee95ee646d2e9d30937dd421327" + }, + { + "path": "packages/db/tests/oracle-replay.test.ts", + "sha256": "7bf2f6e2f73858bf9dd2b61b1e86fd382943b89aa6038ea1f0e453ffcfae08f5" + }, + { + "path": "packages/preact-db/src/useLiveQuery.ts", + "sha256": "6fc91226b13950bd033ba5e7955b4b4427e4c3be1445074cfee19d25d531b986" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "63f89e146567e69cf7ee9d51f9ba1481c83aa9bbf09ff390abd00b2928bb2826" + }, + { + "path": "packages/react-db/src/useLiveSuspenseQuery.ts", + "sha256": "4812eb283ea6a7ac922057a735942c733c979467710472ec427ba0a5cca285f6" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "ced00d99ca8c7bfb89efc78ca7369b2ef77ad3f669de4efdf63d91e8b05fa881" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "10c13bc46724c093d5caf77e3f14bb4325604ca1016ff13b1f7c608b0dcc14b0" + }, + { + "path": "packages/react-db/tests/suspense-lifecycle-oracle.test.tsx", + "sha256": "93af6316c3bdb9958975ddda39fa87bb88acfa4c69aa7509353f89b7346638ba" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "138ce6f5153b03236cb1c8505d012ffea24b25f76f58961755911a0084c7a266" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "e63fb6d7a09d41a7d9641b8aa1b46c96de71a3790806ba58362dde26bb201e4e" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "33bfce543dd29ee44f4485769af0021b76bc7e6649e3fc370877efdaee173b28" + }, + { + "path": "packages/svelte-db/tests/client-window-oracle.svelte.test.ts", + "sha256": "a036b27849ede46ec6aa1bd5111552df9580e29b64607e492f0e080119cf5b5d" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ] + }, + { + "baseHead": "1443dc92e5417394acbdc98b782caa9802848ce6", + "treeStatus": "Second high-effort review and query-local seed-health repair on3cb5df960; digest excludes report and manifest", + "algorithm": "sha256 of sorted UTF-8 path, NUL, file bytes", + "reviewedHead": "3cb5df960508663d6b6b4c1e4d28a37cdde6f9f9", + "pullRequest": "https://github.com/TanStack/db/pull/2348", + "combinedSha256": "26ab15ef15c4b4237977ec64c6f271f094a74aead8de855871af7f74fd0fa86f", + "files": [ + { + "path": ".changeset/fix-client-pages-and-suspense-ownership.md", + "sha256": "bb2841a86bccfbc4f2310ffa33e4e8f63918119ca1b2045684b59258906439a7" + }, + { + "path": "docs/contributing/oracle-coverage.md", + "sha256": "a02aafefe165ae5a00171ab1236077dd236055f7b1455e576fa9c5056cfd3587" + }, + { + "path": "docs/framework/react/overview.md", + "sha256": "fec4e59f6ce4507df1819913963390977e2981f31b44c49a3055238f2dbfe663" + }, + { + "path": "docs/guides/live-queries.md", + "sha256": "a46cb2dba26ded881b62a2d94f335a786e06b3240209c6b3ceeedc4f23f41af2" + }, + { + "path": "packages/db/src/live-query-observer.ts", + "sha256": "ddfc273f9206ad97f7bfd0152fa953188fa92440b053777c1d90c8c23e06c885" + }, + { + "path": "packages/db/src/live-query-options.ts", + "sha256": "41e653166c7daf5d4d422be294f1a98c2d50fc9200042637032d2932c692e532" + }, + { + "path": "packages/db/tests/conformance/client-window-history-oracle.ts", + "sha256": "3a25d706ca2c25eb78679468ef3c2ee8485d8dbd1053e076bb817415c457a2a3" + }, + { + "path": "packages/db/tests/conformance/contract.ts", + "sha256": "94fbf8fa38608747b10330c3a307887c73393b0a64ff2157b824a9ff2e54afb9" + }, + { + "path": "packages/db/tests/conformance/indexing-config-oracle.ts", + "sha256": "4d1b78fd610044d318a1cee7762930c7dece0b709df19b6bdc3322c252c0542f" + }, + { + "path": "packages/db/tests/conformance/indexing-reuse-oracle.test.ts", + "sha256": "f273af8197be4afd9cb4dad7fa17d07cdc15671b82f1704e0d7e12ea5cf0e702" + }, + { + "path": "packages/db/tests/conformance/window-history-oracle.ts", + "sha256": "e5b26430f3f6e1cf82f19e6a1e9fdedf4553810dd88baae731033be4de860e92" + }, + { + "path": "packages/db/tests/live-query-cache-options-oracle.ts", + "sha256": "8e8f873e57fcf448636ad54b9a45100602f1d7a2460f4fff3599b46555d0b37f" + }, + { + "path": "packages/db/tests/live-query-observer-hydration-oracle.property.test.ts", + "sha256": "b2ae5fb670e0d52a6eec0821217d01698e71e9358d1ead71e38cc4e4bbdfe22b" + }, + { + "path": "packages/db/tests/oracle-config.ts", + "sha256": "497c18823374a7d0e769559f16499c7f90f76e2c99edf5fd6df7ace48a3ca6c0" + }, + { + "path": "packages/db/tests/oracle-replay-manifest.ts", + "sha256": "e0480f10c1861a7960bae3100f06673a5e978ee95ee646d2e9d30937dd421327" + }, + { + "path": "packages/db/tests/oracle-replay.test.ts", + "sha256": "7bf2f6e2f73858bf9dd2b61b1e86fd382943b89aa6038ea1f0e453ffcfae08f5" + }, + { + "path": "packages/preact-db/src/useLiveQuery.ts", + "sha256": "6fc91226b13950bd033ba5e7955b4b4427e4c3be1445074cfee19d25d531b986" + }, + { + "path": "packages/react-db/src/useLiveQuery.ts", + "sha256": "5aab14a8d8f1b8bac879773029bad3b13c49a36ccefc23732c69178f5fe28a49" + }, + { + "path": "packages/react-db/src/useLiveSuspenseQuery.ts", + "sha256": "3a7dffba5ce2493ae190b4e82462c78c4e79233ddcb7299b2498d3d8f56d3366" + }, + { + "path": "packages/react-db/tests/client-window-oracle.test.tsx", + "sha256": "ced00d99ca8c7bfb89efc78ca7369b2ef77ad3f669de4efdf63d91e8b05fa881" + }, + { + "path": "packages/react-db/tests/conformance.test.tsx", + "sha256": "4e713186cecf31d8c105f59edacfc5fbc52553c148a2ec67a916dced3664ff27" + }, + { + "path": "packages/react-db/tests/source-id-reuse-oracle.test.tsx", + "sha256": "cd8dec3db947c3f688947ce1cae95a33e8564614d68baa5b40a18793b46b4207" + }, + { + "path": "packages/react-db/tests/suspense-indexing-config-oracle.test.tsx", + "sha256": "10c13bc46724c093d5caf77e3f14bb4325604ca1016ff13b1f7c608b0dcc14b0" + }, + { + "path": "packages/react-db/tests/suspense-lifecycle-oracle.test.tsx", + "sha256": "6abf1ed0fbf08e903b1389d13264d9b2640122688129ca335b6f5dc4281f0bc6" + }, + { + "path": "packages/react-db/tests/suspense-local-options-oracle.test.tsx", + "sha256": "138ce6f5153b03236cb1c8505d012ffea24b25f76f58961755911a0084c7a266" + }, + { + "path": "packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx", + "sha256": "f4a365a23a04282cfab3018d121142040d52070f1de2c8a5ba9536423cd14a6d" + }, + { + "path": "packages/react-db/tests/window-render-oracle.test.tsx", + "sha256": "b63171cba5903d9869390ccffc8861d83413dec0ef52e159306c4af44a7c3769" + }, + { + "path": "packages/svelte-db/src/useLiveQuery.svelte.ts", + "sha256": "33bfce543dd29ee44f4485769af0021b76bc7e6649e3fc370877efdaee173b28" + }, + { + "path": "packages/svelte-db/tests/client-window-oracle.svelte.test.ts", + "sha256": "a036b27849ede46ec6aa1bd5111552df9580e29b64607e492f0e080119cf5b5d" + }, + { + "path": "packages/svelte-db/tests/conformance.svelte.test.ts", + "sha256": "df98bf808e5ecc51962387440062d8763d43c6e5e1ce538d7a2d09cd01d046b2" + } + ] + } + ] +} diff --git a/docs/contributing/oracle-reviews/2026-11-03-client-suspense-page-ownership.md b/docs/contributing/oracle-reviews/2026-11-03-client-suspense-page-ownership.md new file mode 100644 index 0000000000..142405709a --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-03-client-suspense-page-ownership.md @@ -0,0 +1,2144 @@ +# Client-backed pages and independent Suspense outputs + +[Issue #2345](https://github.com/TanStack/db/issues/2345), evaluated on +2026-11-03. Base and current HEAD: `1443dc92e5417394acbdc98b782caa9802848ce6`. +This record concerns the uncommitted working tree identified by the companion +[source manifest](2026-11-03-client-suspense-page-ownership-manifest.json). +No PR exists; PR readiness and remote CI are **not assessed**. + +## Law, authority, and enforcement + +A page is the ordered view selected by the outer offset and limit. The shared +adapter contract owns its meaning. DbClient and Suspense are receiving modes; +they do not require a framework-specific page model. + +The 2026-10-30 decision for #2291/#2316 retained a binding's Collection on a +finite outer-page change, but explicitly excluded client-backed and Suspense +paths. R1 therefore identifies an unsuitable limit of that accepted contract. +The maintainer extended it in this task and explicitly directed removal of +implicit mutable Suspense Collection sharing. The contract, executable owners, +coverage map, and React overview now record that decision. It supersedes the +compatible-sharing expectation in the earlier local-option/indexing decision. +It does not retire DbClient preload admission rules. + +| Law and authority | Previous enforcement gap | Repair and decisive comparison | +| --- | --- | --- | +| Equal query-input bindings own distinct derived Collections; a supplied Collection can be shared explicitly. Maintainer decision, 2026-11-03. | Suspense intentionally reused successful outputs. Ordinary peer page histories were absent. | Shared independent-peer histories compare both outputs through declarative/imperative moves, source writes, and retirement. Successful Suspense results claim exclusive ownership of retry resources. | +| Invalidated inputs that differ only in finite outer offset/limit retain the binding's Collection and upstream subscriptions, including DbClient and Suspense. Same decision extends #2291. | Shared histories had no client/Suspense receiving modes. Svelte had the same exclusion. | React ordinary/Suspense × client/no-client and Svelte client drivers execute the same page owner. Changed plans still replace Collections. | +| Speculative renders cannot move the committed page or acquire its proposed rows. Existing shared adapter contract. | React receiver omitted client and Suspense modes. | Actual React suspended, cancelled, and committed cuts reuse the shared row/identity comparison and check acquisition counts before commit. | +| A successful Suspense binding cannot subsequently adopt another binding's output. Initial retry progress remains owed across discarded refs. Maintainer ownership decision plus existing Suspense loading/error contract. | Removing a cache entry only at success leaves a mounted suspended binding holding an entry subsequently claimed by a peer. | A mounted replacement is blocked before its hook; a peer becomes ready first; the replacement resumes and moves independently. The `missing-peer-claim` mutant fails there. | +| Hook page lookup and preload/hydration use the same page-specific identity; old transported snapshots retain their meaning. Existing SSR contract. | React keyed hooks omitted the outer page from their transport hash. | JSON server-preload receiver compares first/moved seeds before local readiness, local handoff, peer isolation, return, immutable server snapshots, and a same-source client switch. | +| An unchanged explicit query key retains the authored input. Existing shared comparing-identity contract. | Svelte client driver was absent, concealing re-preparation of stable-key input. | Shared stable-key histories now receive Svelte DbClient; changing options/input behind the same key cannot change the authored query. | + +The source registry is not a registry of ordinary hook Collections. Re-registering +a moving local Collection is unnecessary. Each binding retains its Collection; +a client-backed move creates an observer for the requested page's transport +hash. That preserves old preload/hydration snapshot identities. + +Suspense retains a short-lived cache of **unclaimed preloads** because React can +discard hook refs before commit. Returning a ready result claims that Collection +and removes it from retry lookup. A resolver ownership token follows an adopted +compiled plan; a peer claim forces a fresh output. The existing failure retry +and abandoned-preload cleanup remain exercised. + +## Experiment order and RED evidence + +The append-only scratch ledger was created from the raw issue before tests or +edits. R1/R3 were grouped around adapter reuse and R2 around transport identity. +The owner and receiving paths were extended before production changed. After +the maintainer expanded the scope, the Suspense ownership and Svelte receiving +extensions also ran before their production changes. + +| Preproduction experiment | Result on unchanged implementation | Saved log in `/private/tmp/` | +| --- | --- | --- | +| React render receiver, original client extension | 24 no-client cells passed; 72 provider/explicit/descriptor cells failed retention. Speculative acquisition neighbors were observed. | `2345-complete-red.log` | +| Corrected JSON page receiver | 6 failed, 2 passed: ordinary derived retention failed at moved seed; keyed ordinary/Suspense lookup failed at first seed. | `2345-client-red-valid.log` | +| Expanded ownership/options contract | 272 failed, 1,153 passed; independence and page retention rejected. | `2345-suspense-contract-red.log` | +| Real React Suspense/render and shared page receivers | 124 failed, 421 passed. | `2345-suspense-receiving-red.log` | +| Mounted replacement resumes after peer readiness | All 8 campaigns failed exclusive output identity at `resumed binding owns its output`. | `2345-retry-owner-red.log` | +| Svelte client receiver | 18 failed, 6 passed; 130 unrelated tests unselected. Retention and stable-key rows failed. | `2345-svelte-receiving-red.log` | + +Early missing-import/selector errors were repaired before production edits and +are not RED evidence. A cache-deletion simplification failed async initial +progress; those interrupted experiments are not completed RED campaigns. + +## Grammar, model, paths, and checkpoints + +The primary owner remains +`packages/db/tests/conformance/window-history-oracle.ts`. Its independent rule +sorts, filters, and slices complete numeric source rows. It contains no production +hash, graph, cache, resolver, or incremental classifier. Explicit authored-plan +fields distinguish page changes from plan changes. Public ordered rows, keyed +map values, Collection identity, upstream owner continuity, and subscription +count transitions reach comparisons after each driver's native flush. + +The shared grammar retains direct/nested queries; derived/changing/stable/opaque +keys where supported; empty rows, ties, zero widths, jumps beyond the end; +filter/order/projection/nested-window/source/Collection-option changes; and source +updates and growth after reuse. Sources contain up to 12 rows with ranks 0–20; +ordinary offsets are 0–12 and widths 0–6, with compulsory adjacent moves and +safe-integer maximum witnesses. It excludes non-finite bounds from its claim. +A stable key is deliberately not interpreted as a new authored page. + +Pending histories hold real subset acquisitions: initial page move before +readiness, obsolete settlement while the latest stays pending, initial readiness, +overlapping moves, obsolete settlement, final latest rows, and unmount while +pending. Before an initial Suspense result succeeds, the receiver checks actual +fallback and absence of a returned Collection. Retention begins at first success. +Publication-callback histories require actual callback entry before changing the +parameter and check the retained Collection after the native host settles. + +New peer histories construct two equal declarations, move one declaratively and +imperatively, update their shared source, retire one, and move its surviving peer. +Widths 1–3 and jumps 1–4 vary independently. These histories run in all six +adapters; React adds ordinary/Suspense/client modes and Svelte adds client mode. + +React's render receiver crosses ordinary/Suspense, no-client/provider/explicit/ +descriptor scope, eager/on-demand, keyed/derived, and offset/limit/both moves. +Every cell reaches suspended/cancelled/committed cuts. A separate retry schedule +crosses client/no-client and keyed/derived ownership after peer-first readiness. +The JSON receiver crosses keyed/derived and ordinary/Suspense hooks with source +counts 1–12, starting offset 0–4, width 1–4, jump 1–4. It compares server-seeded +rows before source readiness, rather than waiting until local rows hide lookup +errors. Client switching uses the same concrete source to distinguish client +scope from source identity. + +Reconstruction: compulsory moves reproduce R1; imperative peer moves reconstruct +R3; seeded first/moved pages and retained server snapshots reconstruct R2's +identity concern and sibling S1. Svelte stable-key histories reconstruct S2. +The ownership retry schedule reconstructs the peer-first counterexample that a +simple cache eviction misses. The issue's exact heavy grouped-join timing +benchmark was not rerun; the reusable admission defect is reproduced directly. + +Ablation: removing client modes loses R1/S2, keyed seeds lose S1, held readiness +loses the seed/local distinction, peer actions lose mutable-output isolation, +client switch loses scope separation, and speculative cuts lose precommit +mutation/acquisition checks. Offset/limit/both and zero/large bounds distinguish +narrow page patches. Removing changed-plan and stable/opaque controls would +incorrectly admit retention where the contract requires recompilation or keeps +the authored query. Negative/non-finite bounds and arbitrary external schedules +are outside this grammar; it does not classify them as successful page moves. + +The model retains independent authored inputs, source rows, and prior output +identities because a next move or source write distinguishes each. It has no +retry-cache state. “Peer” is a second framework binding over the same source; +“page” maps to the glossary's outer window. No speculative hook object identity +is promised before initial success. + +## Mutation calibration and replay + +Every mutant below failed by the intended assertion, with shrinking preserving +the named law and checkpoint. None was counted from a timeout or setup failure. +All mutations were restored before final verification. + +| Mutant | Rejected observation | +| --- | --- | +| Restore React client reuse exclusion | `outer window retains Collection`, moved preloaded page before local readiness | +| Keep the old observer on a client page move | `ordered window rows`, moved preloaded page before local readiness | +| Permit retention across clients | `changed plan replaces Collection`, same-source client scope switch | +| Restore old keyed hash | `ordered window rows`, first preloaded page render | +| Keep claimed Suspense outputs in retry lookup | Independent peer Collection identity before page move | +| Remove peer-claim invalidation | Independent resumed output after peer-first readiness | +| Restore Svelte client reuse exclusion | Collection retention at input 1 | +| Re-prepare Svelte stable-key input | Ordered rows at input 1 | + +Representative commands: + +```sh +pnpm --filter @tanstack/react-db exec vitest run tests/client-window-oracle.test.tsx -t 'derived/ordinary/fixed' --coverage.enabled=false +pnpm --filter @tanstack/react-db exec vitest run tests/window-render-oracle.test.tsx -t 'retry ownership/client/derived/fixed' --coverage.enabled=false +pnpm --filter @tanstack/svelte-db exec vitest run tests/conformance.svelte.test.ts -t 'window history :: svelte \(DbClient\).*stable/direct window histories \(fixed\)' --coverage.enabled=false +``` + +Checked direct replay with the client-exclusion mutant: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=react.client-window TANSTACK_DB_ORACLE_SEED=2345 TANSTACK_DB_ORACLE_PATH=0 pnpm --filter @tanstack/react-db exec vitest run tests/client-window-oracle.test.tsx -t 'derived/ordinary/fresh' --coverage.enabled=false +``` + +It ran directly and rejected the same moved-seed retention law. Fixed and fresh +campaigns use the same property/driver/comparison and budget. Fixed seeds are +2291 for shared/render campaigns and 2345 for client/peer/retry campaigns. +No oracle run budget was lowered or removed. + +## Lossless issue ledger + +Raw source: the issue body, no comments as retrieved on 2026-11-03. Reported +revision: rc.1.2 / `1443dc92e`. The issue supplies a Bun 1.4.0 / React 19.3.1 / +happy-dom production benchmark with 5,000 orders, 20,000 items, grouped sum plus +join/filter, width 20 and ten page changes over three runs. + +| ID | Original claim and remedy | Technical verdict and action | Durable value/destination | +| --- | --- | --- | --- | +| R1 | `forSuspense \|\| dbClient` prevents previous-Collection reuse; provider/Start and descriptor paths rebuild every page. Reported 11 Collections vs 1, 113–120 ms vs 5.5–5.7 ms. Expected automatic setWindow reuse. | Functional claim confirmed by preproduction retention failures; **fixed-now** under maintainer's contract extension. Numeric timing and exact workload were not independently measured. | Shared client/Suspense mode registration, real React descriptor/provider/explicit cuts, and Svelte client receiving path. | +| R2 | Registry hash includes outer page and is used by preload/dehydrate/hydrate. Suggest re-registering the moved Collection under the new hash. | Identity concern valid; re-registration is unnecessary because ordinary outputs are local. **accepted-design**: preserve snapshots and rebind the observer. S1's separate hash mismatch is repaired below. | JSON seeded-page histories, immutable server snapshots, peer independence, and same-source client switch. | +| R3 | A stable key plus layout-effect imperative setWindow retains one client-backed Collection; window engine works and adapter admission is missing. Reported 5.6–6.5 ms. | Imperative behavior confirmed by shared client peer histories; **accepted-design**. Timings not independently measured. Automatic invalidated-page reuse repaired under R1. | Declarative and imperative moves judged by the same model; stable-key authored-input control retained. | +| S1, discovered sibling | React keyed hook hash omits outer page, missing keyed preload seeds. | **fixed-now**; first-seed RED and keyed-hash mutant reject the defect. | React JSON page receiver; canonical public transport hash on invalidation. | +| S2, discovered sibling | Svelte DbClient re-prepares a stable-key authored query. | **fixed-now**; shared stable-key RED and mutant fail ordered rows. | Shared stable-key client driver, retained authored prepared input. | +| U1, maintainer extension | Independent Suspense calls must not share mutable derived outputs. | **fixed-now**; ownership/peer-first RED and both sharing mutants reject. | Shared peer owner, local-options/indexing ownership comparisons, real React retry schedule. | +| U2, maintainer extension | Svelte already supports DbClient; future adapters should receive the same law. | **fixed-now** for current Svelte/React modes; portable shared contract and driver API for future implementations. | Shared page owner, no React-specific expected page model. | + +Raw accounting: 3 items = 1 fixed-now + 2 accepted-design; no dropped items, +refutations, deferrals, unresolved product decisions, or confirmed-open functional +items. Siblings/extensions are separate from the three raw items. Exact timing +claims remain unverified measurements and are not used as a performance promise. +The final loss audit returned to the raw body and reconciled the code fragment, +benchmark, workaround, expected behavior, registry hypothesis, and proposed fix. + +Reviewer assessment: the reporter found the exact admission guard and supplied +an unusually useful reproducible benchmark. Accuracy and signal were high; +priority was appropriate. The proposed registry repair was a hypothesis rather +than a demonstrated necessity. They missed Svelte, keyed seed lookup, and mutable +Suspense ownership. **Lean hire for debugging**, based on this narrow sample; +this issue alone does not establish broader review or engineering performance. + +## Oracle rubric outcomes + +| Requirement | Outcome and evidence | +| --- | --- | +| ORC-001 | Law authority is recorded above and beside the owners. User-approved extensions are distinct from existing seed/key/scheduling obligations. | +| ORC-002 | Independent authored-plan comparison and full sort/filter/slice; no resolver/hash/cache machinery computes expected output. | +| ORC-003 | Opening/local prose identifies law, model, grammar, production entry, and comparisons. Receiving drivers name the shared judgment. | +| ORC-004 | Reconstruction, ablation, finite range, and exclusions are recorded above. Known heavy-workload timings are explicitly outside the executed evidence. | +| ORC-005 | Actual six-framework paths, React Suspense fallback and render cuts, Svelte client, seeded first render, and per-path reach. Before local readiness the receiver supplies the exact seed; later rows do not stand in for earlier lookup. | +| ORC-006 | Eight intended assertion failures and checked replay, classified above. Core indexing comparison also rejects oversharing. | +| ORC-007 | Fixed/fresh campaign parity; checked seed/path replay. No `fc.commands` grammar, so command replayPath is inapplicable. | +| ORC-008 | Only authored source/input/prior identity is modeled; next move/write distinguishes it. No production cache state was copied into the model. | +| ORC-009 | Framework binding, Collection, source, readiness, window, and checkpoint use glossary meanings; page/peer mapping is explicit. | +| ORC-010 | Existing failure-fidelity wrapper and ScenarioLifetime preserve primary law/cut and distinguish cleanup errors. Mutation shrink/replay retained the intended failures; driver-scoped clients are cleaned after the suite. | +| ORC-011 | No concrete semantic fault shared by the scalar model and production was identified. A second formulation is not triggered; agreement alone is not claimed as evidence. | +| ORC-012 | This versioned record, source manifest, reach, ledger, and limits identify the exact evaluated tree and all numbered outcomes. | +| ORC-013 | Offset-only/limit-only/both; zero/large bounds; stable/changing/opaque identities; peer-first retry; and same-source client switches distinguish nearby wrong boundaries. | +| ORC-014 | Real framework drivers receive controlled source/acquisition premises and share the same judgment. JSON transport uses real DbClient/server/browser-hook paths. No real Electric, hydrateRoot, or network transport claim follows. | +| ORC-015 | Declared reach fails on missing states and is measured below separately per receiving mode/campaign. | +| ORC-016 | Generated/fixed receivers use the shared model/checker. Supplemental lifecycle examples stay in the ordinary uncommitted-render test file. | + +## Limits and code weight + +Evidence bounds closure to the stated finite history × actual adapter mode × +public checkpoint domain. It does not prove all schedules, arbitrary query +operators, non-finite bounds, the original elapsed-time benchmark, actual Start +routing, hydrateRoot, or a live Electric service. Core acquisition/window and +materialization owners retain those contracts; no files under `query/live/` +changed. No known reachable violation remains in the exercised domain. + +Three existing lifecycle examples required sharing. They now preserve async +first-load retry, independent later success, surviving-peer liveness, and source +cleanup obligations. An independent later query must receive a legal completion +from its source; the old fixture's never-completing second request cannot prove +that promise. Already-loaded demand now completes synchronously with `true`. + +`pnpm size:compare --base 1443dc92e --exact-base --packages react-db` measured +React's production package at 4,297 → 4,404 gzip bytes (**+107**), brotli **+99**. +The typical tree-shaken app grew 96,397 → 96,632 gzip bytes (**+235**), brotli +**+132**. Independent ownership and canonical transport identity account for +the added shipped state. Removing the retry cache caused initial async hooks +to keep suspending; that simplification was rejected. Redundant stream-identity +state was removed instead. + +The size script assumes Vite entry points and cannot build Svelte's package +format (`index.html` missing). Both Svelte trees were therefore built with their +normal `svelte-package` command, compiled with the same production Svelte +compiler, then bundled/minified with esbuild and dependencies external. Package +size changed 2,367 → 2,431 gzip bytes (**+64**) and 2,144 → 2,203 brotli bytes +(**+59**). That added state keeps client scope and stable-key authored input. +Neither comparison counts test or report text as shipped bytes. + +## Measured reach + +Counts below are histories entering each declared state, not action totals. +The normal fixed and fresh campaigns both include the owner's fixed witnesses. +Minimum reach remains one per declared state, with fixed-example reach required. +React/Svelte detail comes from runs with `--disableConsoleIntercept` so passed +test output is retained. Other adapters' peer-only runs collect the same counters. + +| Receiver | Campaign cells | Reach for every declared state in each cell | +| --- | --- | --- | +| render | 192 | 7 histories per state per cell | +| client window | 8 | 7 histories per state per cell | +| retry | 8 | 7 histories per state per cell | +| peer | 22 | 7 histories per state per cell | +| pending | 14 | 7 histories per state per cell | + +Render states: suspended move, cancelled move, committed move. The 192 cells +are all ordinary/Suspense × four client scopes × eager/on-demand × keyed/derived +× offset/limit/both × fixed/fresh. Client states: first seed, moved seed, local +handoff, peer isolation, returned page, client scope switch, in all eight +keyed/derived × ordinary/Suspense × fixed/fresh cells. Retry states: replacement +suspended, peer ready first, replacement resumed, page isolated, in all eight +client/no-client × keyed/derived × fixed/fresh cells. + +Peer states: independent outputs, declarative isolation, imperative isolation, +source write isolation, peer survives retirement. Each listed mode reached all +five states seven times **in each** of its fixed and fresh campaigns: + +- `angular` +- `preact` +- `react` +- `react (DbClient)` +- `react (dependency list)` +- `react Suspense` +- `react Suspense (DbClient)` +- `solid` +- `svelte` +- `svelte (DbClient)` +- `vue` + +Pending states: initial loading move, initial oldest settled while latest pending, +initial ready, overlap pending, oldest settled while latest pending, settled +latest, unmounted pending. The modes are React ordinary, React dependency-list +registration, React Suspense, React DbClient, React Suspense DbClient, Svelte, +and Svelte DbClient, each fixed/fresh. The dependency-list name is a retained +registration/replay label, not another current production API path. + +All 100 normal window campaign cells reached every declared state 13 times, +except stable-key source writes listed below. Window cells are direct/nested × +applicable derived/changing/stable/opaque forms × fixed/fresh: 16 per React mode +and Svelte DbClient; four for reactive Svelte. State sets are explicit in the +owner: derived includes page/plan/option changes, writes/growth; changing omits +option changes; nested adds nested-window changes; opaque omits page-retention +claims and adds opaque-key change; stable declares stable-key and source-write +only. Every stable-key state reached 13 histories. Writes are conditional on +available modeled rows; exact source-write counts follow. + +| Stable-key receiving cell | Source-write histories | +| --- | --- | +| react Suspense/stable/direct/fixed | 13 | +| react Suspense/stable/direct/fresh | 13 | +| react Suspense/stable/nested/fixed | 13 | +| react Suspense/stable/nested/fresh | 11 | +| react (DbClient)/stable/direct/fixed | 13 | +| react (DbClient)/stable/direct/fresh | 13 | +| react (DbClient)/stable/nested/fixed | 13 | +| react (DbClient)/stable/nested/fresh | 12 | +| react Suspense (DbClient)/stable/direct/fixed | 13 | +| react Suspense (DbClient)/stable/direct/fresh | 9 | +| react Suspense (DbClient)/stable/nested/fixed | 13 | +| react Suspense (DbClient)/stable/nested/fresh | 12 | +| react/stable/direct/fixed | 13 | +| react/stable/direct/fresh | 12 | +| react/stable/nested/fixed | 13 | +| react/stable/nested/fresh | 13 | +| react (dependency list)/stable/direct/fixed | 13 | +| react (dependency list)/stable/direct/fresh | 9 | +| react (dependency list)/stable/nested/fixed | 13 | +| react (dependency list)/stable/nested/fresh | 9 | +| svelte (DbClient)/stable/direct/fixed | 13 | +| svelte (DbClient)/stable/direct/fresh | 13 | +| svelte (DbClient)/stable/nested/fixed | 13 | +| svelte (DbClient)/stable/nested/fresh | 10 | + +Every publication-callback campaign reached callback entry and callback update +settlement in nine histories. Offset/limit request reach varies with the chosen +move; exact counts are below. Each declared state also has a fixed witness. + +| Mode/campaign | Offset request | Limit request | Entry and settlement, each | +| --- | --- | --- | --- | +| react Suspense/fixed | 6 | 5 | 9 | +| react Suspense/fresh | 5 | 7 | 9 | +| react (DbClient)/fixed | 6 | 5 | 9 | +| react (DbClient)/fresh | 6 | 5 | 9 | +| react Suspense (DbClient)/fixed | 6 | 5 | 9 | +| react Suspense (DbClient)/fresh | 6 | 6 | 9 | +| react/fixed | 6 | 5 | 9 | +| react/fresh | 5 | 5 | 9 | +| react (dependency list)/fixed | 6 | 5 | 9 | +| react (dependency list)/fresh | 7 | 5 | 9 | +| svelte (DbClient)/fixed | 6 | 5 | 9 | +| svelte (DbClient)/fresh | 5 | 6 | 9 | +| svelte/fixed | 6 | 5 | 9 | +| svelte/fresh | 8 | 7 | 9 | + +## GREEN and closeout checks + +| Check | Result | +| --- | --- | +| Full React package: `pnpm --filter @tanstack/react-db exec vitest run --coverage.enabled=false` | 39 files, 4,729 tests passed; no type or unhandled errors (`2345-react-all-green-two.log`) | +| Full Svelte package, same Vitest command/filter | 15 files, 532 tests passed; no type errors (`2345-svelte-all-final.log`) | +| React affected owner/receiving campaign | 1,669 passed before supplemental lifecycle example updates (`2345-exclusive-green-two.log`) | +| Core indexing checker calibration | 245 passed; no type errors (`2345-indexing-checker-green.log`) | +| Preact conformance | 138 passed | +| Vue conformance | 133 passed | +| Solid conformance | 97 passed | +| Angular conformance | 88 passed | +| Per-adapter peer reach reruns | 2 tests each passed in Preact/Vue/Solid/Angular; unrelated tests unselected | +| Svelte production package build, baseline and repaired trees | Both passed | +| Svelte `test:types` | 0 errors, 0 warnings | +| React explicit TypeScript check | Passed before supplemental lifecycle update; final full Vitest typecheck also passed | + +The first complete React attempt identified three obsolete sharing expectations. +A later test-fixture edit returned `undefined` where the typed subset API requires +`true`; its run had passing runtime assertions but unhandled type errors. It is +not GREEN evidence. The corrected final run above exited cleanly. Existing +non-fatal source-cleanup/index advice and React act warnings in some controlled +receivers do not supply product evidence; declared reach and comparisons do. + +Final Svelte conformance rerun after driver-client cleanup: 154 tests passed, +no type errors (`2345-svelte-conformance-last.log`). Full explicit ESLint on all +changed TypeScript files passed; the last supplemental lifecycle edit also +passed ESLint. Final `pnpm lint:changed` exited 0, and `git diff --check` passed. +All 18 source-manifest hashes matched the tested tree at closeout. No changeset, +commit, follow-up push, or PR was created. + + +## Production simplification and focused safety review, 2026-11-03 + +The maintainer requested net-negative production code after the repair. This +append-only entry supersedes the earlier code-weight measurements for the +current source. The earlier evidence remains historical. Its manifest digest +is `1972e2bfdc64b61c11996f50db5c0afc5433cdb50b028220a1d59166dccb1e55`; +the companion manifest preserves that complete snapshot in `priorManifests`. +The current manifest adds `useLiveSuspenseQuery.ts`, for 19 source files. + +The accepted laws, independent models, history grammars, comparison checkpoints, +and oracle budgets did not change during this simplification. No test +expectations were changed. This is a focused self-review of the cuts, not a +new independent audit or an expansion of the original closure boundary. + +### Structural reductions + +- Move Suspense retry resources into `useLiveSuspenseQuery.ts`. The ordinary + hook takes a type-only binding dependency, so ordinary apps omit the cache. + One Suspense binding holds the previous output, readiness and preload promise + in place of three separate refs plus ownership state in the ordinary hook. +- Remove the identity-error catch, stored field and wrapper metadata. Derived + preparation already requires a stable identity; keyed inputs hash at their + resolution boundary. The later streaming fallback could not receive a valid + input that these earlier gates rejected. +- Hash only when query identity invalidates the authored input. Existing stable + explicit-key behavior remains authoritative. +- Replace three cache status listeners and their remover array with one + `status:change` listener. Keep first-load failures through React's immediate + retry. Clear a failed preload at the wrapper's common error boundary. +- Remove the forwarding hook and single-use cache-map lookup helper. Share the + absent-client scope token between retry resources and first-load errors. +- Use one previous-resolution record in Svelte instead of parallel Collection, + client and keyed-input records. Delegate disabled-query resolution to the + resolver, while preserving supplied-Collection ownership. + +The retry cache itself remains necessary: its earlier removal prevented async +first mounts from progressing after React discarded their refs. A first +simplification reduced the React package by 89 gzip bytes but grew the typical +app by 165; moving retry machinery to the Suspense module resolved that cost. +A proposed Svelte hash-assignment consolidation grew its gzip/brotli size by +4/8 bytes and was dropped. An owned-output early return was also dropped: +using the common resolution path updates the retry resource when its output +changes, instead of retaining the old resource throughout the binding lifetime. + +### Final shipped weight against the original base + +React command: +`pnpm size:compare --base 1443dc92e --exact-base --packages react-db`. +Svelte uses the same production compilation/bundling method described above. +Tests and contract/review text are not included in these production measurements. + +| Bundle | Base gzip | Current gzip | Gzip difference | Brotli difference | +| --- | ---: | ---: | ---: | ---: | +| React package, all exports | 4,297 | 4,253 | -44 | -56 | +| Svelte package, all exports | 2,367 | 2,410 | +43 | +34 | +| Sum of separately compressed adapter packages | 6,664 | 6,663 | -1 | -22 | +| Typical tree-shaken React app | 96,397 | 96,244 | -153 | -202 | + +The adapter-package sum is almost unchanged; it is not one app bundle. Svelte's +remaining growth pays for client scope and stable-key authored-input retention. +Every other measured React app scenario also shrank (158–187 gzip bytes). +Relative to the initial successful repair, the two package bundles together +shrank by 172 gzip bytes. No comment or identifier shortening supplied a cut. + +### Which oracles protect the cuts + +The existing owners were extended for the original fix before production was +changed; unchanged older receiving paths alone did not cover the reported bug. +The simplification ran those same extended owners without expectation changes. + +| Obligation | Instrument and observations | Evidence strength and limits | +| --- | --- | --- | +| Page reuse, changed-plan/client replacement, independent peers | Shared page sort/filter/slice model; React ordinary/Suspense and React/Svelte client receivers. Compare ordered rows, output identity and retained upstream subscriptions. | Generated fixed/fresh histories plus eight rejected semantic mutants. Finite pages/operators and controlled source delivery only. | +| No work or page mutation from an abandoned render; peer-first retry ownership | Actual React suspended/cancelled/committed render receiver with shared comparison. | The stale-ownership mutant fails after the peer renders first. It does not establish every concurrent React schedule. | +| Page-specific preload and hydration identity | JSON-transported server preload receiver, before local readiness and through local handoff/client switch. | Restoring the old keyed hash or observer fails at the corresponding public row checkpoint. No actual Electric, Start router or hydrateRoot claim. | +| Earlier identity rejection makes the deleted fallback redundant | Existing source-ID/deferred-release grammar, unhashable IR through the public Suspense hook, mount/rerender and four later readers. | Bypassing `identifyLiveQuery` with a constant identity makes all eight selected histories render `Query` instead of the required error boundary. Restoring production passes all eight. This is a bounded input grammar, not a proof over every JavaScript input. | +| Initial loading/error progress and cleanup | Full Suspense lifecycle examples, source-release grammar, stream-render receiver and pending peer histories. | Runtime assertions pass. Error-cache retirement/reset combinations have less generated-history coverage than page ownership; no comprehensive lifecycle-schedule claim follows. | + +The last row is a coverage limit, not a demonstrated production counterexample. +The status-listener consolidation preserves the same Collection status cases; +no new lifecycle behavior was introduced. The earlier coverage-map limits, +including held commits beyond the GC floor, remain in force. + +### Recalibration after relocation + +All eight original semantic mutants were retargeted to the simplified source +and rejected at their intended comparisons. Each edit was restored in `finally`. +None was counted from a setup failure, timeout or unrelated crash. + +| Mutant edit | Observed rejected law/checkpoint | +| --- | --- | +| Add `!dbClient` to React's previous-Collection admission | Collection retention, moved preloaded page before local readiness | +| Remove `!dbClient` from the observer-preservation condition | Ordered rows, moved preloaded page before local readiness | +| Remove client equality from previous-Collection admission | Changed-plan replacement, client scope switch | +| Replace keyed `getLiveQueryHash` with key-only `getStableValueHash` | Ordered rows, first preloaded page render | +| Disable successful `suspense.claim()` | Independent output identity, peer before move | +| Remove `suspense.ownedByPeer` invalidation | Independent output identity, resumed binding owns its output | +| Add `!dbClient` to Svelte's previous-Collection admission | Collection retention, input 1 | +| Disable Svelte's stable-key comparison | Ordered rows, input 1 | + +The checked direct replay still rejects the original retention failure with +property `react.client-window`, seed `2345`, shrink path `0`. The upstream +validation calibration additionally replaced +`identityDeps: [`derived`, identifyLiveQuery(preparedValue, scope)]` with +`identityDeps: [`derived`, `incorrect-constant-identity`]`. The selected existing +source-release grammar rejected successful rendering in all eight cases, +with no type errors. That mutant attacks the upstream gate; it does not claim +that the deleted downstream fallback itself was reachable. + +Commands and exact mutant edits are retained in the task-local +`2345-simplify-mutants.json` and `2345-simplify-mutant-upstream-identity.json`. +The original RED evidence and measured reach above retain their original run +identities; no new reach count is inferred from aggregate test totals. + +### Verification on the simplified production + +- Full React package: 39 files, 4,729 tests passed, no type errors + (`2345-simplify-react-closeout.log`). +- Full Svelte package: 15 files, 532 tests passed, no type errors on the final + Svelte source before the rejected hash experiment + (`2345-simplify-svelte-final.log`). That experiment also passed the full suite + and was restored exactly; it supplied no necessary correctness behavior. +- After all mutants were restored, the React page/preload/render receivers + passed all 208 tests, and the targeted earlier-identity-rejection grammar + passed all eight selected cases. +- Both production package builds passed. Svelte `test:types` reported zero + errors and warnings. Explicit ESLint and `pnpm lint:changed` passed. + +The work remains uncommitted, with no new push or PR. The report's earlier +controlled-boundary limits still apply. No model, comparison or budget was +relaxed to obtain these results. + +## Prep review: finding evaluation and additional enforcement + +This entry follows the earlier records; it does not replace their experiments. +Review base and HEAD remain `1443dc92e5417394acbdc98b782caa9802848ce6`. +The reviewed implementation was uncommitted. Two independent review roles ran +in parallel: a simplifier and a coordinator with five read-only review angles. +Their aggregate had nine candidate findings and six explicitly rejected +simplification ideas. Sixteen contextual nonfindings are accounted for below. +The review-input manifest pinned 21 paths, including this record and its +companion manifest. The new source manifest below identifies the later tree. +No review agent changed code or independently ran the author's experiments. + +Before tests or edits, the evaluator preserved both complete raw reports in +an append-only task-local ledger, `2345-prep-evaluation-ledger.md`. That ledger +retains the original severity, confidence, anchors, proposed cures and caveats. +The grouped plan crossed keyed admission, client scope, transported-page +lookup and supplied-input ownership. The following entries reconcile claims +with executed evidence rather than treating the source review as instructions. + +### Laws and the gaps the review exposed + +| Law and authority | Earlier evidence and missing distinction | Extended instrument and promised cut | Result / repair | +| --- | --- | --- | --- | +| A disabled input admits no authored plan; first enablement admits one even under the same key. An already enabled stable key retains its authored input. Shared disabled/identity contract and disabled-transition scenario; clarified together in contract.ts. | Enablement had no nonempty-key intersection; stable-key page histories began enabled. | Shared window owner generates initially disabled → enabled with the same key → changed predicate with the same key → changed key. Real ordinary React, Preact and Svelte client receivers compare disabled state, ordered rows and Collection retention/replacement after their host cut. Count 2..10, offset 0..1. | H1: Svelte stayed disabled on first enablement. Require an admitted Collection before stable-key reuse. Removing all stable-key retention fails the next cut, so that narrower-looking cure is rejected. | +| The seeded observer shows its seed until local work settles, then announces the live snapshot. SSR guide “Prepare every server snapshot”, established hydration handoff tests, and observer hydration law 4. | Terminal-status grammar could not express an already-ready Collection whose subset work ends without a row/status event. | Existing observer hydration owner generates seed count 1..3, live count 0..3, version 0..3, both granular and wholesale. Compare current snapshot, last announced snapshot and rebuilt granular rows while held and after subset settlement. Real Svelte page receiver supplies the retained-output premise. | PREP-S1: seed remained after subset settlement. Observer now owns a loadingSubset:change listener that schedules seed handoff. Existing handoff readiness guard still prevents an early transition. No new state. | +| Each requested page has its own transported seed; moving one output cannot retarget a later old-page reader. Independent inputs own distinct Collections. Shared contract and SSR guide’s retained server snapshots. | Existing peer preceded the move; server dehydrate compared the untouched server, not a fresh browser lookup. Svelte had no transported page-move receiver. | Shared transported-page grammar feeds React and Svelte and uses the existing independent window judgment. Preload A/B, JSON transport, mount A and peer, move to B before browser readiness, mount 1..2 fresh A readers, release readiness, return A. Count 1..12, offset 0..4, width/jump 1..4, including empty pages. Compare ordered data, keyed state and distinct outputs at each named cut. | PR-HISTORY-001 and CMT-003: receiving coverage completed; extension exposed PREP-S1. Stable complete-record corruption is rejected at the fresh old-page rows comparison. No native hydrate/hydrateRoot or real Electric claim follows. | +| Changing DbClient scope replaces the binding even for the same concrete source. Existing scope/identity contract. | I1: all eight outer labels called the same ordinary unkeyed switch helper; original evidence overclaimed keyed/Suspense reach. | Pass actual keyed/derived and ordinary/Suspense modes into the explicit-client switch driver; switch once without a page change, then with one. Compare rows and replacement. | Eight later fixed/fresh mode campaigns pass and reject a missing-client-equality mutant. These runs do not retroactively broaden the original pre-edit scope. No DbProvider-context switch is claimed by this helper. | + +The page row model remains a complete independent sort/filter/slice. The core +hydration lane reuses expectedView and changes its model phase only at the +explicit controlled settlement; no production classifier computes the answer. +No model rule was changed to forgive the observed failures. The original +source-ID, explicit supplied sharing, initial Suspense readiness, deferred +acquisition, cleanup and finite-domain limits remain authoritative. + +### Pre-edit evidence and a corrected receiving driver + +H1 first failed both selected Svelte fixed/fresh campaigns at `stable key first +admits enabled plan`: expected source rows, actual empty rows. React's 14 +selected admission/transport neighbors passed on the unchanged prep production. +The Svelte transport extension initially used an observation before the +asynchronous handoff could settle; those failures were driver mistakes and +supply no product RED evidence. With a bounded settled cut, it exposed PREP-S1: +the source and query Collection were ready with browser rows while the hook +still showed server rows. The controlled core lane then failed all four +consumer-mode/campaign cells at `subset settled without rows or status changing`. +Only after those comparisons did the evaluator change production. + +One setup experiment omitted includeInitialState on the external subscription, +and another used an unbound unsubscribe. Neither counts as RED. The corrected +core driver explicitly establishes and asserts held subset work before attach. +The diagnostic handoff spy, later removed, observed calls only while subset +work remained held and no retry at settlement. + +The first Svelte receiving driver used a configuration getter accepted at +runtime but absent from the public typed overloads. A full run passed 538 +runtime tests but exited nonzero for that driver's type error; it is not a GREEN +result. The driver was changed to the public config-object form with reactive +query and queryKey getters, without a cast. The previous prep production was +then restored temporarily, rebuilt and compared again: core 4/4 and typed +Svelte 6/6 selected tests failed at the same admission/handoff comparisons, +with no type errors (`2345-prep-typed-preedit-*.log`). Final source was restored +in finally and DB rebuilt. These later typed RED runs calibrate the corrected +path. They cannot retroactively satisfy the original gate's missing typed +public-overload witness; that chronological process gap remains recorded. + +### Lossless candidate ledger + +| ID | Original claim / proposed action | Executed or source evidence; technical verdict | PR action and durable destination | +| --- | --- | --- | --- | +| I1 (P3) | Client-switch reach mislabeled as keyed/Suspense/DbProvider. Parameterize or narrow attribution. | Source discrepancy confirmed; now actual hook/key dimensions execute, with same-page and changed-page switches. Client-equality mutant rejects replacement in all eight cells. | fixed-now; receiver and this corrected historical attribution. | +| B1 (withdrawn bug framing) | Derived C → supplied same C → derived page might retain old resolver permission and corrupt a peer. Consider retiring ownership at supplied input. | Public direct-Collection probes reach this transition in all four ordinary/Suspense × no-client/provider-client paths: C remains C and moves rows [0,1] → [2,3]. Explicit C peers intentionally share its future mutations. The reviewer withdrew the independent-peer corruption inference. No accepted law requires revoking the original binding's permission in this interval. | refuted as the alleged isolation bug; no fence added. Preserve the input-origin experiment here. A future stronger retirement policy would need maintainer authority and generated authored→supplied→authored histories; no such law is proposed for this repair. | +| H1 (P2) | Stable-key Svelte query initially disabled can never enable. Require an admitted Collection. | Confirmed first-enable RED; admitted-plan and changed-key neighbors distinguish retention from unconditional recompilation. | fixed-now; shared keyed-admission grammar, Svelte receiving path and production guard. | +| H2 (conditional P2) | Shallow saved keys alias nested mutated values; snapshot/hash if mutable nested key values are promised. | React and Svelte probes keep [0,1,2,3] after mutating params.minimum in an existing nested key; replacing the nested record produces [3]. This establishes the mechanism, not authority for deep mutable-key semantics. LiveQueryKey's readonly outer array does not decide the nested policy. | design-decision; retain exact ambiguity here. Mutated nested key support would need an identity law and generated lifetime/value dimension, with real Svelte reception. Do not silently add snapshot machinery. | +| H3 (no actionable severity) | Retained initial Suspense readiness may show an incorrect pending page. | Original reviewer found no distinguishing failure. Existing pending-page histories and all ordinary/Suspense transported-page cells pass the named finite ready→pending/seed→local cuts; page/hash and peer mutants fail their comparisons. | refuted within that named witness domain, without a universal readiness claim. Preserve broader retry/reset schedules as existing limits. | +| PR-HISTORY-001 (coverage hypothesis) | Svelte lacks transported A→B-before-ready receiving evidence. Add real host receiver. | Confirmed driver absence; shared grammar + real runes/native flush receiver added. Its handoff failure exposed PREP-S1; repaired typed path is recalibrated. | fixed-now; Svelte receiver and shared transported-page grammar. | +| CMT-001 (P3) | Overview promises replacement for every derived identity change. | Introductory universal claim contradicts accepted finite-page law; source check sufficient for prose. | fixed-now; qualify the overview. | +| CMT-002 (P3) | Svelte comment says every move retains observer. | Client page moves retain Collection but replace observer for the new page hash. Observer identity is internal. | fixed-now; comment describes ordinary/client branches correctly. | +| CMT-003 (coverage hypothesis) | Fresh browser lookup of old page after move unobserved. | Generated 1..2 late readers added to both host receivers. Complete stable browser-record corruption fails fresh-old-page rows. Earlier snapshot-only/index-guess faults do not establish all-mode sensitivity. | fixed-now; per-host declared fresh-lookup reach and calibrated comparison. | +| SIM-N001 | Consider collapsing wrapper Collection and retry entry. | No reduction proposed: wrapper-observed output transition and possibly borrowed resource ownership have distinct lifetimes. Collapsing them erases a needed distinction; no demonstrated smaller replacement. | refuted as a useful reduction; retain readiness/ownership distinction and future async/error retry calibration requirements in this record. | +| SIM-N002 | Consider removing retry cache once outputs become exclusive. | Rejected: React may discard initial suspended refs. Existing cache-removal experiment fails retry progress; exclusive successful ownership does not remove pending retry work. | refuted as a safe reduction; retain cache and its explicit schedule limits. | +| SIM-N003 | Consider removing resolver token or peer-ownership distinction. | Rejected: adopted pending plans and a resumed binding whose resource was claimed by a peer require distinct authority. Existing peer-first-resume mutant rejects stale ownership. | refuted as a safe reduction; retain token/exclusive claim and render receiver. | +| SIM-N004 | Consider merging supplied-Collection and compiled-query Svelte branches. | Rejected: caller-supplied ownership/start policy differs from compiled deferred-start ownership and retention. No materially simpler branch preserves both. | refuted as a useful reduction; preserve distinct paths; B1 keeps the separate origin-policy question visible. | +| SIM-N005 | Consider equivalent ternary or narrower inherited annotation. | No material state/control-flow or shipped-byte reduction; cosmetic churn lacks value for this repair. | refuted as a useful reduction; no edit. | +| SIM-N006 | Consider collapsing legacy receiving labels whose modern calls coincide. | They are explicitly labeled legacy registrations, not different production-path evidence. No production reduction; replay/discovery churn. | refuted as a useful reduction; preserve labels without lending them extra scope. | +| PREP-S1 (new sibling) | Subset-only readiness handoff must announce local rows on an already-ready seeded observer. | Core granular/wholesale RED and real Svelte handoff RED; missing-notification and premature-handoff mutants fail opposite cuts. Distinct from the original report's S1 keyed hash finding. | fixed-now; core hydration owner + Svelte receiver + owned subset listener. | +| PREP-CMT004 (new sibling) | Adjacent identity-change list and Suspense summary repeat universal replacement wording. | Same source/accepted-law contradiction as CMT-001. | fixed-now; update both neighboring explanations without adding behavior. | + +No simplification was applied merely because it was suggested. The six SIM-N +items were rejected investigations, not six defects found in production. +No confirmed enforcement repair was offered as an optional user decision. + +### Contextual nonfinding reconciliation + +These sixteen source observations were preserved separately from the fifteen +candidate/simplification items. They carry no new product-defect allegation. + +| Source ID | Original observation and disposition | Evidence limit / durable destination | +| --- | --- | --- | +| I-N1 | accepted-design: sharing expectation changes have explicit maintainer authority. | Contract, task decision and original law inventory; explicit supplied sharing stays. | +| I-N2 | accepted-design: peer grammar uses independent window judgment. | Source review corroborated by fixed/fresh receivers and calibrated peer faults; finite scope only. | +| I-N3 | accepted-design: pending Suspense observes fallback; identity retention starts at successful return. | Shared contract and pending host comparison; pre-success identity is not promised. | +| I-N4 | refuted as an oracle-form concern: no standalone unit example/type assertion identified in these oracle additions. | Declared grammar/checks, shared replay examples, fidelity and lint; source review alone was not runtime evidence. | +| I-N5 | stale as a complete evidence inventory after prep: original six RED/eight-mutant record remains historical. | This append adds prep defects and the typed receiving gap rather than rewriting old results. | +| I-N6 | accepted-design: finite pages and selected controlled host schedules bound the evidence. | Coverage map retains Electric/Start/hydrateRoot and arbitrary retry-schedule limits. | +| I-N7 | stale: earlier manifest is preserved as a prior snapshot; final prep manifest is recomputed. | No author-reported hash is treated as independently verified by the reviewer. | +| I-N8 | accepted-design: common owner and neighboring source-ID/release/GC laws remain. | Source inspection; changed receiving paths rerun without entering materialization internals. | +| PRIOR-N1 | accepted-design: old DbClient/Suspense exclusions are explicitly superseded by this task. | #2291/#2316 history and 2026-11-03 maintainer decision. | +| PRIOR-N2 | accepted-design: Infinity→finite capability is not generated finite-domain proof. | Existing coverage-map limit; no new unbounded promise. | +| PRIOR-N3 | accepted-design: replays must select the real adapter. | New prep replay commands name the receiving package and property. | +| PRIOR-N4 | already-fixed historically: validation/pooling concerns were prior resolver repairs. | Current hooks still disable pooling; no new pooled-output counterexample established. | +| PRIOR-N5 | accepted-design: Suspense first-load errors/reset preserve established contract. | Full React suite supplies current example coverage, not arbitrary lifecycle generation. | +| PRIOR-N6 | accepted-design: deferred acquisition and each server snapshot are neighboring obligations. | New held-readiness receiving cuts and existing source-release/SSR owners; no provider leak allegation confirmed. | +| PRIOR-N7 | accepted-design: indexing expectations change only where they depended on implicit output sharing. | Maintainer independence decision, existing option/index owners and full React verification. | +| PRIOR-N8 | stale historical prose, not an instruction: addressed machine-local path and superseded admission retry suggestion. | Retain cleanup-admission law; no bot text authorizes extra product changes. | + +### Reviewer assessment + +Hire the review team for this task. The historical reviewer found the concrete +H1 mechanism and separated it from unapproved key policy. The instruction +reviewer caught a real path-attribution error. The prior-PR and comment angles +supplied receiving experiments that exposed a second production bug. They +reported source hypotheses as hypotheses and kept lower-confidence ideas. +The initial B1 peer-corruption inference was wrong and was withdrawn; its +input-origin observation still has value. H3 supplied no counterexample. +Prioritization and proposed H1 repair were sound after checking the adjacent +stable-key law. The simplifier showed useful restraint and found no worthwhile +additional reduction. These judgments concern review quality, not independent +confirmation of the author's historical executions. + +### Additional sibling PREP-S2: Preact first keyed admission + +The first Preact conformance gate failed the two new keyed-admission campaigns +and passed 138 neighbors. The fresh reduced witness was count 2, offset 0, +seed 1402897452, path `0:0`; the failure was ordered window rows at first +enablement. Preact compared key identity while its previous Collection was +null, so it retained a disabled binding. This is the H1 law on another real +receiving path, not a reason to drop that registration as an older bug. +The existing typed ConditionalUseLiveQueryConfig accepts the driver's exact +configuration shape: a query callback may return null. Preact exposes no client +or Suspense path here. The extended owner had already produced this RED on +unchanged Preact before the production guard was changed. The guard now +resolves when no Collection has been admitted, retaining an enabled stable key. +PREP-S2 is a separately accounted fixed-now sibling, with original-guard replay +calibration and the same shared model/grammar/comparisons. + +### Calibration outcomes and direct replay + +| Wrong design | Observed outcome | +| --- | --- | +| Restore Svelte's disabled-result stable-key shortcut | Both selected campaigns fail first-enabled ordered rows. | +| Remove Svelte's stable-key shortcut entirely | Both fail stable admitted-plan ordered rows. | +| Remove observer subset listener | All four granular/wholesale × fixed/fresh cells fail subset-only handoff. | +| Remove subset readiness guard from handoff | All four fail the held-work seed cut. | +| Ignore client equality in React retained admission | All eight keyed/derived × ordinary/Suspense × fixed/fresh cells fail client-switch Collection replacement. | +| Retarget the complete stable browser old-page record, snapshot and initialSnapshot | React 8/8 and corrected typed Svelte 4/4 cells fail ordered rows at fresh old-page lookup before local readiness. | +| Restore Preact's identity-only admission guard | Captured direct replay fails first-enabled ordered rows; repaired replay passes. | + +The browser-record fault chooses A by independently expected row IDs and +returns one stable corrupt record. Earlier index-zero guesses targeted the +wrong page in some cells. Snapshot-only corruption left ordinary initialSnapshot +intact and survived some paths. A newly allocated corrupt record on every +lookup caused a React maximum-update-depth failure outside the comparison. +Those attempts are not credited as all-path kills. The final stable complete +record fault reaches the intended row comparison without type errors. + +Direct replay was checked in the real receiving package with +TANSTACK_DB_ORACLE_PROPERTY, TANSTACK_DB_ORACLE_SEED and +TANSTACK_DB_ORACLE_PATH. Each recorded mutant replay failed the original +comparison, then passed after restoration. No fixed campaign precedes these +explicit replays. These grammars do not use fc.commands. + +| Property | Seed | Captured shrink path | Package, test file and selector | +| --- | --- | --- | --- | +| framework.svelte-DbClient.keyed-admission | 2345 | 0 | svelte-db; tests/conformance.svelte.test.ts; initial disabled keyed admission.*fresh | +| live-query-observer.hydration-subset-handoff | 2345 | 0:0 | db; tests/live-query-observer-hydration-oracle.property.test.ts; granular seeded ready-subset handoff.*fresh | +| svelte.client-window | 2345 | 0:0:0:0 | svelte-db; tests/client-window-oracle.svelte.test.ts; derived/fresh | +| framework.preact.keyed-admission | 1402897452 | 0:0 | preact-db; tests/conformance.test.ts; initial disabled keyed admission.*fresh | + +For each row, run pnpm exec vitest run --maxWorkers=2 --coverage.enabled=false +with the named file and -t selector from that package, setting the three +environment variables to the table's values. The capture and replay commands, +exit codes, and fault definitions remain in the task-local prep calibration +records; these tables preserve the verdict-critical inputs and observations. + +### Additional oracle rubric accounting + +This table supplements, rather than replaces, the original rubric assessment. + +| Requirement | Prep outcome | +| --- | --- | +| ORC-001 | Disabled admission, stable authored input, independent outputs, transported page identity and announced hydration handoff are tied to the shared contract/SSR authorities above. Nested mutable-key support is unresolved policy, not an oracle expectation. | +| ORC-002 | Existing independent sort/filter/slice and expectedView models remain; adapter/core classifiers do not determine expected phases or rows. | +| ORC-003 | Opening/nearby prose describes admission lane, shared transport grammar, host cuts and subset lane. Shared model, grammar, driver and comparison stay distinct. | +| ORC-004 | H1 reconstructs the disabled→enabled witness and distinguishes deleting retention by its next stable-key step. PREP-S1 reconstructs ready+held→settled with no row/status change; removing hold loses early-handoff discrimination, removing settlement loses progress. Transport axes vary page shape/count and late-reader cardinality; removing held readiness or late readers loses the fresh seed lookup cut. Ranges are stated above; counts/offsets/widths are nonnegative finite integers, late readers are 1..2, and no enabled Collection is assumed for a disabled input. Negative widths, no late reader and settlement-before-hold are outside these grammars. | +| ORC-005 | Actual keyed/ordinary/Suspense/client host paths now execute under their correct labels. Svelte config getter correction and its chronological gap are recorded above. Missing source facts are not suspected: page seeds are explicitly transported; subset completion is controlled and asserted. | +| ORC-006 | The table above classifies intended assertion failures and the unsuccessful earlier fault designs separately. No timeout/setup/runtime-only failure supplies a semantic kill. | +| ORC-007 | Each new lane has equal-budget fixed/fresh campaigns, fixed seed2345 and fresh seed logging. Direct property/seed/path mutant RED and restored GREEN were checked as tabulated. | +| ORC-008 | No new reference state machine: core lane reuses Model/expectedView; the driver supplies explicit held/settled phases. The admitted-plan distinction changes first-enable legality and expected result. | +| ORC-009 | Host bindings, Collections, subset work, seed, settlement and window use glossary terms. The shared sort/slice model deliberately abstracts away graph/cache/subscription internals. | +| ORC-010 | Existing failure-fidelity and ScenarioLifetime preserve original law/checkpoint and secondary cleanup diagnostics. Shrunk failures retain the intended first-enable/handoff/fresh-lookup signature. | +| ORC-011 | No new plausible shared-semantic-fault hypothesis needs a second formulation: independent direct arithmetic and explicit controlled phase contrast the production cache/observer rules. Same-path policy probes for H2 supply no accepted model rule. | +| ORC-012 | This append records the added requirements and known limits. Manifest pins the final source; original manifests and process evidence remain historical. | +| ORC-013 | Disabled→enabled and stable-enabled→changed-input distinguish “reuse everything” from “compile everything”; held→settled rejects both missing and premature handoff. Same-page and changed-page client switches reject window-only client replacement. | +| ORC-014 | Real React/Preact/Svelte hooks receive the shared laws. Native Svelte runes/flush and settled handoff cut transfer the controlled subset premise; no new claim about browser hydration or Electric/Start provider delivery is made. | +| ORC-015 | New properties declare and assert per-mode reach; measured counts are reported below rather than inferred from test totals. | +| ORC-016 | Regressions use shared model/driver/comparison; new modules contain no separate expected-answer examples or standalone type assertions. | + +### Measured reach and repaired-branch checks + +Counts below came from successful normal runs with console interception disabled; +they are the actual createOracleReach counts for each cell. Fixed transported +examples count as histories in those receivers; their six additional random +histories are unchanged between fixed and fresh campaigns. + +| Receiving property / modes | Declared states | Measured histories per state, separately in each campaign | +| --- | --- | --- | +| keyed admission: React ordinary, React legacy dependency-list label, React DbClient, Preact, Svelte DbClient | initial disabled; enabled under same key; stable admitted plan; changed key | 6 for every state in every named fixed/fresh cell | +| React transported page: derived/keyed × ordinary/Suspense | first seed; moved seed; local handoff; peer isolation; returned page; explicit client scope switch; fresh old-page lookup | 8 for every state in each of the eight mode/campaign cells | +| Svelte transported page: derived/keyed | first seed; moved seed; local handoff; peer isolation; returned page; fresh old-page lookup | 8 for every state in each of the four mode/campaign cells; no Svelte client-switch claim | +| core ready-subset hydration: granular/wholesale | ready while subset held; subset-only handoff | 8 for each state in all four mode/campaign cells | + +Final repaired source passed the following executed gates. All Vitest commands +used at most two workers and ran sequentially; no mutant was active during a +credited GREEN run. + +- React: pnpm exec vitest run --maxWorkers=2, 4,735 tests in39 files, no type + errors (`2345-prep-react-full.log`). The first attempt collided with another + probe's coverage directory and is invalid; only the later completed run is + credited. After all prep mutants were restored, the selected client/admission + receivers also passed14 tests and printed the reach above. +- Svelte: pnpm exec vitest run --maxWorkers=2 --coverage.enabled=false + --disableConsoleIntercept, 538 tests in16 files, no type errors on the + corrected typed receiver (`2345-prep-typed-svelte-full.log`). The final + restored transported-page receiver additionally passed all4 cells after its + complete-record fault. Svelte test:types found zero errors and warnings. +- Core observer neighbors: pnpm exec vitest run --maxWorkers=2 + --coverage.enabled=false --disableConsoleIntercept over the six + tests/live-query-observer*.test.ts files,119 tests passed, no type errors + (`2345-prep-core-observer-neighbors.log`). This includes all13 hydration tests. +- Preact: full pnpm exec vitest run --maxWorkers=2 --coverage.enabled=false + --disableConsoleIntercept,260 tests in8 files, no type errors after PREP-S2 + (`2345-prep-preact-full.log`). The initial140-test receiving run is RED, + not a GREEN neighboring run. + +The original full Vue/Solid/Angular receiving results remain those recorded +above; their source did not change in prep. No existing run budget was lowered +or removed. The new core listener increases subscription ownership only while +an observer has a client binding and is released with its other listeners. +It does not broaden the finite-page or provider delivery promises. + +### Loss audit and readiness + +The evaluator returned to both complete raw reports and reconciled their +candidate inventory, prose caveats, considered reductions and contextual +nonfindings against the final diff. Fifteen original candidate/simplification +items resolve to six fixed-now, eight refuted and one design-decision (H2). +The sixteen contextual observations resolve separately to eleven +accepted-design, three stale historical inventories/prose items, one historical +already-fixed item and one refuted oracle-form concern. Thus all31 raw items +are accounted for. The three new siblings PREP-S1, PREP-CMT004 and PREP-S2 are +fixed-now, recorded separately rather than folded into raw-review totals. +No confirmed in-scope counterexample was deferred or removed from comparison. + +Implementation and oracle repairs for the accepted laws are complete within +the finite generated histories and host cuts above. H2 asks whether mutating a +nested object already used in queryKey must invalidate authored input, versus +requiring replacement of that nested value. The current accepted contract does +not decide that policy; it has not been encoded or silently excluded from an +existing mutable-key law. No stronger supplied-input retirement law is proposed +from the withdrawn B1 isolation allegation. + +PR readiness remains unproven: there is no final commit, pushed final head, +PR or CI run yet. The prep skill's required user confirmation precedes those +steps. Local GREEN evidence is not a final-head integration verdict. The +historical missing typed pre-edit witness remains a process gap despite later +typed RED/GREEN calibration. Native hydration, Electric/Start integration, +nonfinite pages and arbitrary retry/error-reset schedules remain the earlier +explicit evidence limits; no universal bug-class proof is claimed. + +### Final prep source identity and shipped weight + +The companion manifest now pins24 changed source/contract/coverage files with +combined SHA256 +`2c1e7f6caaacc77e2e7c51edf177820466b1cf15723e460116399fcb95517fdf`. +It excludes this report and the manifest itself to avoid self-reference. Its +two prior snapshots preserve the original18-file and simplified19-file trees. +No temporary probe, mutant, local install workaround or Superpowers artifact +is in the product diff. Explicit ESLint and Prettier checks on all changed +TypeScript sources passed; pnpm lint:changed and git diff --check passed. +DB/React/Preact size-compare builds and the final Svelte package build passed. + +The final exact-base size comparison uses1443dc92e and NODE_ENV=production. +The typical React app shrinks136 gzip /155 brotli bytes; every other measured +React application scenario shrinks145..170 gzip bytes. The all-exports React +package shrinks44 gzip bytes, core grows17, Preact grows6, and Svelte grows48. +Svelte's corresponding brotli growth is39. The three non-core adapters together +grow10 gzip bytes; those separately compressed package sizes are not one app +bundle. The net-negative result is the measured React application, not every +individual package or the sum of all changed packages. PREP-S1's settlement +notification and first-admission guards were retained because their removal +fails accepted-law comparisons. No additional safe structural reduction was +identified by the simplifier. Commands: pnpm size:compare --base1443dc92e +--exact-base --packages db,react-db,preact-db (with a space after --base), plus +the same Svelte bundle probe used by the earlier entry. + + +## Maintainer-directed strengthening of Suspense lifecycle coverage + +This entry appends evidence after the prep evaluation. The maintainer requested +stronger coverage for the previously weak areas. It adds no production repair +and makes no new nested mutable-key decision. All production source hashes +still match the prior 24-file prep snapshot. The new primary boundary owner is +`packages/react-db/tests/suspense-lifecycle-oracle.test.tsx`; the core +source-health/observer and shared page owners retain their distinct laws. +This is a self-review of the new instrument, not an independent reviewer verdict. + +### Law, model, and production boundary + +Authority: `docs/guides/error-handling.md`, “Using Suspense and Error Boundaries +(React)”; the hook's JSDoc; shared conformance query identity, retry progress and +exclusive successful output laws; and the existing public cleanup/request +recovery examples in `useLiveSuspenseQuery-recovery.test.tsx`. + +A current initial load exposes a fallback until it settles. A first source or +query-preload failure reaches the ErrorBoundary with the original Error or a +wrapped non-Error cause. A stopped source after readiness keeps data and shows +its health problem, including an undefined cause. Cleanup/restart or a new +client request retires the prior initial-view obligation. Its late rejection +cannot poison the replacement. A settled initial load must make retry progress, +and successful independently authored queries expose distinct Collections. +The supplied-Collection overload intentionally permits explicit sharing. + +The reference stores only the current model attempt ordinal, public phase, +rows/cause, boundary wrapping policy, and stopped-source health distinction. +The ordinal combines a boundary reset and the source sync run or client request +it now waits for; it is not an acquisition attempt. An old ordinal cannot +change the newest view. A source failure before readiness versus after it +requires different phase/health observations. Error versus non-Error values +require different identity/cause checks. Removing any of those distinctions +merges states with different promised observations. No production retry cache, +observer state machine, resolver token or subscription classifier is copied. +Expected rows are recomputed from history values separately from inserted rows. + +Real React 19 ErrorBoundary and Suspense components record the committed host +phase, hook data/keyed state, caught error, Collection identity, and health. +Source histories cross no-client/provider/explicit-client and derived/keyed +receivers. Query-only preload histories use a supplied Collection without a +client or under a provider. That overload has no explicit-client config form. +Client-request histories register actual DbClient request records over a +supplied Collection. Query preload is held through its public method seam; +external provider behavior is not inferred. A source is started explicitly when +the seam holds its query preload. ScenarioLifetime and failure fidelity preserve +the primary mismatch, secondary cleanup diagnostics, and original/reduced trace. + +### Driver corrections and calibration-led improvement + +The first request driver incorrectly assumed registration replaces an existing +pending DbClient record. It actually deduplicates. Those failures are driver +premise errors, not production RED evidence. The corrected schedules follow the +established recovery examples: fail then reset; register in the old promise's +rejection reaction after its record fails; cleanup/unmount then reject before +registering; or register under another client before the old rejection. +Admission of the new pending record is asserted before comparing its fallback. + +An attempted message comparison used the source health message. A derived +query has its own problem message (for example, a source-crash explanation), so +that observation was insufficient. No product defect followed. This owner +explicitly checks Error identity/wrapped cause and leaves message text to the +existing supplied-query boundary examples. The first preload fixture also +failed because it had not started its source and attempted an unsupported +explicit-client supplied-Collection config; both premises were corrected. +The initial TypeScript run found fixture-only type mistakes (fast-check's +inferred-history type, invariant Collection generics and sync key type). The +corrected fixture passes the full package's explicit TypeScript check. +No production change was made in response to any setup or model mistake. + +The active-cleanup mutant first survived: the old root was unmounted, so its +late rejection did not render the new root before readiness cleared the error. +That exposed a missing observation cut. The generated driver now explicitly +rerenders the still-pending replacement after the late rejection. The baseline +passes and the mutant fails at that cut. A separate mutant that removes only +the preload-catch client fence survives because the render-time client-record +comparison clears its stale error; removing that latter guard fails. The +single-guard survivor does not show either guard is globally redundant. + +### Campaigns and measured reach + +The normal command runs 18 campaigns:12 source retry cells, 2 client replacement +cells, and 4 supplied-query preload cells. Each fixed-seed campaign uses 2345; +each fresh campaign chooses and reports its own seed. Both use the same grammar, +model, driver, comparison and run budget. There are 448 generated histories plus +52 fixed example replays, 500 histories total. Examples add to the budgets; no +existing run budget was lowered. Each declared state has minimum 1, and fixed +examples reach every declared state independently of random sampling. + +Command from `packages/react-db`: +`pnpm exec vitest run tests/suspense-lifecycle-oracle.test.tsx --maxWorkers=2 --disableConsoleIntercept --coverage.enabled=false`. +Result: 18 passed, no runtime/type-runner failures. The explicit TypeScript check +is separate: `pnpm exec tsc --noEmit -p packages/react-db/tsconfig.json`, exit 0. +The logged reach below is from the final 500-history campaign. + +For every retry row, new-attempt pending, retry progress, independent peer and +post-ready failure each reached 27 histories. Pending retirement equals abandon. +`false` means derived identity and `true` means explicit queryKey. + +| Scope/key/campaign | fail | crash | abandon | root remount | boundary reset | initial fallback | first failure | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| none/false/fixed | 6 | 13 | 8 | 11 | 16 | 14 | 19 | +| none/false/fresh | 5 | 9 | 13 | 11 | 16 | 18 | 14 | +| none/true/fixed | 6 | 13 | 8 | 11 | 16 | 14 | 19 | +| none/true/fresh | 10 | 9 | 8 | 15 | 12 | 18 | 19 | +| provider/false/fixed | 6 | 13 | 8 | 11 | 16 | 14 | 19 | +| provider/false/fresh | 8 | 11 | 8 | 11 | 16 | 16 | 19 | +| provider/true/fixed | 6 | 13 | 8 | 11 | 16 | 14 | 19 | +| provider/true/fresh | 6 | 11 | 10 | 11 | 16 | 16 | 17 | +| explicit/false/fixed | 6 | 13 | 8 | 11 | 16 | 14 | 19 | +| explicit/false/fresh | 13 | 5 | 9 | 14 | 13 | 22 | 18 | +| explicit/true/fixed | 6 | 13 | 8 | 11 | 16 | 14 | 19 | +| explicit/true/fresh | 12 | 8 | 7 | 10 | 17 | 19 | 20 | + +For each replacement campaign, first-request pending, replacement pending and +replacement ready each reached 36 histories. First-request failure equals the +failure-first count. Late rejection counts both replacement-in-reaction and +client-switch schedules. + +| Campaign | failure-first | replace-first | cleanup-first | client-first | late rejection ignored | +| --- | ---: | ---: | ---: | ---: | ---: | +| fixed | 8 | 10 | 10 | 8 | 18 | +| fresh | 8 | 12 | 7 | 9 | 21 | + +For each preload row, pending, restart pending and restart ready each reached 26 +histories. The late-rejection count includes the forced still-pending render. + +| Scope/campaign | failure before cleanup | rejection after cleanup | +| --- | ---: | ---: | +| none/fixed | 14 | 12 | +| none/fresh | 16 | 10 | +| provider/fixed | 14 | 12 | +| provider/fresh | 11 | 15 | + +### Hostile mutations and exact replay + +All five faults fail by assertion at their intended comparison, rather than by +timeout, setup failure or an unrelated crash. Every fault was restored. Each +captured seed/path was then replayed directly with the guarded runner: exactly +one completed witness was recorded for the selected property in RED and GREEN. +No normal campaign runs ahead of an explicit replay. + +| Fault | Property suffix after react.suspense- | Seed | Shrink path | Rejected observation | +| --- | --- | ---: | --- | --- | +| omit pending cache insertion | retry-history | 533028291 | `0:0` | settled load remains pending instead of ready | +| leave claimed output borrowable | retry-history | 32230998 | `0:0` | peer returns the same successful Collection | +| keep errors on Collection cleanup | preload-restart-history | 600053472 | `0:0:0` | restarted query exposes old error instead of pending | +| disable replacement-record error clearing | replacement-history | -2134737032 | `0:0` | replacement exposes old client error instead of pending | +| keep retired preload active | preload-restart-history | -165799692 | `1:0:0:0:1` | forced pending render exposes retired preload error | + +Example guarded replay, from `packages/react-db`: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=react.suspense-preload-restart-history TANSTACK_DB_ORACLE_SEED=-165799692 TANSTACK_DB_ORACLE_PATH=1:0:0:0:1 node --import tsx ../db/tests/oracle-replay.ts tests/suspense-lifecycle-oracle.test.tsx --maxWorkers=2 --disableConsoleIntercept --coverage.enabled=false -t 'none preload restart \(fresh\)' +``` + +The replay manifest now names all three new property owners. Scratch logs are +`/private/tmp/2345-strengthen-{reach-final-v2,all-preedit-v2}.log`, +`2345-strengthen-guarded-replays.json` and each referenced guarded RED/GREEN log. +Direct unguarded discovery replays ran first; those logs are not the guarded +execution claim. The versioned tables above retain the verdict-critical results. + +### Grammar controls and oracle review + +| Requirement | Outcome and evidence | +| --- | --- | +| ORC-001 | Laws and authorities above; limits below. No new mutable-key policy. | +| ORC-002 | Pure current-obligation model; expected rows derive from input values, not production state. | +| ORC-003 | Opening law/limits, local model, grammar, driver and comparison prose teach their distinct responsibilities. | +| ORC-004 | Reconstruction: fixed fail/crash/abandon examples cover empty and populated results, reset and remount; four replacement examples cover every legal schedule; two preload examples cover rejection on either side of cleanup. Ablation: counts/version distinguish empty/stale payloads; Error/record/undefined distinguish identity, wrapping and presence; rerenders exercise discarded refs; reset/remount exercise retained/discarded roots; client/key cells exercise distinct receiving paths; failure origins and schedules separate source status from wrapper/client errors. Range: rows 0..3, versions 0..3, pending rerenders 0..2, three cause classes. Exclusion: pending registration is never treated as replacement; no same-run readiness after fail/crash; no pre-success Collection-retention requirement. | +| ORC-005 | Actual React host phase plus committed data/keyed state/error/health/identity at named cuts. New-record pending and source-run/demand premises asserted. The late-preload survivor required a forced pending render. No concrete missing-source-fact hypothesis remains for these cuts. | +| ORC-006 | Five semantic faults rejected by assertions; one client-fence survivor classified as protected by a neighboring guard; initial active-cleanup survivor repaired with a new observation cut. | +| ORC-007 | Fixed 2345 and fresh campaigns with identical budgets; all five captured seed/path REDs reproduce and restored GREENs execute exactly one guarded witness. No commands arbitrary, so no commands replayPath applies. | +| ORC-008 | Attempt, phase, cause policy, rows/cause and health distinctions each have the distinguishing next observation described above. No cache/lifetime implementation state modeled. | +| ORC-009 | Model-only attempt explicitly combines initial-view obligations and is distinct from acquisition attempts. Failure events name source, preload or client origin. | +| ORC-010 | Shared failure fidelity keeps law/checkpoint and original/reduced trace; ScenarioLifetime releases resources and preserves secondary cleanup diagnostics. Five replays preserve the intended mismatch. | +| ORC-011 | No plausible shared semantic fault requiring a second formulation was identified. Public source/client signals come from separate paths and feed one boundary model; this is not claimed as a second formulation. | +| ORC-012 | This append-only record, source manifest and per-path tables carry the finite boundary claim and known limits. Earlier process gaps remain recorded. | +| ORC-013 | Failure before/after readiness, old/current ordinal, before/after cleanup, and still-pending/ready cuts distinguish the wrong boundary rules; zero rows remain different from a fallback. | +| ORC-014 | Real React host receives the controlled source/preload/request premises. No actual provider/native-hydration claim. | +| ORC-015 | Minimum1 plus fixed-example reach in each of 18 receiving campaigns; all actual counts above. | +| ORC-016 | Fixed examples use the same model/driver/checker as generated histories; no ordinary unit tests or standalone type assertions were added to the oracle. | + +### Evidence block and limits + +- **Law:** current initial-view progress, retired-failure isolation, boundary error/health behavior and exclusive successful outputs, from the authority above. +- **Reach:** 500 histories in 18 receiving campaigns; each declared state reached per mode as recorded above. +- **RED:** no new production defect was found in this strengthening pass. Unchanged production is GREEN; the five deliberately wrong branch mutations are assertion REDs with exact guarded replay evidence. Earlier issue/repair parent RED evidence remains in prior entries. +- **GREEN:** all 18 new campaigns pass; final integration checks are appended below. +- **Mutant:** lost retry resource, borrowable successful output, retained cleanup error, stale replacement error, and active retired preload all rejected at the named cuts and restored. +- **Limits:** finite one-source rows and explicit host task checkpoints in React 19/jsdom; no native hydrateRoot, real provider, React18 lifecycle receiver, arbitrary concurrent rendering, exact GC deadline, separately held failure-retirement timer or all error message text. Existing neighboring owners and boundary examples retain those contracts. The source retry lane disables GC so delayed cache eviction is outside its evidence. These are stated coverage limits, not retired product laws. The optional nested mutable-key policy remains undecided. No extra shipped bytes were introduced. + + +### Strengthening closeout checks and source identity + +The full React package passes **4,753 tests in 40 files**, including the new 18 +campaigns and existing page, render, local-option, indexing, source-ID, preload, +recovery and error-boundary neighbors. Explicit package TypeScript checking, +ESLint on the new oracle/registry/manifest, Prettier and `pnpm lint:changed` pass. +The five guarded RED/GREEN pairs were repeated after the final model/recorder +edits; each still records exactly one execution and the same intended mismatch. +All production files still match the earlier prep source hashes, so the measured +React application reduction remains 136 gzip /155 brotli bytes. This pass adds +only tests, replay registration, coverage documentation and evidence. + +No production defect surfaced in the corrected legal histories. The strengthened +finite claim is protected against the five named wrong designs; it does not +remove the explicit native/provider/GC/concurrency limits above. No commit, +final push, changeset, PR or CI verdict exists yet. The prep workflow's required +confirmation remains the next step. + +The refreshed manifest pins **26 source/contract/coverage files**, combined +SHA256 `d4899416d330bf345599932475fd273aa124e4ecb0f51ecf508162d87edc27d2`. It preserves the prior 24-file snapshot alongside +the earlier 18-file and 19-file snapshots. This report and the manifest remain +self-excluded. The unchanged base is `1443dc92e5417394acbdc98b782caa9802848ce6`. + + +## CodeRabbit review and cancelled-page receiving repair (PR #2348) + +Reviewed commit: `8cf448391e59c0ebdf6a05c2fa2fa7f8dadae711`. +Review [5755501550](https://github.com/TanStack/db/pull/2348#pullrequestreview-5755501550) +contains three inline findings. The mutable summary at 2026-11-03T16:05:57Z +adds two policy/scope warnings. The task-local audit preserved their complete +raw bodies before tests or edits. This entry records all five items and the +resulting source snapshot. It does not replace the earlier evidence. + +### Law, experiment, and inference + +A speculative page render cannot replace the committed page's observable +snapshot. The shared conformance contract requires a committed host update for +a window move and preserves each page's preload/hydration identity. This is +an enforcement gap in that accepted law, not a new product decision. + +The old cancellation receiver compared local Collection rows and acquisition. +The old transported-page receiver checked only committed moves. Their separate +green results missed the crossing: a source whose readiness is held, distinct +server page seeds, an attempted page render that suspends after the hook, then +an original-page render that cancels it. The local Collection stays on the +original page while its observer can retain the abandoned page's seed. + +The existing React client receiver now drives that history through real React +in jsdom. It reuses `clientWindowArbitrary`, `windowRows`, +`checkWindowObservation`, and `checkWindowState`. Each history checks data, +keyed state, and Collection identity after cancellation, then continues the +existing committed move, peer, handoff, return and client-switch checks. +Offset-only, limit-only and combined proposals have separate campaigns, as do +ordinary/Suspense hooks, provider/explicit clients and derived/keyed identity. +The proposed page is preloaded under the same public API as the original page. +The fixture supplies source rows before readiness but holds readiness through +cancellation, so later local rows cannot conceal the wrong seed. + +All 48 campaigns rejected unchanged production at `ordered window rows / +matching seed after cancelled page`. One concrete observation was server rows +`s2,s3,s4` instead of `s0,s1`. The shrunk seed/path `2345 / 0:0:0:0` preserves +that law and checkpoint, including an empty abandoned page versus original +row `s0`. This is a public snapshot mismatch, not a setup failure or timeout. + +The repair keeps each proposed observer, subscribe function, and query hash +local to its render. A retained move saves those values only at commit. The +same render reads its proposed observer, so legitimate committed moves still +select the correct page seed. No key hashing was added. + +### Evidence block + +- **Law:** Committed page snapshots survive discarded window renders. Authority: + the existing shared committed-update and transported-page contracts above. +- **Reach:** 48 fixed/fresh campaigns, each with six generated histories plus + two shared examples: 384 histories, 288 generated and 96 fixed replays. + Each of nine declared states reaches eight histories in every campaign: + first seed, suspended seed, cancelled seed, moved seed, fresh old-page lookup, + local handoff, peer isolation, returned page and client scope switch. +- **RED:** `pnpm --filter @tanstack/react-db exec vitest run + tests/client-window-oracle.test.tsx --maxWorkers=2 --coverage.enabled=false` + rejects all 48 campaigns on reviewed production. The final unchanged-source + TypeScript check passes before that repeated RED. Exact source was restored + from `8cf448391`, not approximated with a mutant. +- **GREEN:** The same 48 campaigns pass after the repair. The final full React + run passes 4,793 tests in 40 files; explicit React TypeScript passes. The full + guarded replay suite passes 110 tests. ESLint, Prettier, `pnpm lint:changed` + and diff checks pass. The size command builds the affected packages. +- **Mutant:** Reintroducing render-time writes of the proposed observer, + subscribe function and hash rejects `react.client-window`, seed `2345`, path + `0:0:0:0`, at the same cancelled-seed comparison. Guarded replay records one + failed execution. Restoring production records one successful execution. +- **Limits:** Finite one-source page inputs, JSON-transported preloads, held + readiness and this real React transition/cancellation schedule. No native + `hydrateRoot`, actual Electric/Start provider, arbitrary concurrent schedule, + exact GC deadline, infinite window or mutable nested-key promise is added. + No oracle run budget decreased. + +The first runtime REDs preceded the repair, but their Vitest "no type errors" +summary did not expose a reach-counter annotation mismatch. An explicit `tsc` +check later found it. The annotation now accepts the receiver's full state +union. With production temporarily restored to the exact reviewed commit, +`tsc` passes and all 48 comparisons reproduce RED before restoring the repair. +The initial attempt's missing explicit type check remains a process +qualification; the repeated typed witness does not rewrite that chronology. + +Commands and outcomes remain in the task-local `2345-ci-loop` directory: +`cancel-typed-unchanged-tsc.log`, `cancel-typed-unchanged-red.log`, +`cancel-typed-restored-tsc.log`, `cancel-mutant-red.log`, +`cancel-restored-green.log`, `react-full-final.log`, and +`replay-full-final.log`. The guarded command, from `packages/react-db`, is: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=react.client-window TANSTACK_DB_ORACLE_SEED=2345 TANSTACK_DB_ORACLE_PATH=0:0:0:0 node --import tsx ../db/tests/oracle-replay.ts tests/client-window-oracle.test.tsx -t 'derived/ordinary/provider/both/fresh' --maxWorkers=2 --coverage.enabled=false +``` + +### Lossless review ledger + +| ID/source | Claim and suggested change | Verdict and completed action | Durable value | +| --- | --- | --- | --- | +| CR1 / inline 4457228450, evidence report:173, minor | The unescaped boolean-expression pipes split the issue-ledger row and lose its verdict/destination cells. Escape both pipes. | **fixed-now**. Escaped both pipes. Prettier's Markdown parser reads R1 as four cells and preserves the original expression. No product behavior changes. | Preserve readable source/decision evidence. | +| CR2 / inline 4457228468, oracle-config.ts:51, major | New DbClient/Suspense window, peer and admission names lack replay owners. Add each real runner and extend the driver matrix; inspect other new names too. | **fixed-now**. The existing owner check rejects 32 missing routes. All 32 now map to their actual owner and shared source. Added originating-driver cases, direct fresh-lane selectors and computed-name provenance checks. Full replay suite: 110 passed. | Every registered property needs a named executable owner; a registered name alone proves no execution. | +| CR3 / inline 4457228488, useLiveQuery.ts:714–716, major | A retained move stores its proposed observer/hash during render. Cancellation can keep those abandoned values because identity dependencies remain committed. Keep them render-local until commit. | **fixed-now**. All 48 unchanged-source receiving campaigns reject the wrong cancelled seed. Commit-only saving passes them; guarded original-behavior mutant fails and restored replay passes. | Cross cancellation with held page seeds; correct local Collection rows do not prove correct transported identity. | +| CR4 / summary scope warning | Suspense ownership, keyed disabled admission and seed handoff exceed #2345. Remove/split them or supply scope authority. | **accepted-design**. The maintainer explicitly requested independent Suspense outputs, Svelte DbClient support and stronger coverage. Admission and handoff are accepted-contract siblings found during that receiving work. The PR body lists the broader scope. No source removal. | Distinguish issue scope from explicit maintainer scope and identify neighboring fixes honestly. | +| CR5 / summary documentation warning | Bot reports 19.05% documentation against an 80% threshold over 21 functions. Add missing docstrings. | **refuted** as a repository requirement. That percentage was not independently reproduced. No repository CodeRabbit policy or 80% docstring gate applies. Existing literate owner/driver prose explains laws and cuts; the changed cancellation path adds purpose prose. No blanket duplicate docstrings. | Retain the clarity question without treating a bot default as contract authority. | + +CR2's actual receiving checks use guarded seed/path replay for all four new +modes: React DbClient, React Suspense, React Suspense with DbClient and Svelte +DbClient. Each records eight successful direct/nested window executions. +Additional guarded checks pass for client peer history, Svelte keyed admission, +React retry ownership, Svelte transported pages and core subset handoff. Source +checking formerly required literal driver names; its explicit finite-product +check now requires React's two boolean axes and exact name expression. Runtime +replays establish those computed names actually execute. + +### Reviewer assessment and final loss audit + +Hire recommendation: positive for correctness review. All three actionable +findings are valid, and the cancellation finding distinguishes observer data +from retained Collection rows. Its proposed render-local repair is sound. The +replay suggestion also asks to inspect other new names, which exposes the full +32-route omission. The scope warning lacks the conversation's authorization; +the docstring threshold is a bot default. Those warnings have less signal than +the three precise source findings. The reviewer does not itself supply the +RED or mutant evidence needed to validate the repair; the repair adds those instruments. + +The final audit returned to the full review body, all inline bodies and the +mutable summary. The overview repeats the three inline findings. Passed title, +description and linked-issue checks add no hidden obligations. Generic bot +controls offer docstrings, unit tests and automation; they are not additional +technical findings. Accounting: five items = three fixed-now + one +accepted-design + one refuted. No confirmed-open repair, unapproved deferral, +unaccounted footnote or product decision required for this repair remains. +The optional mutable nested-key policy is unchanged. + +The measured typical React application remains smaller than `1443dc92e` by +121 gzip bytes and 73 brotli bytes. React's separately compressed package is +18 gzip bytes smaller. These measurements do not imply that every package or +the sum of separately compressed packages shrinks. Command: +`node scripts/size-compare.mjs --base 1443dc92e --exact-base --packages db,react-db,preact-db`. + +Source snapshot: a431a728438cb265f55001ef18d214794c6d78a543db18285b42b6f4a481e1e1. It covers 27 source/contract/coverage +files and adds the replay enforcement test to the prior 26-file snapshot. +The report and manifest exclude themselves. Earlier manifests remain in +`priorManifests`. PR readiness still requires pushed-head CI; these local +results alone do not establish that status. + +## Immutable explicit-key contract (maintainer decision, 2026-11-03) + +This amendment starts from pushed head `69664f1d9215bfdbe7fb2d31e7e5ee0661506f20`. +The maintainer accepted immutable keys and requested enforcement in the adapter +oracle. Equal immutable plain records and arrays retain the authored query; +record property order does not matter. Replacing a nested value invalidates the +input. Applications replace changed objects along their path on the next host +update. In-place mutation is unsupported and has no guaranteed behavior. +An adapter may happen to detect it without creating an obligation for peers. + +The pre-edit inventory was React, React DbClient, React Suspense, React Suspense +DbClient, Preact and Svelte DbClient, plus the existing secondary React +registration. All seven identity-comparing receivers use the same owner, +`packages/db/tests/conformance/window-history-oracle.ts`. The contract, +public guides, coverage map and replay routes now record the law. Reactive +bindings that compare no identity retain their existing reactive-read contract. +No production edit or additional render-time hash is needed. + +### Law, experiment, inference, and limits + +- **Law:** Immutable key value equality retains the authored input. A changed + nested value admits the proposed input. Authority: the maintainer's explicit + decision in this task and the shared contract's existing explicit-key law. +- **Reach:** Fourteen fixed/fresh campaigns execute 84 generated histories. + Each of eight declared states is reached six times in every campaign: + same key object; fresh outer array; fresh equal nested values; reordered + record properties; changed nested record value; equal key after replacement; + changed nested array value; and return to an earlier key. The minimum is the + full six-run campaign budget, not pooled across receivers. This supplies 672 + declared-state comparisons, plus initial and generated-tail observations. +- **Baseline GREEN:** The new histories pass unchanged production on all seven + receiving modes. This work adds enforcement; it does not claim to repair a + newly discovered runtime defect or manufacture an original-production RED. +- **Mutant RED:** Eight well-typed source mutations fail the shared window + comparisons. React and Svelte each reject reference-only equality, ignored + key changes, and unconditional reauthoring. Preact rejects ignored key + changes and unconditional reauthoring. React's five modes each fail all three + mutations in both campaigns; Svelte and Preact fail their respective modes. + The forty failing campaign tests reject wrong rows at fresh equal nested + values, changed nested record value, or same key object. None is counted by + timeout, setup error or reach failure. +- **GREEN:** Restored production passes all fourteen campaigns. Each mode also + has a guarded seed/path replay. The captured React reference-equality failure + replays RED at seed 2345, path `0:0:0:0:0`; restored source replays GREEN. +- **Limits:** Frozen plain records/arrays with finite integer, string, boolean + and null leaves. Histories change integer leaves under records and arrays. + Native keys, signed zero, undefined, cycles, custom prototypes and mutation + are outside this lane. This does not alter their existing support policies. + Native host settlement is the cut; no speculative-render, pending-demand, + transport-seed or external provider claim follows from this key lane. The + existing receivers retain those neighboring obligations. + +The independent model names key equivalence classes with two integers. One +maps to a leaf under a record, the other to a leaf under an array. It stores the +admitted predicate separately from the proposed predicate. Equal keys can +propose different queries, so that distinction determines the next result. +Rows use the existing independent sort/filter/slice judgment. Neither model +imports a production key comparator, hash or invalidation classifier. +Changed keys with identical plans impose no new Collection-replacement rule. + +The compulsory history prefix reconstructs the supported immutable forms. +Same-key proposals change their candidate predicate, which distinguishes +retaining the authored input from running the latest callback. Independent +record and array replacements prevent one path from masking the other. +Returning to the original frozen key distinguishes current admission from +keeping every earlier authored plan forever. Generated tails vary repeats, +returns, new values, predicates and property order. Removing a prefix form +loses its corresponding equality or replacement witness; removing the changed +candidate behind equal keys hides accidental reauthoring. Without independent +leaf changes, a comparator that ignores one nested path can evade the claim. + +Ranges: three to ten ordered source rows; label length zero to eight; zero to +four scalar payload entries; zero to five tail actions; each model key axis +zero to two; tail predicates minus one through ten. Empty payloads, empty +results and complete results occur. Frozen construction excludes in-place +mutation; the grammar creates no cycles or unsupported key leaves. More +sampling would not establish those excluded domains. + +The actual driver calls each framework's real hook and waits for its declared +native settled cut. Each comparison checks ordered data, keyed state and the +public Collection. Stable keys require Collection retention; a changed +predicate requires replacement. An equal plan after key invalidation leaves +reuse free. Failure fidelity and ScenarioLifetime preserve the first law/cut, +keep cleanup diagnostics distinct and release the mounted input and source. + +An initial Svelte reauthor mutant written as literal `false` failed type setup +because of control-flow inference. That attempt is not a mutant rejection. +The equivalent runtime predicate `queryKey.length < 0` stays well-typed and +fails the intended row comparison. A first test-only type alias used a +fast-check API absent in the installed version; it was corrected before any +runtime result. These setup repairs did not change production behavior. + +### Rubric self-review + +ORC-001: authority and limits appear above and beside the owner. ORC-002: +model-only equivalence classes and independent row recomputation avoid the +production equality machinery. ORC-003: opening/local prose identifies the +contract, model, grammar, driver and comparison. ORC-004: reconstruction, +ablation, bounded ranges and exclusion controls are stated above. ORC-005: +seven real hook paths compare data/state/Collection after native settlement; +all source rows are installed before mount, with no concrete missing-input +hypothesis at that cut. ORC-006: eight typed source mutants fail forty intended +comparisons. ORC-007: fixed seed 2345 and fresh campaigns share grammar, budget +and judgment; all seven routes have guarded direct replay. ORC-008: key +classes and admitted predicate are separately observable by the next proposal. +ORC-009: model-only classes map explicitly to concrete record/array leaves. +ORC-010: existing failure-fidelity and lifetime mechanisms remain in use. +ORC-011: no concrete fault shared by this scalar-class model and production's +rich-value comparator was identified; no second model is claimed. ORC-012: +this versioned amendment records the applicable outcomes and source snapshot. +ORC-013: equal fresh trees versus independently changed record/array leaves +supply adjacent legal distinctions. ORC-014: real host drivers receive the +controlled ready source; no external-provider claim is made. ORC-015: each +campaign asserts six histories for each declared state. ORC-016: all histories +use the same independent model/driver/comparison, without per-example answers. +This is a self-review; no new independent reviewer is claimed. + +### Incremental CodeRabbit review 5755842735 + +The review covers `69664f1d9`. The full raw review and mutable summary are +preserved in the task-local lossless ledger. Its wording suggestion is +**fixed-now**: the earlier sentence now says “RED or mutant evidence needed to +validate the repair.” The summary repeats the scope warning (**accepted-design**, +explicit maintainer authorization) and an 80% docstring threshold (**refuted** +as a repository requirement). Its updated 18.18% metric over 22 functions was +not independently reproduced. Passed checks and generic bot controls add no +findings. Three items are accounted for; no confirmed-open item or deferral +remains. The clarity suggestion is useful; repeated policy warnings lack task +context. The prior positive correctness-review assessment remains, while this +incremental wording review supplies little new correctness evidence. + +Source snapshot for this amendment: `0247b20dac86a5f8da38769860bf7c4d78c2baefab61c87c2b2bb06796f8ef3f`. The companion manifest +records 28 source/contract/coverage files and preserves prior snapshots. +The report and manifest exclude themselves. Production files match `69664f1d9`. + +### Completed validation for immutable keys + +Complete receiving conformance commands pass: React 369 tests, Preact 142, +and Svelte 158. Each command selects the package's conformance file and its +existing infinite-query neighbor. The guarded replay suite passes 117 tests. +Explicit React/Preact TypeScript checks, Svelte check (zero errors/warnings), +ESLint, Prettier, `pnpm lint:changed` and `git diff --check` pass. + +Commands, each with at most two Vitest workers: + +```sh +pnpm --filter @tanstack/react-db exec vitest run tests/conformance.test.tsx --maxWorkers=2 --coverage.enabled=false +pnpm --filter @tanstack/preact-db exec vitest run tests/conformance.test.ts --maxWorkers=2 --coverage.enabled=false +pnpm --filter @tanstack/svelte-db exec vitest run tests/conformance.svelte.test.ts --maxWorkers=2 +pnpm --filter @tanstack/db exec vitest run tests/oracle-replay.test.ts --maxWorkers=2 --coverage.enabled=false +``` + +From `packages/react-db`, the captured calibration uses this direct replay: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=framework.react.immutable-key-history TANSTACK_DB_ORACLE_SEED=2345 TANSTACK_DB_ORACLE_PATH=0:0:0:0:0 node --import tsx ../db/tests/oracle-replay.ts tests/conformance.test.tsx --maxWorkers=2 --coverage.enabled=false +``` + +The task-local `2345-ci-loop` directory retains `immutable-reach-results.json`, +`immutable-mutant-results.json`, `immutable-replay-results.json`, per-mutant +explicit type and RED logs, per-package GREEN/type logs, and conformance logs. +The evidence does not claim that the full monorepo test command ran. + + +## High-effort review of PR #2348: co-mounted Suspense ownership + +This amendment evaluates all ten source-ordered claims supplied by the maintainer. +The reviewed input is `d8bf49f2e2457dcd6da5977570e4bb83afd73eb4`. +Production and oracle repairs follow that commit. The companion manifest records +this amendment's exact source snapshot and preserves the preceding snapshots. +This amendment supersedes earlier closure claims for initial sibling progress, +borrower next-use, and copied-seed isolation. Earlier evidence retains its +original meaning and process qualifications. + +### Laws, experiments and inference + +No new product decision is required. The authority remains the shared +conformance contract and the maintainer's 2026-11-03 decision: independently +authored Suspense queries own distinct successful Collections. Retry work may +be reused before success. Finite outer page changes retain each binding's +Collection, transported snapshots keep their page identities, and speculative +renders cannot change a committed page. Immutable equal keys retain the authored +input. The error-handling guide and existing recovery examples supply the +initial-error, retry and post-success health promises. + +| Law / source IDs | Earlier enforcement | Distinguishing experiment | Result and repair | +| --- | --- | --- | --- | +| Retry progress and exclusive successful outputs, R1/R3/R8 | The peer mounted in a separate root after the first success. Initial shared-boundary retries were absent. | Co-mount both hooks, vary one/two boundaries, equal/different queries, readiness order and source readiness. Check every successful layout commit's Collection ownership. | Original code fails progress in shared-boundary histories. Keep retry work available until commit, reserve it once, and give competing successful candidates private seeded outputs. | +| Page ownership persists through another compiler's retry, R4/R7 | Only the original compiler moved after adoption. | A borrower finishes pending work, original compiler retries, borrower moves first, then original moves. Run borrower-first and co-ready schedules. | Original code replaces the borrower's Collection. Each new retry resource now gets a unique resolver token that follows its adopted output. | +| Copied ready rows cannot follow another binding's page, sibling enforcement gap | Initial seed fixture did not force a read after peer publication. | Keep private startup pending while the peer moves, then rerender both unchanged inputs. Cross source Collections that still report loading with Collections that report ready. | A mutant that refreshes copied rows from the peer observer initially survived. The added render rejects it at ordered rows for the unchanged peer. The repair retains copied rows while tracking source health. | +| A cancelled client page proposal preserves the committed seed, R2/R8 | Already enforced by the interrupted-render client receiver. | Speculative offset/limit/combined move, post-hook suspension, urgent original-page render with local readiness held. | Current input passes all 48 campaigns. Reintroducing render-time observer/hash assignment fails all 48. No additional production repair is needed. | +| Equal client keys retain authored input through error recovery, R5/R6 | General immutable-key and rejection owners covered the separate rules. | Declarative move, imperative move, throwing key read twice, throwing native pre-effect, equal/changed-key recovery with changed body/deps. | Original Svelte passes. Removing stable-key retention fails the retained imperative page. No Svelte production change is justified. | + +The React lifecycle owner reuses the independent sort/filter/slice page judgment +for both outputs. Its expectation contains no retry cache, observer, resolver +token or ownership algorithm. Public progress and distinct-output relations +express the lifecycle promise separately. The page receiver checks rows, +keyed state and Collection retention at the borrower and original binding's +next-use cuts. The Svelte receiver uses that same page judgment at native rune +and effect settlement. One framework's scheduling result supplies no credit +for another's scheduler. + +### Lossless finding ledger + +| ID | Original claim and proposed action | Technical verdict / evidence | PR action and durable destination | +| --- | --- | --- | --- | +| R1 | `useLiveQuery.ts:773`: render-time `claim()` removes ready work before a shared Suspense boundary commits. Staggered siblings can continually rebuild. Move claim to an effect. | Confirmed, high severity. On unchanged production, controlled on-demand shared-boundary siblings fail the bounded retry-progress comparison. | **fixed-now**. Cache eviction runs at commit. First-candidate reservation and private completed seeds also preserve successful output independence; moving all ownership work to an effect alone is insufficient. Lifecycle sibling grammar and render-eviction mutant preserve the finding. | +| R2 | `useLiveQuery.ts:716`: speculative client move replaces observer, subscribe and hash before identity commits. An urgent old-page render can consume the wrong seed/client record. | Real earlier defect, stale at the reviewed input. Commit `69664f1d9` already keeps observer/hash local until commit. Same-path interrupted-seed campaigns pass; old assignments fail all 48. | **already-fixed**. Retain the client-window cancellation receiver and exact seed observation. | +| R3 | `useLiveSuspenseQuery.ts:124`: unguarded `claim()` lets the last borrower steal ownership. Co-ready A/B then share B's moved page and A suspends again. Guard owner/release. | Refuted as a whole-hook counterexample. The method was unguarded, but old `ownedByPeer` admission prevents a ready peer from taking another successful output. The mounted replacement/borrower co-ready probe reaches shared pending work and same-pass readiness, observes distinct outputs, then fails only on R4's next-use retention. | **refuted**. Keep the co-ready history and every-commit ownership comparison. The new implementation reserves once explicitly, preserving the useful defensive idea. No reproduced transient page theft is claimed. | +| R4 | `useLiveSuspenseQuery.ts:101`: adopted entry retains its compiler's window token. Compiler retry overwrites the borrower's resolver state, breaking its next offset-only reuse. | Confirmed, high severity. Original source fails Collection retention at `borrower page after original compiler retries`. The same-token hostile mutant reproduces it. | **fixed-now**. Unique token per retry resource, retained through adoption. Both next-use directions remain in the page owner. | +| R5 | `useLiveQuery.svelte.ts:418`: equal-key recovery returns an old move closure after a derived error, replaying the declarative move. | Refuted by the same native path: declarative move, distinct imperative page, two throwing public reads, throwing `flushSync`, then same-key recovery. The imperative page remains intact. | **refuted**. Preserve generated error/recovery crossings in the Svelte client receiver, including changed-key neighbors and both body/dependency proposals. | +| R6 | `useLiveQuery.svelte.ts:418`: stable client keys ignore query body/deps and enabled-to-false changes, unlike nonclient Svelte. | Body/deps retention is the approved identity-comparing law, not a defect. Nonclient Svelte follows reactive inputs. The typed config has no `enabled` option and its config query cannot return undefined. Ordinary callback disablement retains its existing separate contract. | **accepted-design**. The shared contract and native recovery grammar distinguish equal-key retention from changed-key admission. No unsupported config field becomes a new law. | +| R7 | `live-query-options.ts:441`: null versus undefined re-prepares already prepared values, tracks client/Suspense Collections contrary to line 313, and contributes to R4. Add a flag. | Partly valid documentation issue. Null intentionally starts owned tracking; undefined opts out. Re-preparation strips wrappers without invoking the author callback twice or rebinding an already bound builder. R4 requires a token-lifetime repair, not exclusion of these outputs. | **fixed-now** for stale prose. Keep the existing sentinel interface, explain its meaning and tracked ownership. The page retention mutant protects the behavior; source review suffices for the comment repair. | +| R8 | `suspense-lifecycle-oracle.test.tsx:413`: old peer lane misses co-mounted/shared-boundary/co-ready/urgent schedules. R1–R4 pass. Add grammar and render-claim mutant. | Confirmed coverage overclaim for R1/R4. R2's exact cancellation receiver already exists and rejects the old observer mutation, so the claim that all four escape every campaign is too broad. | **fixed-now**. Add shared/separate boundaries, readiness schedules, mounted co-ready replacements, borrower next-use and post-publication seed reads. Declare and measure reach per mode; calibrate the wider law with multiple hostile implementations. | +| R9 | `suspense-local-options-oracle.test.tsx:42`: equality model no longer affects ownership verdict. `_identity` is unused in indexing checker. | Confirmed stale declaration/parameter. `equalLocalDeclarations` still classifies reach, so the helper itself is not dead. Independent outputs no longer depend on effective equality. | **fixed-now**. Remove obsolete model declaration and checker parameter. Keep the equality reach classifier and all identity grammar axes. Existing local-options/indexing campaigns remain. | +| R10 | `useLiveSuspenseQuery.ts:437`: local options still qualify a sharing key despite independent successful outputs. Trim key or fix prose. | Confirmed stale prose. Compatible local options still matter when adopting pending retry work. Removing them could adopt an incompatible callback or output config. | **fixed-now**. Comment now names pending retry adoption. Existing local-option/indexing owners preserve compatibility and independent successful outputs. | + +Totals: **6 fixed-now + 1 already-fixed + 2 refuted + 1 accepted-design = 10**. +No raw finding, proposed fix or useful test idea is omitted. No deferral or +unresolved product decision remains. The separately discovered seed-read gap +is repaired and calibrated; it is not silently counted as an eleventh raw item. + +### RED before production, neighboring regressions and hostile calibration + +Before the first production edit, explicit React and Svelte `tsc --noEmit` +checks pass. The expanded real-host receiving histories run on unchanged +`d8bf49f2e`: 20 React assertion failures (12 sibling progress campaigns and +eight borrower-next-page campaigns), with 258 passing tests. All 48 existing +client cancellation campaigns pass. Native Svelte recovery also passes on +unchanged production, including the throwing pre-effect. Earlier untyped or +incomplete diagnostic attempts supply no product evidence. + +The first repair made every successful candidate fork. Wider tests rejected +extra warm preload work and an incorrect server snapshot read. The next repair +kept the first candidate, forked competitors, and used the server snapshot API +on the server. Co-ready mounted replacements then exposed a skipped ownership +admission for an unclaimed ready resource. The final single-entry ownership +state fixes that admission and captures each committed render's release, so a +replayed older effect cannot release a newer suspended attempt. Existing warm +preload, request failure and SSR stream-cut expectations remain unchanged. + +Each credited source mutant passes explicit package typechecking first. These +are assertion failures at the intended checkpoints, not setup errors or timeouts: + +| Mutant | Rejected observation | +| --- | --- | +| Original reviewed React sources | Sibling retry progress and borrower Collection retention; final expanded receivers fail 20 campaigns. | +| Release retry resource during render | Shared-boundary progress or moved-page rows after discarded candidate work; all 12 sibling campaigns fail. | +| Use compiler binding as every new resource token | Borrower Collection retention after compiler retries; eight borrower-first campaigns fail. Co-ready neighbors remain distinct. | +| Keep shared ready output until an ownership effect | Every successful layout commit must own an independent Collection; all 12 sibling campaigns fail. | +| Refresh copied seed data/state from its peer's live observer | Unchanged peer rows after the successful peer moves and both hooks rerender with private startup held; all 12 sibling campaigns fail. | +| Assign page observer/hash during render | Matching committed seed after cancelled page; all 48 client-window campaigns fail. | +| Remove Svelte stable-key retention | Retained imperative page at recovered client key; both recovery campaigns fail. | + +A literal-false Svelte mutant failed TypeScript setup and receives no credit. +The well-typed runtime-false predicate replaces it. The live-seed mutation +survived two earlier fixtures, including the first lagging-source version. +Lagging readiness alone did not force a post-move read. The final unchanged-input +render provides that missing observation. No surviving mutant is reported as a +kill, and no comparison was relaxed to obtain GREEN. + +### Declared reach, bounded grammar and observation cuts + +The sibling property runs eight generated families per campaign. Each family +contains its unconstrained history plus three structurally required neighbors: +shared-boundary equal query with lagging source readiness, shared-boundary +staggered different queries, and separate-boundary equal queries with ready +sources. This produces 32 histories in each of 12 fixed/fresh campaigns. +Fixed seed is 2348. Limits are widths 1–3, page offsets 1–4, 12 source rows, +one/two source Collections, and at most four controlled retry drains. The drain +comparison establishes progress in this host schedule, not a general time bound. + +Every campaign reaches co-mounted pending peers, both committed, exclusive +outputs and retained peer move **32 times**. The following table reports the +remaining declared states. Each seed-isolation count also equals private-startup +handoff reach. Every state has an asserted minimum of one per receiving campaign. + +| Client scope / key / campaign | Shared / separate boundary | Same / different query | Staggered / same-pass readiness | Seed isolation / handoff | Lagging / ready source | +| --- | --- | --- | --- | --- | --- | +| none/derived/fixed | 20 / 12 | 21 / 11 | 9 / 23 | 9 / 9 | 18 / 14 | +| none/derived/fresh | 21 / 11 | 16 / 16 | 13 / 19 | 8 / 8 | 20 / 12 | +| none/keyed/fixed | 20 / 12 | 21 / 11 | 9 / 23 | 9 / 9 | 18 / 14 | +| none/keyed/fresh | 21 / 11 | 20 / 12 | 9 / 23 | 11 / 11 | 14 / 18 | +| provider/derived/fixed | 20 / 12 | 21 / 11 | 9 / 23 | 9 / 9 | 18 / 14 | +| provider/derived/fresh | 20 / 12 | 21 / 11 | 10 / 22 | 11 / 11 | 16 / 16 | +| provider/keyed/fixed | 20 / 12 | 21 / 11 | 9 / 23 | 9 / 9 | 18 / 14 | +| provider/keyed/fresh | 19 / 13 | 21 / 11 | 8 / 24 | 12 / 12 | 24 / 8 | +| explicit/derived/fixed | 20 / 12 | 21 / 11 | 9 / 23 | 9 / 9 | 18 / 14 | +| explicit/derived/fresh | 19 / 13 | 21 / 11 | 10 / 22 | 12 / 12 | 16 / 16 | +| explicit/keyed/fixed | 20 / 12 | 21 / 11 | 9 / 23 | 9 / 9 | 18 / 14 | +| explicit/keyed/fresh | 22 / 10 | 20 / 12 | 11 / 21 | 9 / 9 | 18 / 14 | + +The retry-ownership receiver has 16 campaigns: client/unscoped × keyed/derived × +borrower-first/co-ready × fixed/fresh. Each reaches all five declared states +seven times: replacement suspended, peer ready, replacement resumed, borrower +moves after compiler retries, and page isolated. Fixed seed remains 2345 and +existing run budgets remain intact. + +Svelte recovery generates 24 numeric histories per campaign and crosses each +with body/dependency proposals and same/changed keys. Each campaign therefore +runs 96 native histories. Committed move, imperative move and rejected read +reach 96 times. Same-key and changed-key recovery, body and deps each reach 48. +Each of the four joint key/proposal states reaches 24. Fixed seed is 2348. +Widths and declarative offsets are 1–3, imperative offsets 4–6, and the admitted +replacement page is nine. These disjoint pages make replayed moves observable. + +Reconstruction: shared initial retries reproduce R1; the mounted replacement +with borrower-next-use reproduces R4; native throwing key recovery reaches +R5's stated path. Ablation: separate-only roots lose R1, compiler-only moves +lose R4, no unchanged-input render admits the live-seed mutant, eager readiness +can hide private startup, and separate marginal Svelte reaches could miss +same-key dependency recovery. The grammar preserves these crossings explicitly. +Range neighbors vary both page width and nonzero offsets, readiness order, +client/key identity, equal/different plans, and key retention/admission. +Exclusions: no negative/nonfinite windows, disabled Suspense input, supplied +shared Collection ownership, unsupported Svelte config field or key mutation +is generated. Those are not silently classified as successful histories. + +### Rubric self-review and remaining limits + +ORC-001: the accepted contract and maintainer decision authorize each relation. +ORC-002: pure row recomputation and public ownership/progress relations do not +copy production caches or resolver logic. ORC-003: owner prose separates law, +grammar, production driver and observation. ORC-004: reconstruction, ablation, +ranges and exclusions appear above. ORC-005: real React boundaries/layout +commits and Svelte rune/effect cuts supply the receiving path; held requests and +installed rows give the needed facts at each named cut. ORC-006: typed mutants +fail the intended comparisons, with setup failures and survivors recorded. +ORC-007: fixed/fresh campaigns share grammar, run budgets and comparison; guarded +seed/path replay selects the exact owner. ORC-008: no independent model state +was added. The existing admitted-page distinction is observable at next-use. +ORC-009: the existing model-only attempt abstraction is unchanged; page and +framework binding use glossary terms. ORC-010: failure fidelity and lifetime +cleanup preserve the original law/cut and secondary diagnostics. ORC-011: +no concrete shared semantic fault between independent slicing and production +ownership was identified; no new second-formulation claim is made. ORC-012: +this versioned amendment and source manifest retain evidence and process limits. +ORC-013: equal versus changed keys, equal versus different queries, ready versus +lagging sources, and both next-use directions distinguish neighboring wrong +rules. ORC-014: actual React/Svelte supply scheduling, with controlled finite +sources. No external provider claim follows. ORC-015: every required receiving +campaign asserts and reports its own reach. ORC-016: generated histories all +use shared judgments without per-example expected answers. This is a self-review. + +The repair preserves existing SSR stream cuts, warm preload work, local output +options, failures and source health, client seed cancellation, immutable keys +and finite page retention. No additional per-render hash was added. This +extension enters no correlated materialization implementation and changes no +core materialization law. No oracle run budget was lowered or removed. + +The claim remains bounded to React 19/jsdom and Svelte's native rune/effect host, +finite query shapes and controlled on-demand acquisition settlement. Existing +SSR owners cover their own stream cuts. No arbitrary concurrent schedule, +React 18 client schedule, native hydration, Electric/Start provider timing, +exact GC deadline, nonfinite-window law or heavy-query benchmark follows. +These are evidence limits, not waivers for known in-scope violations. + +### Reviewer assessment + +**Hire for correctness review, with calibration feedback.** The reviewer found +two consequential production bugs and the missing histories that concealed +them. R1/R4/R8 show strong ownership and lifecycle analysis, and their priority +was appropriate. R7/R9/R10 supply useful cleanup. Accuracy is uneven: R2 was +already fixed, R3 infers an unreachable whole-hook outcome from one method, +R5 fails a native same-path recovery probe, and R6 misses the approved identity +contract and typed config boundary. Moving claim into an effect alone also +fails the independence law. Require executable counterexamples and current-head +verification before treating all ten ranked items as blockers. + +### Final validation and PR status + +All commands use at most two Vitest workers, with Vitest jobs sequential. +Full React passes **4,823 tests / 40 files**. Full Svelte passes **542 / 16**; +after strengthening the key/proposal Cartesian family, its two recovery +campaigns pass again with 96 histories each. Final sibling campaigns pass all +12 modes after adding stable law/checkpoint fingerprints. Core indexing plus +guarded replay passes **362 tests / two files**. Explicit React/Svelte +TypeScript, formatting, ESLint and `pnpm lint:changed` pass. Production sources +are restored after every mutation. No dependency workaround entered this diff. + +```sh +pnpm --filter @tanstack/react-db exec vitest run --maxWorkers=2 --coverage.enabled=false +pnpm --filter @tanstack/svelte-db exec vitest run --maxWorkers=2 --coverage.enabled=false +pnpm --filter @tanstack/db exec vitest run tests/conformance/indexing-reuse-oracle.test.ts tests/oracle-replay.test.ts --maxWorkers=2 --coverage.enabled=false +pnpm exec tsc --noEmit -p packages/react-db/tsconfig.json +pnpm exec tsc --noEmit -p packages/svelte-db/tsconfig.json +pnpm lint:changed +pnpm size:compare --base 1443dc92e5417394acbdc98b782caa9802848ce6 --exact-base --packages db,react-db,preact-db +``` + +From `packages/react-db`, this captured live-seed mutant failure replays RED +and restored production replays GREEN at the same row checkpoint: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=react.suspense-siblings TANSTACK_DB_ORACLE_SEED=2348 TANSTACK_DB_ORACLE_PATH=0:0:0:0:0:0:0 node --import tsx ../db/tests/oracle-replay.ts tests/suspense-lifecycle-oracle.test.tsx -t 'co-mounted siblings/none/false/fresh' --maxWorkers=2 --coverage.enabled=false +``` + +Guarded seed 2348 / path `0` also passes for all selected receiving campaigns +of `react.suspense-siblings`, `react.window-retry-ownership` and +`svelte.client-window-recovery`. Each route executes its actual owner, skips +the fixed campaign and records nonzero requested property execution. Existing +manifest checks pass with both newly registered properties. + +The measured tree-shaken app remains 61 bytes smaller with gzip and 94 bytes +smaller with Brotli than base `1443dc92e`. This is not an every-package claim. +The all-exports React package grows 245 gzip / 198 Brotli bytes. Across the +measured db/react-db/preact-db exports, the total grows 268 gzip / 292 Brotli +bytes. The ownership repair's extra state is visible in that measurement. + +The item accounting and authorized production/oracle repairs are complete. +Final pushed-head CI status is separate from this finite evidence. The full +monorepo `pnpm test` command did not run, and its PR checklist remains unchecked. + +Source snapshot for this amendment: `d70fdf29acd5de0c9e24322f6a5e271a93e35d08ac1300657101c3be7781d657` across 29 source, grammar, contract and coverage files. The companion manifest records each file hash, excludes itself and this report, and preserves the earlier manifests. + + +## CodeRabbit review 5756985798: failed private startup under a copied result + +The reviewed input is `2c605f92bd26ce722448446f6102b5e19d7f6919`. +The complete formal review, associated inline 4458423132, and mutable summary +6608664047 at 17:48:08 UTC were preserved before experiments. This amendment +records their lossless dispositions. It supersedes earlier sibling closure +claims for failed private startup; all earlier process qualifications remain. + +### Law, distinguishing histories and pre-edit evidence + +A copied ready snapshot is provisional until the owned query reaches +initial-query readiness. Its failed first load must end the seed and reach +React's ErrorBoundary. After owned readiness, an ordinary source problem keeps +the data and appears in health. Authority: materialization architecture laws +10/16, the observer hydration oracle's failure-ending-seed laws 1–2, the +error-handling guide's Suspense boundary contract, and the hook JSDoc. + +The earlier sibling receiver fulfilled private startup but never rejected it. +Its public view model also did not distinguish copied readiness from owned +initial-query readiness. The model now keeps that one additional fact. A seed +and an owned ready result can show equal rows, but the next first-load failure +requires different boundary observations. The model has no cache, resolver +or observer algorithm. Existing source failures after owned readiness remain +health observations; no comparison or campaign budget is removed. + +Before production edits, the existing sibling grammar was extended with held +private first acquisitions and Error/record/undefined rejections through real +React boundaries in all six client/key modes, in fixed and fresh campaigns. +Explicit React typechecking passed. Running +`pnpm --filter @tanstack/react-db test tests/suspense-lifecycle-oracle.test.tsx --maxWorkers=2` +on unchanged `2c605f92b` failed all 12 sibling campaigns at +`current Suspense attempt` / `private startup rejects after seed commit`: +expected `error`, observed `ready`. All 18 neighboring lifecycle tests passed. +The public private Collection was verified `loading` before rejection and +`error` afterward. The failure did not require the peer or source to fail. + +An earlier diagnostic used the general post-data health rule and found the +hidden private failure, but that did not settle which view was owed. Before +production changes, the narrower seeded-observer authority corrected that +interpretation. The credited pre-edit RED uses the boundary rule above. +The first repair exposed a driver-input mistake: subset acquisition normalizes +non-Error provider rejections before the adapter receives them. This receiver +now takes the Collection's public failure cause as input, then checks boundary +Error identity/wrapping through the same model and comparison. It does not +reimplement core normalization or claim its raw provider-cause semantics. +The original pre-edit failure was already at the phase comparison, before +that cause check. Later calibration also rejects the original source with this +corrected input boundary. + +Production now ends a copied seed on `isLoadingError` and clears the hook's +ready latch at that same boundary. Clearing only the seed still returns a +successful hook result because the latch blocks error handling. No hash, +public API, Svelte production code or extra retry state is added. + +### Generated reach and hostile calibration + +At this review boundary, each generated family contains six histories: its +unconstrained member, three existing shared/separate and equal/different +neighbors, and two additional private rejection schedules before and after a +peer page move. The shared-boundary equal-query neighbor also covers fulfilled +private startup; that case is not a seventh member. +Eight families produce 48 histories per campaign. The existing page comparison +still checks seed rows, ordered rows, state and independent/retained Collections. +The lifecycle comparison checks the real boundary and normalized public cause +after rejection. The pre-move schedule was added during calibration, after the +initial production repair; it is not claimed as part of the original RED gate. +Each declared new state has minimum one per receiving campaign. + +| Client / keyed / campaign | Histories | Private pending after success | Private rejection | Before peer move | After peer move | +| --- | --- | --- | --- | --- | --- | +| none/false/fixed | 48 | 25 | 16 | 8 | 8 | +| none/false/fresh | 48 | 25 | 17 | 8 | 9 | +| none/true/fixed | 48 | 25 | 16 | 8 | 8 | +| none/true/fresh | 48 | 27 | 19 | 11 | 8 | +| provider/false/fixed | 48 | 25 | 16 | 8 | 8 | +| provider/false/fresh | 48 | 26 | 16 | 8 | 8 | +| provider/true/fixed | 48 | 25 | 16 | 8 | 8 | +| provider/true/fresh | 48 | 26 | 18 | 9 | 9 | +| explicit/false/fixed | 48 | 25 | 16 | 8 | 8 | +| explicit/false/fresh | 48 | 27 | 18 | 8 | 10 | +| explicit/true/fixed | 48 | 25 | 16 | 8 | 8 | +| explicit/true/fresh | 48 | 26 | 17 | 8 | 9 | + +Both source mutants pass explicit React typechecking, then all 12 sibling +campaigns fail the boundary phase comparison: (1) restore the original seed +handling; (2) end the seed on error but leave the ready latch set. These are +assertion failures, not setup errors, timeouts or missed paths. The restored +source passes all 30 lifecycle tests. Direct replay of +`react.suspense-siblings`, seed `2348`, path `0:0:0:0:0:0:0:0`, filter +`co-mounted siblings/none/false/fresh`, gives original-source RED and repaired +GREEN. Replays use `TANSTACK_DB_ORACLE_PROPERTY`, `TANSTACK_DB_ORACLE_SEED` +and `TANSTACK_DB_ORACLE_PATH`; no normal campaign runs first. + +### Finding ledger and reviewer assessment + +| ID | Original claim / proposed action | Technical judgment and PR action | Durable destination | +| --- | --- | --- | --- | +| CR1 | Report row 1371 uses ambiguous source-readiness and mutant phrases; make both explicit. | **fixed-now**, prose only. The row now states loading versus ready Collections and names copying peer rows as the mutant behavior. | Existing report row; no behavioral test needed. | +| CR2 | A private output's failed first load stays hidden by the copied seed and latched readiness; ending the seed alone is insufficient. Surface the initial error and reset/bypass readiness. | **fixed-now**, confirmed through all client/key receivers. End both seed and ready latch; retain post-owned-readiness health behavior. | Lifecycle model, sibling grammar/reach, boundary comparison, typed original and seed-only mutants. | +| S1 | Scope warning asks to split Suspense, keyed enablement, hydration handoff and immutable keys from issue 2345. | **accepted-design** under the maintainer's explicit requests in this session. The issue scope is narrower than the authorized PR scope. | PR behavior/scope description and preceding decision records. | +| S2 | Default docstring warning reports 18.52%, 27 functions/26 files, two unsupported, against an 80% threshold; add blanket docstrings. | **refuted** as a repository requirement. The summary applies defaults and no repository configuration; no such repository gate exists. The reported metric is preserved, not independently reproduced. Required law/model/driver prose is retained. | Full raw summary and prior default-warning assessments. | +| S3 | One evidence file was not fully reviewed; coverage/manifest skipped as similar, two files had no reviewable changes. | **refuted** as a code-repair obligation; accepted as external-review coverage metadata. Do not call the whole report externally reviewed. | This review limit and the independently checked source manifest. | + +The summary repeats CR2; that duplicate links to its inline finding without +creating a second repair obligation. Passed checks, generic bot controls and +promotional links add no findings. All five source claims are accounted for: +two fixed-now, one accepted-design, two refuted as repair obligations; zero +confirmed-open, deferred or unresolved design decisions. + +Reviewer assessment: hire for correctness review. The seed/latch finding is +accurate, local and includes the needed warning against an incomplete fix. +Its static evidence needed the executable receiving witness supplied here. +The scope/default docstring warnings add little signal under this task's +explicit authorization. The prose clarification is valid and inexpensive. + +### Evidence bounds and self-review + +This is a self-review, not an independent review. ORC-001/002 trace the law to +seeded failure authority and keep the model independent; core's public failure +is the declared input boundary. ORC-003/009 keep model, source facts, grammar, +host driver and comparison distinct. ORC-004/008 reconstruct both rejection +schedules and preserve the seed/owned distinction because the next failure +separates them. Removing rejection, timing or owned readiness loses those +witnesses. Widths 1–3 and offsets 1–4 retain their existing page domain; +rejection after a demand aborted is excluded because it has no authority. +ORC-005/014 use real React 19/jsdom boundaries, public private status/health +and actual source promises; no external provider claim follows. ORC-006/007 +have typed hostile assertion failures, matching fixed/fresh budgets and direct +RED/GREEN replay. ORC-010 retains failure fidelity and ScenarioLifetime cleanup. +ORC-011 has no named shared semantic fault needing a second independent model; +the existing owned-readiness health histories distinguish the rival rule that +all failures throw. ORC-012 records finite scope and source snapshot here; +ORC-013/015 have both first-failure and post-ready neighbors plus measured reach. +ORC-016 keeps the regression in the shared model/driver/comparison, without a +separate hand-written expected answer. + +Limits: finite one-source queries and controlled subset work in React/jsdom; +no arbitrary scheduler, native hydration, external provider, raw provider-error +normalization, crash-recovery expansion or exact garbage-collection claim. +The core hydration/status owners retain their neighboring contracts. This +amendment does not erase the original Svelte pre-edit type-gate qualification. +Green local checks and complete review accounting do not establish green CI or +whole-PR readiness at a newly pushed head. + +Final local verification for this follow-up: full React `pnpm --filter @tanstack/react-db test --maxWorkers=2` passes **4,823 tests in 40 files**, including all 30 lifecycle campaigns, with no type errors. Explicit React `tsc --noEmit`, focused ESLint, Prettier and `pnpm lint:changed` pass. The same shared page and conformance suites run in that full package command. No Svelte runtime changed; its preceding 542-test run remains the recorded Svelte evidence. The full monorepo `pnpm test` command was not run. Raw RED, GREEN, type, reach, hostile-mutant and replay logs remain in the task-local `2345-ci-loop` directory under the `5756985798-` prefix. + +Source snapshot for this follow-up: `c970caa0f5dc74b6fbc04da7c1092cf57ea7d2ec2fe7c05cbf195534bf3f79f5` across 30 files. The companion manifest preserves the preceding snapshots and records every source hash. + + +## Second high-effort review and query-local seed health + +The user supplied ten unverified findings against `2c605f92b`. Evaluation started +on `3cb5df960508663d6b6b4c1e4d28a37cdde6f9f9`, which already fixes the private +first-load failure in item 2. The raw review and all source-order claims were +preserved before experiments in `/private/tmp/2348-high-review-r2/`. This entry +records the complete evaluation and resulting source snapshot. Earlier evidence, +including the original Svelte pre-edit type-gate qualification, remains intact. + +### Laws and the oracle-first experiment + +No product law changed. The shared conformance contract promises retry progress, +exclusive successful outputs and retained complete pages during pending window +moves. It explicitly does not promise retention of an uncommitted Collection. +The maintainer requested independent implicit query outputs, including Suspense. +The proposed common-case sharing in item 7 conflicts with that decision. + +The glossary's health tree and request-error definitions distinguish source +health from a query's own failed demand. Source reports reach each successful +binding without an unrelated host render. A peer's page-request failure has no +authority over a private output's health. Copied rows remain provisional under +the existing first-load failure law. Those laws interact: copying all of a +peer's changing snapshot would break both page isolation and health ownership. + +Before production edits, the existing sibling receiver gained post-hook holds +and source report/recovery observations. The mounted replacement receiver gained +peer-first admission and after-hook holds. Generated histories reach shared +boundaries, discarded successful candidates, a peer committing before a held +candidate, and a mounted owner changing to a plan a peer already awaits. Public +progress, distinct Collections and each binding's next page move are compared. +Acquisition and returned-Collection counters are diagnostic observations, not +an invented exact request budget. The shared pending receiver now also checks +that a native Suspense fallback does not hide the retained complete page. + +Explicit React `tsc --noEmit` passed. On unchanged `3cb5df960`, the first extension +reported 258 passes and 12 failures because the new checker expected undefined +for cleared `failureReason`. The public representation is null. Correcting that +model mistake gave **270/270** receiving tests and **10/10** pending-page tests. +This is disproof/neighbor evidence, not a production RED. Source reports and +recovery reached the seeded output without forced renders, refuting item 5's +source-notification claim. Same-path retry histories made progress with exclusive +outputs, while counters confirmed finite extra acquisition work in items 1/3/4. + +Challenging the neighboring health law exposed a new defect, S1. The grammar +holds private startup, rejects the peer's new page demand after peer readiness, +then re-reads unchanged inputs. The peer remains ready with its own request +problem, the source remains healthy, and the private initial acquisition remains +held. Its committed hook health nevertheless contains the peer's root error. +The observer already supplies the correct own-query health at this checkpoint. +The seed replaces it with the peer's health: production is the repair location. + +Before any behavioral source edit, the explicit type gate passed and all **12** +client/key sibling campaigns failed at `query-local request health` / +`seed after peer request failure`. The public private root error was non-null +where this query had no failed demand. Command: + +```sh +pnpm --filter @tanstack/react-db exec vitest run tests/suspense-lifecycle-oracle.test.tsx -t 'co-mounted siblings' --maxWorkers=2 --coverage.enabled=false +``` + +Production now copies only seed rows. The private observer supplies health and +notifications, with the existing ready-view status helper deriving its flags. +This removes the `seedObserver` field and its stale reference. Retry-cache +lifetimes remain subject to their existing cleanup policy. The qualified +retry key is computed only if lookup or replacement needs it. One existing +commit effect now releases a successful retry entry and commits window moves. +Its original placement stays before the external-store hook. Snapshot callbacks +read their observer once and apply the optional Suspense view explicitly. No +extra render hashing, public API or Svelte runtime change is introduced. + +### Enforcement and measured reach + +The repaired source passed **639** lifecycle/render/conformance tests. The final +reach run passed all 60 selected sibling/retry campaign tests. Each sibling +family now has ten members: the six historical members, a ready-source health +member, a peer request-failure member and two post-hook hold members. Eight +families give 80 histories per campaign. Existing budgets were not lowered. + +| Mode | Histories | Post-hook discard | Peer commits first | Source report before move | Source report after move | Source recovery | Peer request isolation | +| --- | --- | --- | --- | --- | --- | --- | --- | +| none/false/fixed | 80 | 19 | 10 | 11 | 8 | 11 | 9 | +| none/false/fresh | 80 | 18 | 10 | 8 | 8 | 8 | 9 | +| none/true/fixed | 80 | 19 | 10 | 11 | 8 | 11 | 9 | +| none/true/fresh | 80 | 20 | 9 | 8 | 8 | 8 | 8 | +| provider/false/fixed | 80 | 19 | 10 | 11 | 8 | 11 | 9 | +| provider/false/fresh | 80 | 21 | 13 | 8 | 8 | 8 | 8 | +| provider/true/fixed | 80 | 19 | 10 | 11 | 8 | 11 | 9 | +| provider/true/fresh | 80 | 20 | 11 | 8 | 8 | 8 | 8 | +| explicit/false/fixed | 80 | 19 | 10 | 11 | 8 | 11 | 9 | +| explicit/false/fresh | 80 | 18 | 8 | 10 | 9 | 10 | 8 | +| explicit/true/fixed | 80 | 19 | 10 | 11 | 8 | 11 | 9 | +| explicit/true/fresh | 80 | 20 | 10 | 8 | 8 | 8 | 8 | + +All 48 mounted replacement campaigns reach their declared states. Each retains +the original compiler-first histories and adds peer-first and post-hook schedules, +with client/unscoped, keyed/derived, co-ready/borrower-first and fixed/fresh modes. +The original six generated histories plus one fixed example per campaign remain. + +Two hostile sources pass explicit typechecking and each fail all 12 sibling +campaigns at the intended assertion. Restoring the original peer-health copy +fails request isolation. Freezing seed health instead fails source notification. +The first frozen mutant left an unused import and failed its type gate. After +that correction, calibration exposed a checker defect: toBeDefined permits null, +so the checker dereferenced a missing problem. Requiring a truthy problem makes +the final calibration an assertion failure, not that accidental TypeError. +Neither diagnostic is counted as a hostile kill. + +Direct replay of `react.suspense-siblings`, seed `2348`, path +`0:0:0:0:0:0:0:0:0`, filter `co-mounted siblings/none/false/fresh`, rejects the +original source and passes the restored repair. Replay uses the standard +property/seed/path environment variables and runs the requested trace directly. +Raw type, RED, GREEN, replay, work and reach logs are in the task-local R2 folder. + +### Lossless finding ledger + +| ID | Claim | Technical judgment and PR action | Durable value | +| --- | --- | --- | --- | +| R2-1 | Render reserves an owner that can be discarded, causing another Collection and load. | **accepted-design** for uncommitted retention. Reservation and extra work are real. Shared-boundary staggered and post-hook histories still progress with distinct successful outputs. This is not the prior render-time eviction livelock. No exact pre-commit work promise exists. | Generated discarded-candidate schedules and measured acquisition/returned-Collection counts preserve the optimization question. No exact GC claim. | +| R2-2 | Private first-load error remains hidden by its seed. | **already-fixed** in `3cb5df960`. Valid at reviewed `2c605f92b`; previous entry records 12 pre-edit failures, repaired GREEN and two typed seed/latch mutants. Current receivers preserve both failure timings. | Existing provisional-readiness model and private rejection histories. | +| R2-3 | A private output inherits the shared release and can evict it before its original candidate commits. | **accepted-design** for uncommitted retention. The source mechanism is real. Separate-boundary post-hook holds let the peer commit first, then the held candidate resumes with correct independent data. An abandoned initial candidate can compile again. | Retain work diagnostics and both participant observations. A new exact preload-retention budget would require a contract decision. | +| R2-4 | Mounted replacement overwrites a peer's pending cache entry. | **accepted-design** for uncommitted retention. Peer-first receiving histories measure two upstream subscriptions before settlement and check both subsequent owned page moves. No stuck fallback or output alias was observed at the stated finite cuts. | Peer-first admission dimension now belongs to the primary mounted replacement receiver. | +| R2-5 | Lack of seedObserver subscription prevents health notification. | **refuted** for shared source health: report/recovery reaches committed output without a host render, before and after peer move. The own observer supplies notification. Peer-local request errors should not propagate at all; that neighboring defect is S1 below. | Explicit source notification/recovery and query-local isolation comparisons. | +| R2-6 | Client Suspense returns old page while new page is pending instead of falling back. | **accepted-design**: pending moves keep the last complete window under the shared contract. Ten native pending campaigns pass, including client Suspense without transported page seeds. Initial readiness still uses fallback. | Added native fallback assertion prevents stale renderHook output from masking loss of the visible page. | +| R2-7 | Independent outputs perform multiple acquisitions. Share until first move instead. | **accepted-design** under the explicit maintainer decision for independent successful implicit queries. Multiple loadSubset calls are measured; these are acquisition attempts, not necessarily provider transports. Lazy shared mutable outputs reverse the approved ownership rule. | Preserve counts without promoting one request per arbitrary retry history into a new law. | +| R2-8 | Qualified key computed before an owned retained move returns. | **fixed-now**, mechanical: defer key computation until lookup or replacement needs it. | Direct source order plus retained-window receiving comparisons. | +| R2-9 | Recompilation clears seed but retains seedObserver. | **fixed-now**: remove seedObserver entirely as part of S1. The field retained a peer reference; no experiment claims an exact GC lifetime. | The obsolete seed-observer field is gone. No broader cache GC guarantee is claimed. | +| R2-10 | Unreachable Suspense snapshot fallback and extra commit effect. | **fixed-now**, mechanical: explicit single snapshot read, one shared commit effect, captured entry release and original window-effect placement preserved. | Full React lifecycle/SSR/StrictMode and receiving suites. | +| R2-T1 | No generated same-boundary siblings exist; render reservation proves old class remains open. | **refuted** as stated: reviewed `2c605f92b` already generated shared-boundary, staggered and same-pass siblings. New post-hook and peer-first schedules close real observation gaps. Reservation exposes finite extra work, not the old progress failure in these histories. | Keep the stronger generated schedules and finite coverage bounds. | +| S1 | Peer query-local request health leaks into a private seed. | **fixed-now**, discovered while challenging R2-5. Own observer health replaces peer health. Pre-edit RED, final GREEN and both hostile calibrations above enforce the law. | Primary lifecycle sibling grammar, health judgments and per-mode reach. | + +Ten numbered raw claims equal three fixed-now, one already-fixed, one refuted +and five accepted-design. Including the broader testing claim and discovered +sibling gives 12 items: four fixed-now, one already-fixed, two refuted and five +accepted-design. No item is deferred, confirmed-open or awaiting a design choice. +Accepted retry work does not become an unfixed violation of an exact request +budget: the approved contract explicitly leaves uncommitted retention free. + +Reviewer assessment: useful hypothesis generation, but no hire for independent +high-effort correctness review on this evidence. The review identified real +mechanisms and three useful cleanups. It did not execute its claims, missed +existing sibling histories and treated approved ownership/page policy as bugs. +The first-load issue was valid on its target commit. The health question led to +a real adjacent bug, but adding the proposed peer subscription would propagate +that peer's query-local errors more reliably rather than fix ownership. + +CodeRabbit review `5757342434` on `3cb5df960` separately requested clarification +of overlapping fulfilled-startup history. The historical paragraph now states +six distinct members explicitly. Its repeated scope warning remains authorized +scope, its default docstring threshold is not a repository requirement, and its +incomplete report review and paused automatic review remain external limits. +No GitHub reply or review-control command was sent. + +### Self-review and limits + +This is a self-review, not an independent challenge. ORC-001/002 derive the new +health judgment from the glossary and keep it independent of observer/cache +logic. ORC-003/009 explain source versus query-local health and post-hook holds +beside their grammar/checks. ORC-004 reconstructs every reported schedule and +keeps old families. Removing peer failure loses S1, source reports lose the +frozen-health rejection, and post-hook/peer-first axes lose their retry cuts. +Widths1–3, offsets1–4, one or two source Collections and two consumers bound the +domain. A rejected acquisition after abort has no authority and is excluded. +ORC-005/014 use actual React19/jsdom boundaries with controlled on-demand sources, +not external provider or native hydration evidence. ORC-006/007 use typed +assertion failures, matching fixed/fresh budgets and direct RED/GREEN replay. +ORC-008 preserves the existing model distinction between seed visibility and +owned readiness. ORC-010 retains failure fidelity and ScenarioLifetime cleanup. +ORC-011 has no named shared semantic fault needing another reference model: +source reports and query-local request failures provide independent rival +inputs. ORC-012 records the source digest and the complete loss audit here. +ORC-013/015 cross notification/recovery and source/query-local neighbors with +measured per-mode reach. ORC-016 keeps laws and histories in the primary owner. +No pinned example substitutes for a missing generator dimension. + +Limits remain finite React19/jsdom schedules, controlled request settlement and +public committed health/rows/Collection identity. No arbitrary scheduler, exact +GC timing, exact retry work budget, real provider transport or native hydration +claim follows. The broader PR still retains the original Svelte pre-edit gate +qualification. Full monorepo `pnpm test` was not run. CI and PR readiness remain +separate from this completed review accounting. + +Final package verification for the second review: React **4,855 tests in 40 +files** passes with no type errors (`pnpm --filter @tanstack/react-db test +--maxWorkers=2 --coverage.enabled=false`). The selected lifecycle/render/shared +conformance run passes639 tests, and direct health replay and both final typed +hostile mutants produce their recorded outcomes. No Svelte runtime changed. +The earlier Svelte542-test result remains historical evidence, and the original +Svelte pre-edit type-gate qualification remains unchanged. No full monorepo +`pnpm test` or new bundle-size measurement is claimed. + +Focused ESLint initially requested const for the captured commit callback. The +final source keeps that const after the snapshot read. The earlier effect closes +over it and executes only after render, preserving the original effect position. +Explicit typechecking and all270 lifecycle/render receiver tests pass after this +lint-only change. The full4,855-test package run precedes that const spelling +change. The final SSR rerun and lint results are recorded with publication. + +The final SSR effects and SSR error-handoff run passes63 tests in two files, +with no type errors. Focused ESLint and explicit React typechecking pass. +Prettier and `pnpm lint:changed` pass. The PR draft passes both the structural +English check (zero hard findings) and the GitHub paragraph-format check. + +Source snapshot for the second review: `26ab15ef15c4b4237977ec64c6f271f094a74aead8de855871af7f74fd0fa86f` across 30 files. The companion manifest preserves all preceding snapshots. + +## Four receiving-oracle enforcement repairs before merge + +Parent production head: `42514b45f3e8d63f3bb4000280d915ab4079d41c`. +The maintainer authorized closing the four independent audit findings before +merge. This amendment changes only oracle drivers, observations, reach and +coverage documentation. It changes no production code or reference-model rule. +The prior qualifications remain historical facts, including the original Svelte +receiver's missing typed pre-edit gate. These later results do not repair that +process order retroactively. + +### Law and authority + +The existing error-handling guide and observer hydration laws require a private +first-load error to end provisional rows and notify the real ErrorBoundary. +The shared conformance contract requires retry progress and independent +successful output Collections. Its page law requires each binding's rows and +keyed state to remain on its own page when another binding moves. Transported +seeds obey that same law. No new law or narrowed expectation is introduced. + +The production paths are real React 19/jsdom hooks and boundaries, plus Svelte +rune construction and native flush/settled cuts. Sources and request settlement +remain controlled. The independent initial-view and sort/filter/slice models +are unchanged; existing shared comparisons judge the new observations. + +### Lossless audit account and permanent repairs + +| Audit ID | Missing enforcement | Repair and observation cut | Disposition | +| --- | --- | --- | --- | +| S1 | A forced host reread supplied notification before the error comparison. | Compare the actual ErrorBoundary before any application rerender. Retain the later reread as a second comparison. Declare notification reach before/after peer movement. | fixed-now | +| SCHED-1 | Releasing one source did not prove a successful first hook return. | Count successful hook returns. Drain only that query, with at most four host retries, while the peer has no successful return and retains demand. Shared boundaries also have no layout commit. Declare successful-candidate reach. | fixed-now | +| O1 | The original binding acted before its page was observed after the borrower moved. | Before the original's next action, compare original rows, keyed state, retained identity and captured Collection rows. Compare both keyed maps after later moves too. Declare unchanged-original reach. | fixed-now | +| O2 | Transported existing peers had correct data checks but no keyed-state comparison. | Both React and Svelte compare the peer map with the existing independent window judgment at every peer cut. Declare held-seed and local-handoff reach per mode. | fixed-now | + +All four raw audit findings have durable repairs. No newly demonstrated +production violation remains. The audit's frozen-ready-seed concern already had +sensitive receiving coverage and remains a rejected concern, not a fifth gap. + +### Experiment order and calibration + +Before any new production change, explicit React/Svelte typechecks passed and +the expanded receivers passed unchanged production: 318 React tests and 6 Svelte +tests. Therefore no production repair was necessary. No new product-defect RED +is claimed. RED below calibrates the repaired enforcement against deliberate +wrong subjects. Each transformed source passed its explicit package type gate +before its selected campaign ran. Every counted RED is an intended assertion +failure, not a timeout, setup error or unrelated crash. + +| Calibration | Selected RED campaigns | Intended rejection | +| --- | ---: | --- | +| Drop only notifications for a seeded private initial-loading error | 12 sibling | `current Suspense attempt` at `private startup rejects after seed commit`: expected error, observed ready. The dropped branch executed 105 times. | +| Remove the selected-query drain/retry loop | 12 sibling | Positive successful-return premise while the peer remains pending. This is driver calibration, not a production mutant. | +| Record borrower rows as the unchanged original result after borrower movement | 48 replacement | `ordered window rows` at `original page after borrower move`. This is a wrong-observation control. | +| Keep React peer data/identity correct but replace its keyed map with moved-page rows | 48 transported | `keyed window rows` at `peer while local readiness is held`. | +| Apply the same wrong keyed-map observation to Svelte | 4 transported | The same shared law and held-peer checkpoint. | + +The first React peer-state calibration used a wrong test-name filter and +selected zero tests. Its zero exit supplies neither survival nor rejection +credit. The complete first-attempt script/results/output remain in task scratch. +The corrected 48-campaign run above supplies the intended RED. No experiment is +silently replaced by a later successful run. + +The notification fault is a narrow production mutant. The other calibrations +challenge a premise or recorder directly; they do not prove a general production +map-sharing mechanism. Prior audit controls showed these faults could pass the +old observations. The durable comparisons now reject them. + +### Measured reach at the final unchanged runtime + +Each sibling campaign runs 8 generated base histories, each expanded into 10 +legal family members, for 80 histories per mode. There are no separate examples. +The three new state counts are: + +| Client scope / keyed / campaign | Successful candidate with pending peer | Error notification before peer move | Error notification after peer move | +| --- | ---: | ---: | ---: | +| none/false/fixed | 9 | 10 | 8 | +| none/false/fresh | 8 | 8 | 8 | +| none/true/fixed | 9 | 10 | 8 | +| none/true/fresh | 10 | 8 | 8 | +| provider/false/fixed | 9 | 10 | 8 | +| provider/false/fresh | 8 | 10 | 8 | +| provider/true/fixed | 9 | 10 | 8 | +| provider/true/fresh | 10 | 8 | 8 | +| explicit/false/fixed | 9 | 10 | 8 | +| explicit/false/fresh | 9 | 9 | 8 | +| explicit/true/fixed | 9 | 10 | 8 | +| explicit/true/fresh | 10 | 8 | 8 | + +Every replacement mode reaches `original page retained after borrower move` +7 times:6 generated histories plus1 fixed replay example. This holds separately +for all 48 combinations of client/unscoped, keyed/derived, co-ready/borrower-first, +compiler-first/peer-first/post-hook and fixed/fresh. The total is336 histories, +including48 fixed examples. + +Every React transported mode reaches both `held peer keyed state` and +`local peer keyed state`8 times:6 generated histories plus2 fixed examples. +This holds separately for all 48 combinations of keyed/derived, ordinary/Suspense, +provider/explicit, offset/limit/both and fixed/fresh. Each state totals384 +histories, including96 fixed examples. + +Every Svelte transported mode has the same8 reaches per state:6 generated +histories plus2 fixed examples. This holds separately for derived/fixed, +derived/fresh, keyed/fixed and keyed/fresh. Each state totals32 histories, +including8 fixed examples. Svelte does not claim a client-switch path here. + +All new states have minimum 1 and fail the normal campaign when missed. +Fixed/fresh run budgets and examples are unchanged. The captured sibling +counts distinguish actual hook success from source-release ordering. + +### Direct replay and final GREEN + +Each row below ran through the guarded replay runner with exactly 1 selected +execution. The same captured seed/path fails with its stated fault and passes +after restoration, preserving the original law/checkpoint. The first runner +attempt could not create the `tsx` CLI IPC socket in the sandbox and ran no +property. Its setup log is preserved. The successful runner uses +`node --import tsx`, which avoids that CLI socket. + +| Calibration | Property | Seed | Shrink path | Guarded RED / GREEN | +| --- | --- | ---: | --- | --- | +| missing-private-error-notify | `react.suspense-siblings` | 1481104148 | `0:0:0:0:0:0:0` | assertion / pass,1 execution each | +| missing-staggered-premise | `react.suspense-siblings` | -271402343 | `0:0:0:0:0:0` | assertion / pass,1 execution each | +| wrong-original-page | `react.window-retry-ownership` | 2073608762 | `0:0:0` | assertion / pass,1 execution each | +| wrong-react-peer-state | `react.client-window` | 1577995267 | `0:0:0:0` | assertion / pass,1 execution each | +| wrong-svelte-peer-state | `svelte.client-window` | -902254508 | `0:0:0:0` | assertion / pass,1 execution each | + +The exact replay commands (from the repository root) are: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=react.suspense-siblings TANSTACK_DB_ORACLE_SEED=1481104148 TANSTACK_DB_ORACLE_PATH=0:0:0:0:0:0:0 pnpm --filter @tanstack/react-db exec node --import tsx ../db/tests/oracle-replay.ts tests/suspense-lifecycle-oracle.test.tsx -t 'co-mounted siblings/none/false/fresh' --maxWorkers=2 --coverage.enabled=false --disableConsoleIntercept +TANSTACK_DB_ORACLE_PROPERTY=react.suspense-siblings TANSTACK_DB_ORACLE_SEED=-271402343 TANSTACK_DB_ORACLE_PATH=0:0:0:0:0:0 pnpm --filter @tanstack/react-db exec node --import tsx ../db/tests/oracle-replay.ts tests/suspense-lifecycle-oracle.test.tsx -t 'co-mounted siblings/none/false/fresh' --maxWorkers=2 --coverage.enabled=false --disableConsoleIntercept +TANSTACK_DB_ORACLE_PROPERTY=react.window-retry-ownership TANSTACK_DB_ORACLE_SEED=2073608762 TANSTACK_DB_ORACLE_PATH=0:0:0 pnpm --filter @tanstack/react-db exec node --import tsx ../db/tests/oracle-replay.ts tests/window-render-oracle.test.tsx -t 'retry ownership/unscoped/derived/borrower-first/compiler-first/fresh' --maxWorkers=2 --coverage.enabled=false --disableConsoleIntercept +TANSTACK_DB_ORACLE_PROPERTY=react.client-window TANSTACK_DB_ORACLE_SEED=1577995267 TANSTACK_DB_ORACLE_PATH=0:0:0:0 pnpm --filter @tanstack/react-db exec node --import tsx ../db/tests/oracle-replay.ts tests/client-window-oracle.test.tsx -t derived/ordinary/provider/offset/fresh --maxWorkers=2 --coverage.enabled=false --disableConsoleIntercept +TANSTACK_DB_ORACLE_PROPERTY=svelte.client-window TANSTACK_DB_ORACLE_SEED=-902254508 TANSTACK_DB_ORACLE_PATH=0:0:0:0 pnpm --filter @tanstack/svelte-db exec node --import tsx ../db/tests/oracle-replay.ts tests/client-window-oracle.svelte.test.ts -t derived/fresh --maxWorkers=2 --coverage.enabled=false --disableConsoleIntercept +``` + +The restored source matches its pre-calibration bytes. Full React passes +**4,855 tests in 40 files**; full Svelte passes **542 tests in 16 files**. Both +report no type errors. Commands: + +```sh +pnpm exec tsc --noEmit -p packages/react-db/tsconfig.json +pnpm exec tsc --noEmit -p packages/svelte-db/tsconfig.json +pnpm --filter @tanstack/react-db exec vitest run --maxWorkers=2 --coverage.enabled=false --disableConsoleIntercept +pnpm --filter @tanstack/svelte-db exec vitest run --maxWorkers=2 --coverage.enabled=false --disableConsoleIntercept +``` + +The final size comparison uses the original exact base `1443dc92e5417394acbdc98b782caa9802848ce6`. +The typical tree-shaken app changes from 96,397 to 96,341 gzip bytes (**−56**), +and decreases **100 Brotli bytes**. The all-exports React package grows 258 +gzip/207 Brotli bytes. The maintainer accepted optional Suspense package growth. +This amendment adds no runtime bytes. Command: + +```sh +pnpm size:compare --base 1443dc92e5417394acbdc98b782caa9802848ce6 --exact-base --packages db,react-db,preact-db +``` + +### Independent review and bounded closure + +The independent oracle reviewer inspected the permanent diff, every typed +calibration, all 10 guarded replay outcomes and the per-mode reach results. +The final verdict finds all four named gaps closed at the declared finite +histories, production paths and observation cuts, with no supported blocker. +This review supplies independent evidence review, not a new external GitHub +review of the whole semantic head. + +ORC-001/002 preserve accepted authority and independent models. ORC-003/009 +keep explanations beside actual host cuts and use existing glossary terms. +ORC-004 reconstruction retains the reported error, staggered, borrower and +transported cases. Ablation controls reject missing notification, missing +successful-return work and missing peer comparisons. Finite widths, offsets, +empty pages and existing invalid-history exclusions stay unchanged. +ORC-005/014 use actual React/Svelte hosts with controlled source work and make +no real-provider or native-hydration claim. ORC-006/007 have typed intended +REDs, unchanged fixed/fresh budgets and direct replay. ORC-008 is not triggered: +no reference-model state changes. ORC-010 keeps the existing failure-fidelity +and lifetime mechanisms; captured reductions retain their law/checkpoint. +ORC-011 has no named shared model/production semantic fault requiring a second +formulation. ORC-012 binds this review to the companion source digest. +ORC-013/015 distinguish notification from host reread, successful return from +source release, and unchanged peer from moved peer, with per-mode reach. +ORC-016 adds no ordinary unit expectations to oracle files. + +Limits remain finite React 19/jsdom and Svelte host histories, controlled +requests and the named public error/row/map/Collection cuts. No arbitrary +scheduling, React 18, native hydration, actual Electric/Start transport, exact +retry-work budget, GC bound or heavy-query benchmark result is claimed. The +original Svelte typed pre-edit qualification remains. Full monorepo `pnpm test` +was not run. Current-head CI and discovered-review evaluation remain separate +merge conditions. No accepted in-scope counterexample is known after these +repairs. + +Final replay infrastructure verification passes **130 tests in three files** with no type errors (`oracle-replay.test.ts`, `replay-adapter-ownership.test.ts`, `replay-publication-storage.test.ts`). Prettier, `pnpm lint:changed` and `git diff --check` pass. No oracle run budget decreased. + +Source snapshot for the four enforcement repairs: `2324d2e46262f0e5a63498bc53824e44aa33db945904159727bd8905792cc5ad` across 30 files. The companion manifest preserves previous snapshots and excludes itself and this report. diff --git a/docs/framework/react/overview.md b/docs/framework/react/overview.md index 83737dd157..f57c8f1564 100644 --- a/docs/framework/react/overview.md +++ b/docs/framework/react/overview.md @@ -58,7 +58,7 @@ function TodoList() { ### Query Identity -React live query hooks derive the live query identity from structured query IR by default. The hook runs the query builder, normalizes the resulting IR, and uses that as the identity. When the derived identity changes, the old live query collection is cleaned up and a new one is created. +React live query hooks derive the live query identity from structured query IR by default. The hook runs the query builder, normalizes the resulting IR, and uses that as the identity. A changed query plan creates a new live-query Collection. Changing only a finite outer limit or offset moves the existing Collection's window. That means normal structured queries do not need a separate `queryKey`. Collection descriptors provide stable collection IDs, and captured values inside structured expressions become part of the derived identity: @@ -82,8 +82,11 @@ unmount it, clean up its source and client scope, then mount the replacement. Separate hooks and independent client scopes can use the same ID. A mounted binding keeps its Collection options until query identity changes. -Independent Suspense bindings share a Collection only when their supplied -Collection options agree. Object and function options use reference equality. +Independent query inputs to `useLiveQuery` and `useLiveSuspenseQuery` receive +their own live-query Collections, even with equal query keys and options. Their +source Collections can be shared. Passing the same pre-created Collection to +both hooks explicitly shares that Collection. Changing only the outer finite +limit or offset retains each hook's Collection, including under `DbProvider`. Cached `DbClient` results additionally reject conflicting `getKey` or `schema` references under the same query identity; use distinct query keys for those callers. @@ -120,6 +123,12 @@ An explicit `queryKey` is the caller's identity contract. If its source object changes, change the key too, even when the source ID stays the same. Use a primitive scope token in the key; Collection objects cannot be hashed there. +Treat `queryKey` and its nested values as immutable. Fresh objects with equal +contents keep the authored query, including when record properties appear in a +different order. To change a nested value, replace that object and its parent +objects, as with React state. In-place mutation has no supported invalidation +behavior. This applies to both `useLiveQuery` and `useLiveSuspenseQuery`. + An unhashable query without `queryKey` throws `UnhashableQueryIRError`, so represent the values that opaque logic captures in the key. An empty `queryKey` counts as none, as it does for a `DbClient` preload. A dependency @@ -129,11 +138,11 @@ as a performance escape hatch. #### What Happens When Identity Changes -When the derived identity or explicit query key changes: -1. The previous live query collection is cleaned up -2. A new query is created with the updated values -3. The component re-renders with the new data -4. The hook suspends (for `useLiveSuspenseQuery`) or shows loading state +When an identity change requires a new query plan, the hook replaces its +live-query Collection and renders the new result. While its initial result +loads, `useLiveSuspenseQuery` shows the Suspense fallback and `useLiveQuery` +reports loading. A finite outer limit or offset change retains the Collection +and moves its window after commit. #### Best Practices @@ -224,4 +233,7 @@ function App() { } ``` -When the derived identity or explicit `queryKey` changes, `useLiveSuspenseQuery` will re-suspend, showing your Suspense fallback until the new data is ready. +When a changed query plan creates a new live-query Collection, +`useLiveSuspenseQuery` shows your Suspense fallback until its initial result +is ready. Finite outer-window moves retain the Collection and follow its +window publication policy. diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index ae63485d47..e8e48a9ba0 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -277,6 +277,12 @@ function UserSearch({ search }: { search: string }) { You can also provide a `queryKey` as a performance escape hatch for a very hot render path, but normal structured queries should omit it. +For bindings that compare query identity, treat the key and its nested values +as immutable. Fresh plain objects and arrays with equal contents keep the +authored query; record property order does not matter. Replace changed nested +objects and their parents to invalidate the query on the next framework update. +Mutating a key in place has no supported invalidation behavior. + Query identity matters where a hook compares it to decide whether to compile again: React and Preact compare it on every render, and a `DbClient` matches preloaded and hydrated queries by it. Those hooks throw diff --git a/packages/db/src/live-query-observer.ts b/packages/db/src/live-query-observer.ts index b778c62cf3..480288fb88 100644 --- a/packages/db/src/live-query-observer.ts +++ b/packages/db/src/live-query-observer.ts @@ -632,7 +632,16 @@ class LiveQueryObserverImpl< // still being created. Register the release hook up front; if detach() // ran during that replay (collectionUnsub no longer points at our hook), // undo the subscription as soon as the call returns. - if (this.clientBinding) ownRelease(this.clientBinding.subscribe()) + if (this.clientBinding) { + ownRelease(this.clientBinding.subscribe()) + // A retained ready Collection can settle subset work without another + // row or status change. That settlement must release its hydration seed. + ownRelease( + collection.on(`loadingSubset:change`, () => { + if (this.clientBinding?.seed) this.clientBinding.scheduleHandoff() + }), + ) + } const subscription = collection.subscribeChanges( (changes) => notify(changes as Array>), { includeInitialState: !this.wholesale && !attachedWithHydrationSeed }, diff --git a/packages/db/src/live-query-options.ts b/packages/db/src/live-query-options.ts index 20e6092651..1ab2bb10b9 100644 --- a/packages/db/src/live-query-options.ts +++ b/packages/db/src/live-query-options.ts @@ -310,8 +310,8 @@ export function getLiveQueryHash( return getStableValueHash(identity, `queryKey`) } -// Only adapter-owned Collections enter this table. Cache- and Suspense-owned -// Collections retain their result identity and are never movable by a peer. +// Each entry belongs to one framework binding or an unclaimed retry resource. +// DbClient snapshots remain separate; peers never share a mutable window owner. const resolvedQueryValues = new WeakMap< object, { collection: object; config: NonNullable> } @@ -430,7 +430,7 @@ export function resolveLiveQueryValue( // A pool here has passed `configPool`. pool?: PooledLiveQueries | false startSync?: boolean - /** @internal Previous adapter-owned result; absent for shared results. */ + /** @internal undefined skips window tracking; null starts an owned result. */ previousCollection?: Collection | null /** @internal Identity of one framework binding; never shared by peers. */ windowOwner?: object diff --git a/packages/db/tests/conformance/client-window-history-oracle.ts b/packages/db/tests/conformance/client-window-history-oracle.ts new file mode 100644 index 0000000000..c946336ea0 --- /dev/null +++ b/packages/db/tests/conformance/client-window-history-oracle.ts @@ -0,0 +1,42 @@ +/** Transported page histories receive the window owner's sort/slice law. + * Each history moves while local readiness is held, mounts fresh old-page + * readers, then releases local rows and returns. Bounds include empty pages. + * Host receivers supply the public hook and native flush; this module only + * owns input grammar. It imports no adapter or production identity machinery. + */ +import fc from 'fast-check' +import { declareOracle } from '../oracle-declaration' +// Width/offset vary independently, including pages beyond the final row. +// Every history moves twice and returns to the first page. Removing the held +// readiness cut loses the distinction between a matching seed and local rows. +export const clientWindowArbitrary = fc.record({ + count: fc.integer({ min: 1, max: 12 }), + offset: fc.integer({ min: 0, max: 4 }), + width: fc.integer({ min: 1, max: 4 }), + jump: fc.integer({ min: 1, max: 4 }), + latePeers: fc.integer({ min: 1, max: 2 }), +}) +export type ClientWindowHistory = { + count: number + offset: number + width: number + jump: number + latePeers: number +} +export const clientWindowExamples: Array<[ClientWindowHistory]> = [ + [{ count: 10, offset: 0, width: 2, jump: 2, latePeers: 1 }], + [{ count: 2, offset: 0, width: 1, jump: 1, latePeers: 1 }], +] +export const clientWindowStates = [ + `first seed`, + `moved seed`, + `local handoff`, + `peer isolation`, + `held peer keyed state`, + `local peer keyed state`, + `returned page`, + `client scope switch`, + `fresh old-page lookup`, +] as const + +declareOracle({ grammar: [clientWindowArbitrary] }) diff --git a/packages/db/tests/conformance/contract.ts b/packages/db/tests/conformance/contract.ts index 766c8c21e5..aa495fee01 100644 --- a/packages/db/tests/conformance/contract.ts +++ b/packages/db/tests/conformance/contract.ts @@ -31,8 +31,21 @@ * projections, predicates, order, nested windows and Collection options still * require compilation. An unchanged * explicit key keeps its authored input, and changed opaque keyed inputs compile. - * DbClient and Suspense result sharing retain their identity-based behavior; - * they are outside this adapter-owned optimization. + * An initially disabled input has no authored plan to retain: enabling it admits + * its first plan even with the same key. + * Explicit keys are immutable values (maintainer decision, 2026-11-03): fresh + * equal plain records and arrays retain the authored input, regardless of + * record property order. Replacing a nested value with a different value + * invalidates it. Replace changed objects along their path; mutating a key + * in place is unsupported and has no promised invalidation behavior. + * The same law applies to every client-backed adapter (issue #2345) and to + * Suspense hooks (maintainer decision, 2026-11-03). Independent query inputs + * own distinct live-query Collections, even under equal keys and options. + * Each moved client input uses the new page's preload/hydration identity; + * transported page snapshots keep their original identities. An explicitly + * supplied Collection remains caller-owned and may be shared. Before an initial + * Suspense render succeeds, only its fallback is public; retry progress is owed, + * but retention of an uncommitted Collection is not. * * A window move requires a committed host update and runs after any current * Collection publication. A speculative React or Preact render cannot move the @@ -237,6 +250,8 @@ export interface LiveQueryHandle { * still tell a loading error (`isLoadingError`) from a crash (`isCrashed`). */ statusSurface?: () => ConformanceStatusSurface + /** Public fallback visibility for a hook that suspends before initial readiness. */ + isSuspended?: () => boolean /** The live-query Collection currently backing this result. */ collection: () => Collection | null /** Let the framework scheduler + core sync settle, then resolve. */ @@ -387,9 +402,8 @@ export interface LiveQueryDriver { * Whether the binding compares query identity to decide when to compile * again (see the opening query-identity law). The driver declares the * adapter's documented policy for the inputs it mounts; the suite does not - * infer it from observed output. Svelte compares identity only for a query - * that a `DbClient` backs, and its shared drivers mount none, so they - * declare `false`. + * infer it from observed output. Svelte compares identity for client-backed queries; the driver declares + * this per input mode rather than per framework. */ comparesQueryIdentity: boolean /** No active waivers. Kept empty in drivers to make this boundary explicit. */ @@ -404,6 +418,8 @@ export interface LiveQueryDriver { features?: { serverSnapshot?: boolean suspense?: boolean + /** A successful return is the initial-readiness observation; pending reads expose a fallback. */ + suspendsUntilReady?: boolean /** * Public sharing policy: queries whose config passes `pooledLiveQueries` * and that filter only by `eq` on one source share one source subscription diff --git a/packages/db/tests/conformance/indexing-config-oracle.ts b/packages/db/tests/conformance/indexing-config-oracle.ts index 31efa612fd..90482331a9 100644 --- a/packages/db/tests/conformance/indexing-config-oracle.ts +++ b/packages/db/tests/conformance/indexing-config-oracle.ts @@ -114,38 +114,15 @@ export function expectIndexingConfiguration( expect(collection.config.defaultIndexType).toBe(expected.constructor) } -// Existing Suspense sharing remains available to separate bindings with the -// same query identity and compatible effective indexing declarations. An -// omitted mode means off; constructor identity is reference equality. -export function expectedIndexingReuse( - first: IndexSettings, - second: IndexSettings, - identity: ReturnType[number][`identity`], -): boolean { - const left = expectedIndexing(first) - const right = expectedIndexing(second) - return ( - identity !== `different-key` && - !left.rejects && - !right.rejects && - left.mode === right.mode && - left.constructor === right.constructor - ) -} - -export function expectIndexingReuse( +// Independent Suspense bindings own distinct outputs (maintainer decision, +// 2026-11-03). Effective indexing equality never authorizes mutable sharing. +export function expectIndependentIndexingOutputs( firstOutput: object | undefined, secondOutput: object | undefined, first: IndexSettings, second: IndexSettings, - identity: ReturnType[number][`identity`], ): void { - if (expectedIndexingReuse(first, second, identity)) - expect(secondOutput).toBe(firstOutput) - else if ( - !expectedIndexing(first).rejects && - !expectedIndexing(second).rejects - ) + if (!expectedIndexing(first).rejects && !expectedIndexing(second).rejects) expect(secondOutput).not.toBe(firstOutput) } @@ -167,12 +144,12 @@ declareOracle({ indexConfigurationPairs, indexOptionAbsencePairs, ], - model: [expectedIndexing, expectedIndexingReuse], + model: [expectedIndexing], check: [ expectIndexing, expectIndexingAdmission, expectIndexingConfiguration, - expectIndexingReuse, + expectIndependentIndexingOutputs, ], }) diff --git a/packages/db/tests/conformance/indexing-reuse-oracle.test.ts b/packages/db/tests/conformance/indexing-reuse-oracle.test.ts index e99f882c97..55abe7e867 100644 --- a/packages/db/tests/conformance/indexing-reuse-oracle.test.ts +++ b/packages/db/tests/conformance/indexing-reuse-oracle.test.ts @@ -1,7 +1,7 @@ /** * Calibrate the shared initial-binding identity comparison over its existing * finite grammar. Each pair of admitted declarations supplies the opposite of - * the promised identity relation: an overshared or undershared output. Rejected + * the promised identity relation: a shared output despite independent bindings. Rejected * declarations supply no output and impose no output-identity comparison. * This tests checker sensitivity, not another production receiving path. */ @@ -9,9 +9,8 @@ import { expect, test } from 'vitest' import { BTreeIndex, BasicIndex } from '../../src' import { declareOracle } from '../oracle-declaration' import { - expectIndexingReuse, + expectIndependentIndexingOutputs, expectedIndexing, - expectedIndexingReuse, indexConfigurationLabel, indexConfigurationPairs, indexConfigurations, @@ -24,25 +23,23 @@ const histories = indexConfigurationPairs( ) declareOracle({ grammar: [indexConfigurationPairs], - model: [expectedIndexing, expectedIndexingReuse], - check: [expectIndexingReuse], + model: [expectedIndexing], + check: [expectIndependentIndexingOutputs], }) test.each(histories)( `identity comparison rejects the opposite relation: %j`, - ({ first, second, identity }) => { + ({ first, second }) => { const left = expectedIndexing(first).rejects ? undefined : {} const right = expectedIndexing(second).rejects ? undefined : {} if (!left || !right) { expect(() => - expectIndexingReuse(left, right, first, second, identity), + expectIndependentIndexingOutputs(left, right, first, second), ).not.toThrow() } else { - const wrong = expectedIndexingReuse(first, second, identity) - ? right - : left + const wrong = left expect(() => - expectIndexingReuse(left, wrong, first, second, identity), + expectIndependentIndexingOutputs(left, wrong, first, second), ).toThrow() } }, diff --git a/packages/db/tests/conformance/window-history-oracle.ts b/packages/db/tests/conformance/window-history-oracle.ts index 1ccd022d88..c613774083 100644 --- a/packages/db/tests/conformance/window-history-oracle.ts +++ b/packages/db/tests/conformance/window-history-oracle.ts @@ -4,7 +4,11 @@ * 2026-10-30, issue #2291). This extends the recompilation law in contract.ts. * Query identity still includes the page for cache/hydration lookup. * - * This owner checks adapter-owned ordered list queries without a DbClient. + * This owner checks adapter-owned ordered list queries across framework drivers, + * including DbClient and Suspense modes (maintainer decision, 2026-11-03). + * The React and Svelte client receivers check JSON page seeds before local readiness, + * handoff to local rows and independent peers. React also checks client switches. + * Independent Suspense bindings own their Collections; neither may move a peer. * An explicit key still owns invalidation: an unchanged key keeps its query. * A changed key can reuse a window only when both plans have stable structured * identities and differ solely in the outer window. Opaque callbacks retain @@ -30,6 +34,7 @@ import { } from '../oracle-config' import { expectOrderedRows, expectUnorderedRows } from './result-laws' import { ScenarioLifetime } from './scenario-lifetime' +import { expectDisabledResult } from './disabled-laws' import type { ControllableHandle, LiveQueryDriver, @@ -106,15 +111,17 @@ export function checkWindowObservation( if (expected.previousCollection !== undefined) { if (expected.retain) atOracleCheckpoint(`outer window retains Collection`, checkpoint, () => - expect(actual.collection, `outer window retains Collection`).toBe( - expected.previousCollection, - ), + expect( + actual.collection === expected.previousCollection, + `outer window retains Collection`, + ).toBe(true), ) else atOracleCheckpoint(`changed plan replaces Collection`, checkpoint, () => - expect(actual.collection, `changed plan replaces Collection`).not.toBe( - expected.previousCollection, - ), + expect( + actual.collection === expected.previousCollection, + `changed plan replaces Collection`, + ).toBe(false), ) } } @@ -615,6 +622,14 @@ export function registerWindowHistoryOracle(driver: LiveQueryDriver) { ) }, ) + registerPeerWindowOracle(driver) + if (driver.mountKeyedControllable) registerImmutableKeyOracle(driver) + // Disabled input is legal only for ordinary bindings that accept keys. + for (const receiver of driver.mountKeyedControllable && + !driver.features?.suspendsUntilReady + ? [driver] + : []) + registerKeyedAdmissionOracle(receiver) registerPendingWindowOracle(driver) registerPublicationWindowOracle(driver) for (const nested of [false, true]) { @@ -789,11 +804,18 @@ async function checkPendingWindowHistory( false, ) await handle.flush() - const collection = handle.collection() + let collection = handle.collection() const failures: Array = [] + // Suspense publishes a fallback before its first successful return. It has + // no public Collection then; after readiness the same retention law applies. + const suspends = driver.features?.suspendsUntilReady === true const readiness = (ready: boolean, checkpoint: string) => atOracleCheckpoint(`initial-query readiness`, checkpoint, () => - expect(handle.current().isReady).toBe(ready), + suspends + ? expect(handle.isSuspended!(), `public Suspense fallback`).toBe( + !ready, + ) + : expect(handle.current().isReady).toBe(ready), ) const acquisitionPending = (pending: boolean, checkpoint: string) => atOracleCheckpoint(`acquisition pending premise`, checkpoint, () => @@ -804,6 +826,22 @@ async function checkPendingWindowHistory( // last settled offset and recompute that page independently. const compare = (offset: number | undefined, checkpoint: string) => { try { + if (suspends && offset === undefined) { + expect(handle.isSuspended!(), `initial fallback is visible`).toBe( + true, + ) + expect( + handle.collection(), + `no successful hook result yet`, + ).toBeNull() + return + } + // The previous complete page remains public during a later move. A + // stale renderHook result behind a fallback is not that observation. + if (suspends) + expect(handle.isSuspended!(), `settled page stays visible`).toBe( + false, + ) const expectedRows = offset === undefined ? [] : expected(offset) checkWindowObservation( { rows: handle.current().data, collection: handle.collection() }, @@ -845,6 +883,7 @@ async function checkPendingWindowHistory( acquisitionPending(false, `initial ready`) readiness(true, `initial ready`) reach.reach(`initial ready`) + if (suspends) collection = handle.collection() compare(history.jump, `initial ready`) const firstMove = history.jump + history.width + 1 @@ -1183,3 +1222,571 @@ declareOracle({ grammar: [publicationWindowArbitrary], check: [checkPublicationWindowHistory], }) + +/** Independent bindings may read the same source, but their mutable pages are + * local. The shared sort/slice model judges both outputs after declarative and + * imperative moves, source writes, and retirement of the other binding. A + * supplied Collection is not generated: sharing one is explicit caller intent. + */ +async function checkPeerWindowHistory( + driver: LiveQueryDriver, + history: { width: number; jump: number }, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + let first: ControllableHandle | undefined + let peer: ControllableHandle | undefined + lifetime.defer(() => first?.unmount()) + lifetime.defer(() => peer?.unmount()) + await lifetime.run(async () => { + const rows = Array.from({ length: 12 }, (_, rank) => ({ + id: String(rank), + rank, + })) + const source = driver.makeSource(rows) + lifetime.defer(() => source.collection.cleanup({ dependents: true })) + const build = (q: Parameters[0], offset: number) => + q + .from({ row: source.collection }) + .orderBy(({ row }: { row: WindowRow }) => row.rank) + .limit(history.width) + .offset(offset) + .select(({ row }: { row: WindowRow }) => ({ ...row })) + const mount = () => + driver.mountKeyedControllable + ? driver.mountKeyedControllable( + build, + (offset) => [`peer-window`, offset], + 0, + ) + : driver.mountControllable(build, 0) + first = mount() + peer = mount() + await first.flush() + await peer.flush() + const firstCollection = first.collection() + const peerCollection = peer.collection() + const compare = ( + handle: ControllableHandle, + offset: number, + checkpoint: string, + previousCollection: unknown, + ) => { + const expected = windowRows( + rows, + { + offset, + limit: history.width, + minimum: -1, + descending: false, + nestedLimit: rows.length, + }, + false, + ) + checkWindowObservation( + { rows: handle.current().data, collection: handle.collection() }, + { rows: expected, previousCollection, retain: true }, + checkpoint, + ) + checkWindowState(handle.current().state, expected, checkpoint) + } + checkWindowObservation( + { rows: peer.current().data, collection: peerCollection }, + { + rows: windowRows( + rows, + { + offset: 0, + limit: history.width, + minimum: -1, + descending: false, + nestedLimit: rows.length, + }, + false, + ), + previousCollection: firstCollection, + retain: false, + }, + `independent equal-query outputs`, + ) + reach.reach(`independent outputs`) + await first.setParam(history.jump) + compare(first, history.jump, `first hook page`, firstCollection) + compare(peer, 0, `peer after declarative move`, peerCollection) + reach.reach(`declarative isolation`) + // A public imperative move must obey the same ownership law. It does not + // change the authored input, so the first hook later returns declaratively. + await first.apply(() => { + void firstCollection!.utils.setWindow({ + offset: history.jump + 1, + limit: history.width, + }) + }) + compare(first, history.jump + 1, `imperative page`, firstCollection) + compare(peer, 0, `peer after imperative move`, peerCollection) + reach.reach(`imperative isolation`) + const changed = { ...rows[0]!, rank: 20 } + rows[0] = changed + await peer.apply(() => source.update({ ...changed })) + compare(peer, 0, `peer source update`, peerCollection) + compare(first, history.jump + 1, `first source update`, firstCollection) + reach.reach(`source write isolation`) + first.unmount() + first = undefined + await peer.setParam(history.jump) + compare(peer, history.jump, `peer after other unmount`, peerCollection) + reach.reach(`peer survives retirement`) + }) +} +function registerPeerWindowOracle(driver: LiveQueryDriver) { + for (const campaign of [`fixed`, `fresh`] as const) { + const property = windowHistoryProperty(driver, `peer-window-history`) + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`peer page histories (${campaign})`, async () => { + const reach = createOracleReach( + `${driver.name}/peer/${campaign}`, + [ + `independent outputs`, + `declarative isolation`, + `imperative isolation`, + `source write isolation`, + `peer survives retirement`, + ], + { fixedHistories: pendingExamples.length }, + ) + let index = 0 + const fidelity = createOracleFailureFidelity<{ + width: number + jump: number + }>() + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(pendingWindowArbitrary, async (history) => { + reach.beginHistory({ fixed: index++ < pendingExamples.length }) + await fidelity.run(history, () => + checkPeerWindowHistory(driver, history, reach), + ) + }), + campaign === `fixed` + ? { + numRuns: oracleRuns(6) + pendingExamples.length, + seed: 2345, + examples: pendingExamples, + } + : oraclePropertyOptions(6, property, { + examples: pendingExamples, + }), + ), + ) + } finally { + console.info( + `peer reach ${driver.name}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + reach.assertReached() + }) + } +} +declareOracle({ + grammar: [pendingWindowArbitrary], + check: [checkPeerWindowHistory], +}) + +// A disabled input admits no authored plan. The enablement contract therefore +// precedes stable-key retention: first admit the enabled plan, then keep it +// until its key changes. These histories cross the two formerly separate lanes. +const keyedAdmissionArbitrary = fc.record({ + count: fc.integer({ min: 2, max: 10 }), + offset: fc.integer({ min: 0, max: 1 }), +}) +async function checkKeyedAdmission( + driver: LiveQueryDriver, + history: { count: number; offset: number }, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + let mounted: + | ControllableHandle<{ + enabled: boolean + minimum: number + key: number + }> + | undefined + lifetime.defer(() => mounted?.unmount()) + await lifetime.run(async () => { + const rows = Array.from({ length: history.count }, (_, rank) => ({ + id: String(rank), + rank, + })) + const source = driver.makeSource(rows) + lifetime.defer(() => source.collection.cleanup({ dependents: true })) + const page = { + offset: history.offset, + limit: history.count, + minimum: -1, + descending: false, + nestedLimit: history.count, + } + const first = { enabled: false, minimum: -1, key: 0 } + mounted = driver.mountKeyedControllable!( + (q, input: typeof first) => + input.enabled + ? q + .from({ row: source.collection }) + .where(({ row }: { row: WindowRow }) => + driver.ops.gt(row.rank, input.minimum), + ) + .orderBy(({ row }: { row: WindowRow }) => row.rank) + .limit(page.limit) + .offset(page.offset) + .select(({ row }: { row: WindowRow }) => ({ + id: row.id, + rank: row.rank, + })) + : null, + (input) => [`admission`, input.key], + first, + ) + await mounted.flush() + expectDisabledResult(mounted.current(), driver.disabledRepresentation) + expect( + mounted.collection(), + `disabled input has no authored Collection`, + ).toBeNull() + reach.reach(`initial disabled`) + await mounted.setParam({ ...first, enabled: true }) + checkWindowObservation( + { rows: mounted.current().data, collection: mounted.collection() }, + { rows: windowRows(rows, page, false), retain: true }, + `stable key first admits enabled plan`, + ) + reach.reach(`enabled under same key`) + const collection = mounted.collection() + await mounted.setParam({ enabled: true, minimum: history.count, key: 0 }) + checkWindowObservation( + { rows: mounted.current().data, collection: mounted.collection() }, + { + rows: windowRows(rows, page, false), + previousCollection: collection, + retain: true, + }, + `stable key retains admitted plan`, + ) + reach.reach(`stable admitted plan`) + await mounted.setParam({ enabled: true, minimum: history.count, key: 1 }) + checkWindowObservation( + { rows: mounted.current().data, collection: mounted.collection() }, + { + rows: windowRows(rows, { ...page, minimum: history.count }, false), + previousCollection: collection, + retain: false, + }, + `changed key admits changed plan`, + ) + reach.reach(`changed key`) + }) +} +function registerKeyedAdmissionOracle(driver: LiveQueryDriver) { + // Disabled inputs are not a Suspense overload. Its pending fallback law has + // separate receiving histories; ordinary identity-comparing bindings run here. + for (const campaign of [`fixed`, `fresh`] as const) { + const property = windowHistoryProperty(driver, `keyed-admission`) + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`initial disabled keyed admission (${campaign})`, async () => { + const reach = createOracleReach(`${property}/${campaign}`, [ + `initial disabled`, + `enabled under same key`, + `stable admitted plan`, + `changed key`, + ]) + const fidelity = createOracleFailureFidelity<{ + count: number + offset: number + }>() + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(keyedAdmissionArbitrary, async (history) => { + reach.beginHistory() + await fidelity.run(history, () => + checkKeyedAdmission(driver, history, reach), + ) + }), + campaign === `fixed` + ? { numRuns: oracleRuns(6), seed: 2345 } + : oraclePropertyOptions(6, property), + ), + ) + reach.assertReached() + } finally { + console.info( + `keyed admission reach ${driver.name}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }) + } +} +declareOracle({ + grammar: [keyedAdmissionArbitrary], + check: [checkKeyedAdmission], +}) + +// Immutable explicit keys obey contract.ts (maintainer decision, 2026-11-03). +// Equal values keep the authored query even when a new callback proposes a +// different predicate. Changed nested values admit that predicate. Mutation is +// outside the legal grammar; frozen key trees catch accidental fixture writes. +// This lane covers plain records/arrays and JSON scalar leaves, not rich keys, +// disabled admission, pending work, transport seeds or speculative host cuts. +const immutableKeyArbitrary = fc.record({ + count: fc.integer({ min: 3, max: 10 }), + label: fc.string({ maxLength: 8 }), + values: fc.array( + fc.oneof(fc.integer(), fc.string(), fc.boolean(), fc.constant(null)), + { + maxLength: 4, + }, + ), + tail: fc.array( + fc.record({ + object: fc.integer({ min: 0, max: 2 }), + array: fc.integer({ min: 0, max: 2 }), + minimum: fc.integer({ min: -1, max: 10 }), + reordered: fc.boolean(), + }), + { maxLength: 5 }, + ), +}) +type ImmutableKeyHistory = + typeof immutableKeyArbitrary extends fc.Arbitrary ? T : never + +function immutableKey( + history: ImmutableKeyHistory, + object: number, + array: number, + reordered = false, +): ReadonlyArray { + const filter = Object.freeze({ minimum: object }) + const clauses = Object.freeze([Object.freeze({ minimum: array })]) + const values = Object.freeze([...history.values]) + return Object.freeze([ + `immutable-key`, + Object.freeze( + reordered + ? { values, clauses, filter, label: history.label } + : { label: history.label, filter, clauses, values }, + ), + ]) +} + +// The two integers are model-only key equivalence classes. One names a value +// under a record, the other under an array. Equality needs no production deep +// comparator or hash. The authored minimum is separate: equal keys may propose +// different queries, so the next render distinguishes it from the candidate. +async function checkImmutableKeyHistory( + driver: LiveQueryDriver, + history: ImmutableKeyHistory, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + let mounted: + | ControllableHandle<{ + key: ReadonlyArray + minimum: number + }> + | undefined + lifetime.defer(() => mounted?.unmount()) + await lifetime.run(async () => { + const rows = Array.from({ length: history.count }, (_, rank) => ({ + id: String(rank), + rank, + })) + const source = driver.makeSource(rows) + lifetime.defer(() => source.collection.cleanup({ dependents: true })) + const page = { + offset: 0, + limit: history.count, + minimum: -1, + descending: false, + nestedLimit: history.count, + } + const firstKey = immutableKey(history, 0, 0) + mounted = driver.mountKeyedControllable!( + (q, input: { key: ReadonlyArray; minimum: number }) => + q + .from({ row: source.collection }) + .where(({ row }: { row: WindowRow }) => + driver.ops.gt(row.rank, input.minimum), + ) + .orderBy(({ row }: { row: WindowRow }) => row.rank) + .limit(page.limit) + .select(({ row }: { row: WindowRow }) => ({ + id: row.id, + rank: row.rank, + })), + (input) => input.key, + { key: firstKey, minimum: -1 }, + ) + await mounted.flush() + let previous = { object: 0, array: 0 } + let minimum = -1 + const compare = ( + checkpoint: string, + previousCollection?: unknown, + retain = true, + ) => { + const expected = windowRows(rows, { ...page, minimum }, false) + checkWindowObservation( + { rows: mounted!.current().data, collection: mounted!.collection() }, + { + rows: expected, + previousCollection, + retain, + }, + checkpoint, + ) + checkWindowState(mounted!.current().state, expected, checkpoint) + } + compare(`initial immutable key`) + // Each history reconstructs equal reference/deep-value proposals, then + // changes each nested path independently and restores the original key. + // Tail actions vary repetitions, returns, predicates and property order. + const steps = [ + { + object: 0, + array: 0, + minimum: history.count, + key: firstKey, + cut: `same key object`, + }, + { + object: 0, + array: 0, + minimum: history.count, + key: Object.freeze([...firstKey]), + cut: `fresh outer array`, + }, + { + object: 0, + array: 0, + minimum: history.count, + key: immutableKey(history, 0, 0), + cut: `fresh equal nested values`, + }, + { + object: 0, + array: 0, + minimum: history.count, + key: immutableKey(history, 0, 0, true), + cut: `reordered record properties`, + }, + { + object: 1, + array: 0, + minimum: history.count, + key: immutableKey(history, 1, 0), + cut: `changed nested record value`, + }, + { + object: 1, + array: 0, + minimum: -1, + key: immutableKey(history, 1, 0), + cut: `equal key after replacement`, + }, + { + object: 1, + array: 1, + minimum: -1, + key: immutableKey(history, 1, 1), + cut: `changed nested array value`, + }, + { + object: 0, + array: 0, + minimum: 0, + key: firstKey, + cut: `return to earlier key`, + }, + ...history.tail.map((step) => ({ + ...step, + key: immutableKey(history, step.object, step.array, step.reordered), + cut: `generated key proposal`, + })), + ] + for (const step of steps) { + const retain = + previous.object === step.object && previous.array === step.array + const collection = mounted.collection() + const changedPlan = minimum !== step.minimum + if (!retain) minimum = step.minimum + await mounted.setParam({ key: step.key, minimum: step.minimum }) + // The driver's native settled cut supplies the real host witness. Check + // data, keyed state and Collection identity without normalizing output. + // An invalidated key with an identical plan leaves Collection reuse free. + compare(step.cut, retain || changedPlan ? collection : undefined, retain) + if (step.cut !== `generated key proposal`) reach.reach(step.cut) + previous = step + } + }) +} + +function registerImmutableKeyOracle(driver: LiveQueryDriver) { + for (const campaign of [`fixed`, `fresh`] as const) { + const property = windowHistoryProperty(driver, `immutable-key-history`) + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`immutable query key histories (${campaign})`, async () => { + const reach = createOracleReach( + `${property}/${campaign}`, + [ + `same key object`, + `fresh outer array`, + `fresh equal nested values`, + `reordered record properties`, + `changed nested record value`, + `equal key after replacement`, + `changed nested array value`, + `return to earlier key`, + ], + { minimum: oracleRuns(6) }, + ) + const fidelity = createOracleFailureFidelity() + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(immutableKeyArbitrary, async (history) => { + reach.beginHistory() + await fidelity.run(history, () => + checkImmutableKeyHistory(driver, history, reach), + ) + }), + campaign === `fixed` + ? { numRuns: oracleRuns(6), seed: 2345 } + : oraclePropertyOptions(6, property), + ), + ) + reach.assertReached() + } finally { + console.info( + `immutable key reach ${driver.name}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }) + } +} +declareOracle({ + grammar: [immutableKeyArbitrary], + check: [checkImmutableKeyHistory], +}) diff --git a/packages/db/tests/live-query-cache-options-oracle.ts b/packages/db/tests/live-query-cache-options-oracle.ts index 9226d09fc4..90bf7654c9 100644 --- a/packages/db/tests/live-query-cache-options-oracle.ts +++ b/packages/db/tests/live-query-cache-options-oracle.ts @@ -186,8 +186,9 @@ export function checkCacheOptionPublications( checkCacheOptionRows(publication, rows, key) } -/** Independent Suspense callers share only compatible local output options. - * Authority: maintainer decision during PR #2314 review, 2026-10-30. Portable +/** Independent Suspense callers own distinct output Collections, even with equal + * declarations. Authority: maintainer decision, 2026-11-03, superseding the + * compatible-sharing rule from PR #2314. Portable * query identity and mounted first-options retention remain separate laws. * Declaration names abstract absent or exact runtime references/scalar values. * Each option varies alone, so a combined-option guard cannot hide a gap. @@ -223,17 +224,19 @@ export const localOptionPairs = localOptionHistories.flatMap((option) => ), ) export type LocalOptionHistory = (typeof localOptionPairs)[number] -export function compatibleLocalOptions(history: LocalOptionHistory) { +export function equalLocalDeclarations(history: LocalOptionHistory) { const effective = history.declarations.map((declaration) => declaration === `undefined` ? `absent` : declaration, ) return history.identity !== `different-key` && effective[0] === effective[1] } -export function checkLocalOptionSharing( +export function checkLocalOutputOwnership( left: object, right: object, history: LocalOptionHistory, ) { - if (compatibleLocalOptions(history)) expect(right).toBe(left) - else expect(right).not.toBe(left) + expect( + right, + `independent Suspense output (${history.option}/${history.identity})`, + ).not.toBe(left) } diff --git a/packages/db/tests/live-query-observer-hydration-oracle.property.test.ts b/packages/db/tests/live-query-observer-hydration-oracle.property.test.ts index e7995997e0..f3f2ab9aba 100644 --- a/packages/db/tests/live-query-observer-hydration-oracle.property.test.ts +++ b/packages/db/tests/live-query-observer-hydration-oracle.property.test.ts @@ -3,9 +3,18 @@ import { afterAll, describe, expect, it } from 'vitest' import { DbClient } from '../src/client.js' import { createCollection } from '../src/collection/index.js' import { createLiveQueryObserver } from '../src/live-query-observer.js' -import { oraclePropertyOptions, oracleRuns } from './oracle-config.js' +import { + oraclePropertyOptions, + oracleReplayTest, + oracleRuns, +} from './oracle-config.js' import { declareOracle } from './oracle-declaration.js' import { createOracleReach } from './oracle-reach.js' +import { ScenarioLifetime } from './conformance/scenario-lifetime.js' +import { + atOracleCheckpoint, + createOracleFailureFidelity, +} from './oracle-failure-fidelity.js' import type { ChangeMessage, CollectionStatus, @@ -68,7 +77,8 @@ import type { | failure ends the seed | the same, with cleanup after the failure | notify/attach -> seed handoff | `cleaned-up` and live rows after cleanup | this generated oracle | | cleanup keeps the seed | cleanup and restart while the seed is active | notify early return under the seed | `ready` and seed rows | this generated oracle; recorded decision, not a law-16 claim | | announced changes | every step while attached | emit -> flushPublications | last-read snapshot and granular rebuilt rows equal the current snapshot | this generated oracle | -| handoff timing, on-demand subsets, streamed errors, consumption | | | | focused `live-query-observer.test.ts` hydration tests | +| already-ready subset handoff | held subset at attach, then settlement without row/status change; both modes | seeded observer -> subset settlement | seed while held, live snapshot and announced granular/wholesale rows after settlement | generated ready-subset lane below; Svelte page receiver | +| other handoff timing, streamed errors, consumption | | | | focused `live-query-observer.test.ts` hydration tests | * Keys are `a`..`c`, versions 0..3, histories of 1..10 generated steps after * an optional fixed three-step prefix, both modes. Rows are @@ -479,3 +489,182 @@ describe(`LiveQueryObserver hydration seed under terminal statuses`, () => { }) } }) + +/** + * Readiness also includes the end of pending subset work on an already ready + * Collection. A retained page can stay ready throughout that work (#2345). + * The existing terminal-status grammar cannot generate this boundary. The + * hydration handoff law above and the SSR guide require the seed until local + * work settles, then live rows, with an announcement for both consumer modes. + * + * This lane uses a plain on-demand Collection as the observer's input. An + * external subscription establishes held subset work before observer attach. + * Resolving its controlled load changes no row or Collection status. The + * shared expectedView model predicts seed -> live from that explicit event; + * production's subset/status classifiers never choose the expected phase. + * The real Svelte transported-page receiver supplies this same premise through + * retained live-query Collections and native rune scheduling. + */ +const subsetHandoffArbitrary = fc.record({ + count: fc.integer({ min: 1, max: 3 }), + liveCount: fc.integer({ min: 0, max: 3 }), + version: fc.integer({ min: 0, max: 3 }), +}) +type SubsetHandoff = { count: number; liveCount: number; version: number } +async function checkSubsetHandoff( + input: SubsetHandoff, + mode: Mode, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + const client = new DbClient() + let observer: + ReturnType> | undefined + let unsubscribe: (() => void) | undefined + lifetime.defer(() => { + observer?.dispose() + unsubscribe?.() + }) + lifetime.defer(() => client.cleanup()) + await lifetime.run(async () => { + let release!: () => void + const pending = new Promise((resolve) => { + release = resolve + }) + const local = Array.from({ length: input.liveCount }, (_, i) => ({ + id: String(i), + version: input.version + 1, + })) + const source = createCollection({ + getKey: (row) => row.id, + syncMode: `on-demand`, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (const value of local) write({ type: `insert`, value }) + commit() + markReady() + return { loadSubset: () => pending } + }, + }, + }) + lifetime.defer(() => source.cleanup()) + const demand = source.subscribeChanges(() => {}, { + includeInitialState: true, + }) + unsubscribe = () => demand.unsubscribe() + expect(source.status, `local status is already ready`).toBe(`ready`) + expect(source.isLoadingSubset, `external demand is held`).toBe(true) + const model: Model = { + seed: Array.from({ length: input.count }, (_, i) => [ + String(i), + input.version, + ]), + ended: false, + endedByFailure: false, + attached: true, + } + const queryHash = `held-ready-subset` + client.hydrate({ + collections: [], + liveQueries: [ + { + queryHash, + dehydratedAt: 1, + snapshot: { + rows: model.seed.map(([rowKey, rowVersion]) => ({ + key: rowKey, + value: { id: rowKey, version: rowVersion }, + })), + }, + }, + ], + }) + observer = createLiveQueryObserver(source, { client, queryHash, mode }) + const read = (): View => { + const snapshot = observer!.getSnapshot() + return { status: snapshot.status, rows: sortedRows(snapshot.state ?? []) } + } + const rebuilt = new Map() + let lastRead: View | undefined + observer.subscribe((changes) => { + if (!changes) { + rebuilt.clear() + for (const [rowKey, row] of observer!.getSnapshot().state ?? []) + rebuilt.set(rowKey, row) + } + for (const change of changes ?? []) { + if (change.type === `delete`) rebuilt.delete(change.key) + else rebuilt.set(change.key, change.value) + } + lastRead = read() + }) + lastRead = read() // the host's post-subscription consistency read + const compare = (cut: string) => + atOracleCheckpoint(`seeded subset handoff`, cut, () => { + const expected = expectedView( + model, + `ready`, + local.map((row) => [row.id, row.version]), + ) + expect(read()).toEqual(expected) + expect(lastRead, `handoff is announced`).toEqual(expected) + if (mode === `granular`) + expect(sortedRows(rebuilt), `granular rows follow handoff`).toEqual( + expected.rows, + ) + }) + await settle() // the initial scheduled handoff must leave held work alone + compare(`ready Collection with held subset`) + reach.reach(`ready while subset held`) + release() + await settle() + expect(source.isLoadingSubset, `the held work settled`).toBe(false) + expect(source.status, `settlement changes no Collection status`).toBe( + `ready`, + ) + model.ended = true + compare(`subset settled without rows or status changing`) + reach.reach(`subset-only handoff`) + }) +} +for (const mode of [`granular`, `wholesale`] as const) + for (const campaign of [`fixed`, `fresh`] as const) { + const property = `live-query-observer.hydration-subset-handoff` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`${mode} seeded ready-subset handoff (${campaign})`, async () => { + const reach = createOracleReach( + `${property}/${mode}/${campaign}`, + [`ready while subset held`, `subset-only handoff`], + ) + const fidelity = createOracleFailureFidelity() + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(subsetHandoffArbitrary, async (input) => { + reach.beginHistory() + await fidelity.run(input, () => + checkSubsetHandoff(input, mode, reach), + ) + }), + campaign === `fixed` + ? { numRuns: oracleRuns(8), seed: 2345 } + : oraclePropertyOptions(8, property), + ), + ) + reach.assertReached() + } finally { + console.info( + `subset handoff reach ${mode}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }) + } +declareOracle({ + grammar: [subsetHandoffArbitrary], + check: [checkSubsetHandoff], +}) diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 5238c61180..3cc8ee9f13 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -48,7 +48,51 @@ const staticOracleProperties = [ `framework.svelte.window-history`, `framework.svelte.pending-window-history`, `framework.svelte.publication-window-history`, + `framework.react-DbClient.window-history`, + `framework.react-DbClient.pending-window-history`, + `framework.react-DbClient.publication-window-history`, + `framework.react-Suspense.window-history`, + `framework.react-Suspense.pending-window-history`, + `framework.react-Suspense.publication-window-history`, + `framework.react-Suspense-DbClient.window-history`, + `framework.react-Suspense-DbClient.pending-window-history`, + `framework.react-Suspense-DbClient.publication-window-history`, + `framework.svelte-DbClient.window-history`, + `framework.svelte-DbClient.pending-window-history`, + `framework.svelte-DbClient.publication-window-history`, + `framework.react.peer-window-history`, + `framework.react-dependency-list.peer-window-history`, + `framework.preact.peer-window-history`, + `framework.vue.peer-window-history`, + `framework.solid.peer-window-history`, + `framework.angular.peer-window-history`, + `framework.svelte.peer-window-history`, + `framework.react-DbClient.peer-window-history`, + `framework.react-Suspense.peer-window-history`, + `framework.react-Suspense-DbClient.peer-window-history`, + `framework.svelte-DbClient.peer-window-history`, + `react.client-window`, + `react.suspense-retry-history`, + `react.suspense-siblings`, + `react.suspense-replacement-history`, + `react.suspense-preload-restart-history`, + `svelte.client-window`, + `svelte.client-window-recovery`, + `live-query-observer.hydration-subset-handoff`, + `framework.react.keyed-admission`, + `framework.react-dependency-list.keyed-admission`, + `framework.preact.keyed-admission`, + `framework.react-DbClient.keyed-admission`, + `framework.svelte-DbClient.keyed-admission`, + `framework.react.immutable-key-history`, + `framework.react-dependency-list.immutable-key-history`, + `framework.react-DbClient.immutable-key-history`, + `framework.react-Suspense.immutable-key-history`, + `framework.react-Suspense-DbClient.immutable-key-history`, + `framework.preact.immutable-key-history`, + `framework.svelte-DbClient.immutable-key-history`, `react.window-render`, + `react.window-retry-ownership`, `preact.window-render`, `lint.part-reach`, `oracle.declared-reach`, diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index e1d6db09ec..5bd8dd02e5 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -36,49 +36,80 @@ const ownerGroups: ReadonlyArray< `db/tests/conformance/result-shape-oracle.ts`, ] as const, ), + ...[ + [`react`, `react-db/tests/conformance.test.tsx`], + [`react-dependency-list`, `react-db/tests/conformance.test.tsx`], + [`react-DbClient`, `react-db/tests/conformance.test.tsx`], + [`react-Suspense`, `react-db/tests/conformance.test.tsx`], + [`react-Suspense-DbClient`, `react-db/tests/conformance.test.tsx`], + [`preact`, `preact-db/tests/conformance.test.ts`], + [`vue`, `vue-db/tests/conformance.test.ts`], + [`solid`, `solid-db/tests/conformance.test.tsx`], + [`angular`, `angular-db/tests/conformance.test.ts`], + [`svelte`, `svelte-db/tests/conformance.svelte.test.ts`], + [`svelte-DbClient`, `svelte-db/tests/conformance.svelte.test.ts`], + ].map( + ([driver, owner]) => + [ + owner!, + `framework.${driver}`, + `window-history pending-window-history publication-window-history peer-window-history`, + `db/tests/conformance/window-history-oracle.ts`, + ] as const, + ), + ...[ + [`react`, `react-db/tests/conformance.test.tsx`], + [`react-dependency-list`, `react-db/tests/conformance.test.tsx`], + [`react-DbClient`, `react-db/tests/conformance.test.tsx`], + [`preact`, `preact-db/tests/conformance.test.ts`], + [`svelte-DbClient`, `svelte-db/tests/conformance.svelte.test.ts`], + ].map( + ([driver, owner]) => + [ + owner!, + `framework.${driver}`, + `keyed-admission`, + `db/tests/conformance/window-history-oracle.ts`, + ] as const, + ), + [`react-db/tests/client-window-oracle.test.tsx`, `react`, `client-window`], + ...[ + [`react`, `react-db/tests/conformance.test.tsx`], + [`react-dependency-list`, `react-db/tests/conformance.test.tsx`], + [`react-DbClient`, `react-db/tests/conformance.test.tsx`], + [`react-Suspense`, `react-db/tests/conformance.test.tsx`], + [`react-Suspense-DbClient`, `react-db/tests/conformance.test.tsx`], + [`preact`, `preact-db/tests/conformance.test.ts`], + [`svelte-DbClient`, `svelte-db/tests/conformance.svelte.test.ts`], + ].map( + ([driver, owner]) => + [ + owner!, + `framework.${driver}`, + `immutable-key-history`, + `db/tests/conformance/window-history-oracle.ts`, + ] as const, + ), [ - `react-db/tests/conformance.test.tsx`, - `framework.react`, - `window-history pending-window-history publication-window-history`, - `db/tests/conformance/window-history-oracle.ts`, - ], - [ - `react-db/tests/conformance.test.tsx`, - `framework.react-dependency-list`, - `window-history pending-window-history publication-window-history`, - `db/tests/conformance/window-history-oracle.ts`, - ], - [ - `preact-db/tests/conformance.test.ts`, - `framework.preact`, - `window-history pending-window-history publication-window-history`, - `db/tests/conformance/window-history-oracle.ts`, - ], - [ - `vue-db/tests/conformance.test.ts`, - `framework.vue`, - `window-history pending-window-history publication-window-history`, - `db/tests/conformance/window-history-oracle.ts`, + `svelte-db/tests/client-window-oracle.svelte.test.ts`, + `svelte`, + `client-window client-window-recovery`, ], [ - `solid-db/tests/conformance.test.tsx`, - `framework.solid`, - `window-history pending-window-history publication-window-history`, - `db/tests/conformance/window-history-oracle.ts`, + `db/tests/live-query-observer-hydration-oracle.property.test.ts`, + `live-query-observer`, + `hydration-subset-handoff`, ], [ - `angular-db/tests/conformance.test.ts`, - `framework.angular`, - `window-history pending-window-history publication-window-history`, - `db/tests/conformance/window-history-oracle.ts`, + `react-db/tests/window-render-oracle.test.tsx`, + `react`, + `window-render window-retry-ownership`, ], [ - `svelte-db/tests/conformance.svelte.test.ts`, - `framework.svelte`, - `window-history pending-window-history publication-window-history`, - `db/tests/conformance/window-history-oracle.ts`, + `react-db/tests/suspense-lifecycle-oracle.test.tsx`, + `react`, + `suspense-retry-history suspense-replacement-history suspense-preload-restart-history suspense-siblings`, ], - [`react-db/tests/window-render-oracle.test.tsx`, `react`, `window-render`], [`preact-db/tests/window-render-oracle.test.tsx`, `preact`, `window-render`], [ `react-db/tests/ssr-effects-oracle.test.ts`, @@ -893,6 +924,15 @@ const replayTestNames = new Map([ `generated same-key page shifts.*replay campaign`, ], [`react.window-render`, `window changes at React commit.*fresh`], + [`react.window-retry-ownership`, `retry ownership/.*fresh`], + [`react.client-window`, `client page identity.*fresh`], + [`svelte.client-window`, `Svelte client page identity.*fresh`], + [`svelte.client-window-recovery`, `client key recovery/.*fresh`], + [`react.suspense-siblings`, `co-mounted siblings/.*fresh`], + [ + `live-query-observer.hydration-subset-handoff`, + `seeded ready-subset handoff \\(fresh\\)`, + ], [`preact.window-render`, `window changes at Preact commit.*fresh`], [`ivm.topk-numeric-window`, `Numeric window oracle:.*random campaign`], [ @@ -1062,6 +1102,12 @@ function directReplayTestName(property: string): string | undefined { return `pending window histories \\(fresh\\)` case `publication-window-history`: return `publication callback window histories \\(fresh\\)` + case `peer-window-history`: + return `peer page histories \\(fresh\\)` + case `keyed-admission`: + return `initial disabled keyed admission \\(fresh\\)` + case `immutable-key-history`: + return `immutable query key histories \\(fresh\\)` } } if (property.startsWith(`react.ssr-effects-`)) { diff --git a/packages/db/tests/oracle-replay.test.ts b/packages/db/tests/oracle-replay.test.ts index a188f77ab7..e79e90a43e 100644 --- a/packages/db/tests/oracle-replay.test.ts +++ b/packages/db/tests/oracle-replay.test.ts @@ -412,17 +412,52 @@ describe(`named oracle replay manifest`, () => { [ [`react`, `react-db/tests/conformance.test.tsx`], [`react-dependency-list`, `react-db/tests/conformance.test.tsx`], + [`react-DbClient`, `react-db/tests/conformance.test.tsx`], + [`react-Suspense`, `react-db/tests/conformance.test.tsx`], + [`react-Suspense-DbClient`, `react-db/tests/conformance.test.tsx`], [`preact`, `preact-db/tests/conformance.test.ts`], [`vue`, `vue-db/tests/conformance.test.ts`], [`solid`, `solid-db/tests/conformance.test.tsx`], [`angular`, `angular-db/tests/conformance.test.ts`], [`svelte`, `svelte-db/tests/conformance.svelte.test.ts`], + [`svelte-DbClient`, `svelte-db/tests/conformance.svelte.test.ts`], ].flatMap(([driver, owner]) => [ `window-history`, `pending-window-history`, `publication-window-history`, - `result-shape`, + `peer-window-history`, + ...([ + `react`, + `react-dependency-list`, + `react-DbClient`, + `react-Suspense`, + `react-Suspense-DbClient`, + `preact`, + `svelte-DbClient`, + ].includes(driver!) + ? [`immutable-key-history`] + : []), + ...([ + `react`, + `react-dependency-list`, + `preact`, + `vue`, + `solid`, + `angular`, + `svelte`, + ].includes(driver!) + ? [`result-shape`] + : []), + ...([ + `react`, + `react-dependency-list`, + `react-DbClient`, + `preact`, + `svelte-DbClient`, + ].includes(driver!) + ? [`keyed-admission`] + : []), ].map((family) => [`framework.${driver}.${family}`, owner] as const), ), )(`keeps the originating driver for %s`, (property, owner) => { @@ -433,6 +468,7 @@ describe(`named oracle replay manifest`, () => { file: `packages/${owner}`, status: `assertion`, source: `packages/db/tests/conformance/${property.endsWith(`.result-shape`) ? `result-shape-oracle` : `window-history-oracle`}.ts`, + testNamePattern: expect.any(String), }) }) @@ -506,7 +542,28 @@ describe(`named oracle replay manifest`, () => { driver === `react-dependency-list` ? `react (dependency list)` : driver - expect(driverSource).toContain(`name: \`${driverName}\``) + if ( + [ + `react-DbClient`, + `react-Suspense`, + `react-Suspense-DbClient`, + ].includes(driver!) + ) { + // These three names come from one finite runtime product. Require + // both generating axes and the exact naming expression; guarded + // receiving replays verify the resulting names actually execute. + expect(driverSource).toContain( + `for (const suspense of [false, true])`, + ) + expect(driverSource).toContain(`for (const client of [false, true])`) + expect(driverSource).toContain( + 'name: `react${suspense ? ` Suspense` : ``}${client ? ` (DbClient)` : ``}`', + ) + } else { + expect(driverSource).toContain( + `name: \`${driverName === `svelte-DbClient` ? `svelte (DbClient)` : driverName}\``, + ) + } if (family === `result-shape`) expect(source).toContain(`framework.\${id}.result-shape`) else diff --git a/packages/preact-db/src/useLiveQuery.ts b/packages/preact-db/src/useLiveQuery.ts index 0949e0a6e7..d9f01e6f60 100644 --- a/packages/preact-db/src/useLiveQuery.ts +++ b/packages/preact-db/src/useLiveQuery.ts @@ -253,7 +253,7 @@ export function useLiveQuery( const bindingRef = useRef(null) let binding = bindingRef.current let commitWindow: (() => void) | undefined - if (binding?.identity !== identity) { + if (!binding?.collection || binding.identity !== identity) { const collection = resolveLiveQueryValue(preparedValue, { gcTime: DEFAULT_GC_TIME_MS, previousCollection: binding?.collection ?? null, diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index d89793feb0..22a01f4eb5 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -2,12 +2,11 @@ import { useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react' import { - UnhashableQueryIRError, createLiveQueryObserver, createLiveQuerySourceIdClaims, deepEquals, + getLiveQueryHash, getLiveQueryKey, - getSourceObjectToken, getStableValueHash, identifyLiveQuery, isCollection, @@ -23,6 +22,7 @@ import { getPreparedSources, getUnboundDescriptorReporter, } from './source-id-bindings' +import type { SuspenseBinding } from './useLiveSuspenseQuery' import type { Collection, CollectionImpl, @@ -51,8 +51,8 @@ const useWindowCommitEffect = typeof window === `undefined` ? useEffect : useLayoutEffect const DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC) -// Suspense renders can be abandoned before React subscribes. Keep their -// collection briefly so a nearby retry can commit without starting over. +// Suspense can discard refs before the first commit. Unclaimed preloads live +// briefly across retries; a successful binding takes exclusive ownership. const DEFAULT_SUSPENSE_GC_TIME_MS = 5000 const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16 const DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10 @@ -60,61 +60,6 @@ const DERIVED_IDENTITY_TOTAL_WARN_MS = 50 const warnedDerivedIdentityCallsites = new Set() const unpreparedQueryValue = Symbol(`unpreparedQueryValue`) -type SuspenseCollection = Collection -type SuspenseCollectionEntry = { - collection: SuspenseCollection - removeStatusListeners: Array<() => void> -} - -const unscopedSuspenseScope = {} -const suspenseCollectionsByClient = new WeakMap< - object, - Map ->() -function getSourceQualifiedSuspenseKey( - preparedValue: unknown, - queryHash: string, - client: DbClient | undefined, -): string { - const sourceIds = getPreparedSources( - preparedValue, - client, - `useLiveSuspenseQuery`, - ).map(getSourceObjectToken) - const { - query: _query, - pool: _pool, - ...options - } = (preparedValue as { query?: unknown } | null | undefined)?.query - ? (preparedValue as Record) - : {} - // Local output options qualify sharing; query identity still binds mounted - // options. Object/function declarations compare by exact runtime reference. - options.autoIndex ??= `off` - for (const key in options) { - const value = options[key] - if (value === undefined) delete options[key] - else if ( - value && - (typeof value === `object` || typeof value === `function`) - ) - options[key] = getSourceObjectToken(value) - } - return getStableValueHash([sourceIds, options, queryHash]) -} - -function getSuspenseCollections( - client: DbClient | undefined, -): Map { - const scope = client ?? unscopedSuspenseScope - let collections = suspenseCollectionsByClient.get(scope) - if (!collections) { - collections = new Map() - suspenseCollectionsByClient.set(scope, collections) - } - return collections -} - export type DerivedIdentityProfiler = { renderCount: number totalMs: number @@ -573,11 +518,6 @@ export function useLiveQuery(configOrQueryOrCollection: any) { return useLiveQueryImpl(configOrQueryOrCollection) as any } -/** @internal Shared implementation for the Suspense wrapper. */ -export function useLiveQueryForSuspense(configOrQueryOrCollection: unknown) { - return useLiveQueryImpl(configOrQueryOrCollection, true) -} - // What one hook instance keeps across renders. Fields produced by resolution // are installed with their first value, rather than initialized and overwritten // in the same render. @@ -592,7 +532,6 @@ type HookInstance = { cancelReclaimRelease?: () => void observer?: LiveQueryObserver queryHash?: string - identityError?: UnhashableQueryIRError subscribe?: (onStoreChange: () => void) => () => void } @@ -600,20 +539,26 @@ type LiveQueryResult = ReturnType< LiveQueryObserver[`getSnapshot`] > -function useLiveQueryImpl(configOrQueryOrCollection: unknown): LiveQueryResult -function useLiveQueryImpl( +/** @internal Shared implementation for ordinary and Suspense bindings. */ +export function useLiveQueryImpl( + configOrQueryOrCollection: unknown, +): LiveQueryResult +export function useLiveQueryImpl( configOrQueryOrCollection: unknown, - forSuspense: true, + suspense: SuspenseBinding, ): { result: LiveQueryResult client: DbClient | undefined queryHash: string | undefined - identityError: UnhashableQueryIRError | undefined observer: LiveQueryObserver /** Sync starts this render deferred, released if the render throws. */ deferredCollections: DeferredLiveQueryCollections } -function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { +export function useLiveQueryImpl( + configOrQueryOrCollection: any, + suspense?: SuspenseBinding, +) { + const forSuspense = suspense !== undefined const contextDbClient = useOptionalDbClient() // Check if it's already a collection const inputIsCollection = isCollection(configOrQueryOrCollection) @@ -646,15 +591,11 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { let preparedQueryValue: unknown | typeof unpreparedQueryValue = unpreparedQueryValue let identityDeps: ReadonlyArray - let streamIdentity: ReadonlyArray - let identityError: UnhashableQueryIRError | undefined if (queryKey) { identityDeps = queryKey - streamIdentity = [`queryKey`, queryKey] } else if (inputIsCollection) { identityDeps = [] - streamIdentity = [`collection`, configOrQueryOrCollection.id] } else { const preparation = prepareDerivedQuery( configOrQueryOrCollection, @@ -664,20 +605,11 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { ) preparedQueryValue = preparation.value identityDeps = preparation.identityDeps - streamIdentity = preparation.identityDeps - } - - let queryHash: string | undefined - try { - queryHash = getStableValueHash(streamIdentity, `queryKey`) - } catch (error) { - if (!(error instanceof UnhashableQueryIRError) || queryKey !== undefined) - throw error - identityError = error } // Check if we need to create/recreate the collection let needsNewCollection = + suspense?.needsOwnCollection || !instance.collection || (inputIsCollection ? instance.config !== configOrQueryOrCollection @@ -696,8 +628,24 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { ) } + let queryHash = instance.queryHash let commitWindow: (() => void) | undefined + let compile: + | (( + ownsCollection: boolean, + windowOwner: object, + ) => Collection | null) + | undefined if (needsNewCollection) { + // Preparation rejects unhashable derived inputs; keyed inputs must hash. + queryHash = queryKey + ? getLiveQueryHash(preparedQueryValue, queryKey) + : getStableValueHash( + inputIsCollection + ? [`collection`, configOrQueryOrCollection.id] + : identityDeps, + `queryKey`, + ) if (inputIsCollection) { // Warn when passing a collection directly with on-demand sync mode // In on-demand mode, data is only loaded when queries with predicates request it @@ -722,98 +670,60 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { configOrQueryOrCollection.startSyncImmediate() instance.collection = configOrQueryOrCollection } else { - // Only a new binding consults the shared cache. Mounted bindings keep - // their Collection and its first options until query identity changes. - // The creation path above has already prepared keyed inputs too. - const suspenseKey = - forSuspense && queryHash - ? getSourceQualifiedSuspenseKey( - preparedQueryValue, - queryHash, - dbClient, - ) - : undefined - const suspenseCollections = suspenseKey - ? getSuspenseCollections(dbClient) - : undefined - const suspenseCollection = suspenseCollections?.get( - suspenseKey!, - )?.collection - if (suspenseCollection) { - instance.collection = suspenseCollection - } else { + compile = (ownsCollection: boolean, windowOwner: object) => { // A keyed query claims nothing, so it reports an unbound descriptor // here; the other forms reported it when they claimed. if (!dbClient && queryKey !== undefined) { getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`) } - instance.collection = resolveLiveQueryValue(preparedQueryValue, { + const collection = resolveLiveQueryValue(preparedQueryValue, { gcTime: forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS, - // Hydration and Suspense key the live-query Collection by identity, - // so they never pool, even when the config passes a pool. + // Suspense and client observers have distinct preload/snapshot + // policies, so neither uses equality-partition pooling. pool: forSuspense || dbClient ? false : undefined, previousCollection: - forSuspense || dbClient ? undefined : instance.collection, - windowOwner: instance, + ownsCollection && instance.client === dbClient + ? instance.collection + : null, + windowOwner, onWindowChange: (move) => { commitWindow = move }, // Without a DbClient, Suspense starts it through a preload below. startSync: !forSuspense || dbClient !== undefined, - }) as SuspenseCollection | null + }) // A render that never commits releases its deferred sync starts // when garbage collection reclaims the live query it built. A // Collection the callback returned is not this render's to reclaim. - if (instance.collection && !isCollection(preparedQueryValue)) { + if (collection && !isCollection(preparedQueryValue)) { instance.cancelReclaimRelease?.() instance.cancelReclaimRelease = releaseDeferredOnReclaim( - instance.collection, + collection, instance.deferredCollections, ) } - if (suspenseCollections && instance.collection) { - const collection = instance.collection - const releaseCollection = () => { - const entry = suspenseCollections.get(suspenseKey!) - if (entry?.collection !== collection) return - suspenseCollections.delete(suspenseKey!) - entry.removeStatusListeners.forEach((remove) => remove()) - } - // Keep a failed or crashed collection through React's immediate - // retry so the hook can throw its actual error to an ErrorBoundary. - // Retire it afterward so a later boundary reset starts fresh. - const retireAfterRetry = () => setTimeout(releaseCollection, 0) - suspenseCollections.set(suspenseKey!, { - collection, - removeStatusListeners: [ - collection.on(`status:cleaned-up`, releaseCollection), - collection.on(`status:error`, retireAfterRetry), - collection.on(`status:crashed`, retireAfterRetry), - ], - }) - } + return collection } + instance.collection = suspense + ? suspense.resolve(preparedQueryValue, queryHash, dbClient, compile) + : compile(true, instance) // An abandoned render must leave invalidation pending. Its retry or // urgent replacement still prepares and commits the requested move. if (!commitWindow) instance.identityDeps = [...identityDeps] } instance.config = configOrQueryOrCollection instance.client = dbClient - instance.queryHash = queryHash - instance.identityError = identityError + if (!commitWindow) instance.queryHash = queryHash } - if (commitWindow) needsNewCollection = false - useWindowCommitEffect(() => { - if (commitWindow) { - instance.identityDeps = [...identityDeps] - commitWindow() - } - }) - - if (needsNewCollection) { + // A client-backed move retains the graph but binds a fresh observer to the + // requested page's transport hash. Peers keep their own snapshots/observers. + if (commitWindow && !dbClient) needsNewCollection = false + let observer = instance.observer! + let subscribe = instance.subscribe! + const watchCollection = () => { // Recreate the observer when the underlying collection changes. The observer // is not disposed explicitly here or on unmount: `useSyncExternalStore` // unsubscribes it when the subscribe changes or the component unmounts, which @@ -826,24 +736,33 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { // hook's pre-observer loading policy, and — because wholesale delivers // nothing synchronously during subscribe — never notifies // useSyncExternalStore inside its own subscribe call. - const observer = (instance.observer = createLiveQueryObserver( - instance.collection, - { - mode: `wholesale`, - client: dbClient, - queryHash: instance.queryHash, - onPreload: resumeDeferredCollections, - suspendUntilReady: forSuspense, - }, - )) + observer = createLiveQueryObserver(instance.collection, { + mode: `wholesale`, + client: dbClient, + queryHash, + onPreload: resumeDeferredCollections, + suspendUntilReady: forSuspense, + }) // The subscribe function stays bound to the observer that created it. - instance.subscribe = (onStoreChange: () => void) => { + subscribe = (onStoreChange: () => void) => { const unsubscribe = observer.subscribe(onStoreChange) resumeDeferredCollections() return unsubscribe } } - const observer = instance.observer! + if (needsNewCollection) watchCollection() + // Until commit, a proposed page's observer and transport identity belong + // only to this render. Cancellation must leave the committed seed intact. + useWindowCommitEffect(() => { + if (commitWindow) { + instance.identityDeps = [...identityDeps] + instance.queryHash = queryHash + instance.observer = observer + instance.subscribe = subscribe + commitWindow() + } + claim?.() + }) // A Suspense render asks for its data. Preloading starts the sync run, so a // source that already holds the rows makes it ready before the snapshot is // read instead of suspending once. useLiveSuspenseQuery surfaces failures. @@ -857,19 +776,34 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { observer.preload().catch(() => {}) } + const privateCollection = compile && suspense?.forkReady(observer, compile) + if (privateCollection) { + instance.collection = privateCollection + watchCollection() + } + if (!commitWindow) { + instance.observer = observer + instance.subscribe = subscribe + } const returned = useSyncExternalStore( - instance.subscribe!, - () => observer.getSnapshot(), - () => observer.getServerSnapshot(forSuspense), + subscribe, + () => { + const snapshot = observer.getSnapshot() + return suspense ? suspense.read(snapshot) : snapshot + }, + () => { + const snapshot = observer.getServerSnapshot(forSuspense) + return suspense ? suspense.read(snapshot) : snapshot + }, ) + const claim = returned.isReady ? suspense?.claim() : undefined // Pass render-local metadata directly to the Suspense wrapper. The public // hook returns the observer's stable snapshot without a wrapper allocation. if (forSuspense) { return { result: returned, client: dbClient, - queryHash: instance.queryHash, - identityError: instance.identityError, + queryHash, observer, deferredCollections: instance.deferredCollections, } diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index b51d4b7d2c..8aaef24f0b 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -1,8 +1,15 @@ 'use client' import { useRef } from 'react' -import { rejectLiveQueryInput, toBoundaryError } from '@tanstack/db' -import { useLiveQueryForSuspense } from './useLiveQuery' +import { + getLiveQueryStatusFlags, + getSourceObjectToken, + getStableValueHash, + rejectLiveQueryInput, + toBoundaryError, +} from '@tanstack/db' +import { useLiveQueryImpl } from './useLiveQuery' +import { getPreparedSources } from './source-id-bindings' import type { UseLiveQueryConfig } from './useLiveQuery' import type { Collection, @@ -12,15 +19,178 @@ import type { HealthNode, InferResultType, InitialQueryBuilder, + LiveQueryObserver, + LiveQuerySnapshot, NonSingleResult, QueryBuilder, SingleResult, } from '@tanstack/db' +type SuspenseCollection = Collection +type SuspenseCollectionEntry = { + collection: SuspenseCollection + windowOwner: object + owner?: SuspenseBinding + observer?: LiveQueryObserver + release: () => void +} + +// Retry resources and first-load errors use the same scope without a DbClient. +const unscopedClient = {} +const suspenseCollectionsByClient = new WeakMap< + object, + Map +>() +function getSourceQualifiedSuspenseKey( + preparedValue: unknown, + queryHash: string, + client: DbClient | undefined, +): string { + const sourceIds = getPreparedSources( + preparedValue, + client, + `useLiveSuspenseQuery`, + ).map(getSourceObjectToken) + const { + query: _query, + pool: _pool, + ...options + } = (preparedValue as { query?: unknown } | null | undefined)?.query + ? (preparedValue as Record) + : {} + // Local output options qualify pending retry adoption; query identity binds + // mounted options. Object/function declarations compare by exact runtime reference. + options.autoIndex ??= `off` + for (const key in options) { + const value = options[key] + if (value === undefined) delete options[key] + else if ( + value && + (typeof value === `object` || typeof value === `function`) + ) + options[key] = getSourceObjectToken(value) + } + return getStableValueHash([sourceIds, options, queryHash]) +} + +/** Uncommitted retries may adopt work; successful outputs have one owner. */ +export class SuspenseBinding { + private entry?: SuspenseCollectionEntry + private seed?: LiveQuerySnapshot + collection?: Collection + promise?: Promise + ready = false + + get needsOwnCollection(): boolean { + return this.entry !== undefined && this.entry.owner !== this + } + + resolve( + input: unknown, + hash: string, + client: DbClient | undefined, + compile: (owned: boolean, windowOwner: object) => SuspenseCollection | null, + ): SuspenseCollection | null { + const scope = client ?? unscopedClient + let cache = suspenseCollectionsByClient.get(scope) + if (!cache) { + cache = new Map() + suspenseCollectionsByClient.set(scope, cache) + } + const owned = this.entry?.owner === this + const key = owned + ? undefined + : getSourceQualifiedSuspenseKey(input, hash, client) + const cached = key === undefined ? undefined : cache.get(key) + if (cached) return (this.entry = cached).collection + + // A new attempt gets a fresh resolver token. Adoption retains that token; + // another attempt must not overwrite the adopted Collection's plan. + const windowOwner = owned ? this.entry!.windowOwner : {} + const collection = compile(owned, windowOwner) + if (owned && collection === this.entry!.collection) return collection + this.seed = undefined + if (collection && this.entry?.collection !== collection) { + const entryKey = key ?? getSourceQualifiedSuspenseKey(input, hash, client) + const release = () => { + if (cache.get(entryKey) === entry) cache.delete(entryKey) + removeStatusListener() + } + const entry = { collection, windowOwner, release } + this.entry = entry + cache.set(entryKey, entry) + const removeStatusListener = collection.on( + `status:change`, + ({ status }) => { + if (status === `cleaned-up`) release() + // Preserve failures through React's immediate retry. + else if (status === `error` || status === `crashed`) + setTimeout(release, 0) + }, + ) + } + return collection + } + + // The first successful candidate may keep the preload. Other candidates get + // private outputs seeded from its completed snapshot. Keep the retry entry + // available until commit, since React can discard a successful candidate. + forkReady( + observer: LiveQueryObserver, + compile: (owned: boolean, windowOwner: object) => SuspenseCollection | null, + ): SuspenseCollection | undefined { + if (!this.entry || this.entry.owner === this) return + const resource = (this.entry.observer ??= observer) + const snapshot = + typeof window === `undefined` + ? resource.getServerSnapshot(true) + : resource.getSnapshot() + if (!snapshot.isReady) return + if (!this.entry.owner) { + this.entry.owner = this + return + } + const windowOwner = {} + const collection = compile(false, windowOwner) + if (!collection) return + this.seed = { ...snapshot, collection } + this.entry = { + collection, + windowOwner, + owner: this, + release: this.entry.release, + } + return collection + } + + read(snapshot: LiveQuerySnapshot) { + if (snapshot.isReady || snapshot.isLoadingError || snapshot.isCrashed) { + // A copied ready view is provisional until this output's first load. + // Its failure must end both the seed and the hook's ready latch. + if (this.seed && snapshot.isLoadingError) this.ready = false + this.seed = undefined + } + // Copy only rows from the peer. This output's observer owns its health, + // including source notifications and query-local request failures. + if (this.seed && snapshot.health !== this.seed.health) { + this.seed = { + ...this.seed, + ...getLiveQueryStatusFlags(`ready`, snapshot.health), + } + } + return this.seed ?? snapshot + } + + claim(): (() => void) | undefined { + // Capture this render's entry: React can replay an older commit's effect + // after a replacement suspends. That effect must release only its entry. + return this.entry?.release + } +} + // React can discard a render that suspends, including its refs. Keep the // initial-render failure across retries, scoped by client because streamed // preloads of the same collection can fail independently. -const unscopedErrorScope = {} const initialRenderErrors = new WeakMap< Collection, WeakMap @@ -30,7 +200,7 @@ function clearInitialRenderError( collection: Collection, client: DbClient | undefined, ) { - initialRenderErrors.get(collection)?.delete(client ?? unscopedErrorScope) + initialRenderErrors.get(collection)?.delete(client ?? unscopedClient) } function rememberInitialRenderError( @@ -48,7 +218,7 @@ function rememberInitialRenderError( }) initialRenderErrors.set(collection, entry) } - entry.set(client ?? unscopedErrorScope, { error, clientQuery }) + entry.set(client ?? unscopedClient, { error, clientQuery }) } /** @@ -193,19 +363,12 @@ export function useLiveSuspenseQuery< // Implementation - uses useLiveQuery internally and adds Suspense logic export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { - const promiseRef = useRef | null>(null) - const collectionRef = useRef | null>(null) - const hasBeenReadyRef = useRef(false) + const bindingRef = useRef(null) + const binding = (bindingRef.current ??= new SuspenseBinding()) // Use useLiveQuery to handle collection management and reactivity - const { - result, - client, - queryHash, - identityError, - observer, - deferredCollections, - } = useLiveQueryForSuspense(configOrQueryOrCollection) + const { result, client, queryHash, observer, deferredCollections } = + useLiveQueryImpl(configOrQueryOrCollection, binding) if (!result.isEnabled) { // Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior @@ -226,10 +389,10 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { // those starts. try { // Reset promise and ready state when query identity changes - if (collectionRef.current !== collection) { - promiseRef.current = null - collectionRef.current = collection - hasBeenReadyRef.current = false + if (binding.collection !== collection) { + binding.promise = undefined + binding.collection = collection + binding.ready = false } // SUSPENSE LOGIC: Throw promise or error based on collection status @@ -238,8 +401,8 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { // Track when we reach ready state if (result.isReady) { - hasBeenReadyRef.current = true - promiseRef.current = null + binding.ready = true + binding.promise = undefined clearInitialRenderError(collection, client) } @@ -249,32 +412,22 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { const clientQuery = client && queryHash ? client._getLiveQuery(queryHash) : undefined if ( - !hasBeenReadyRef.current && + !binding.ready && (observerError !== undefined || clientQuery?.status === `error`) ) { - promiseRef.current = null throw observerError === undefined ? clientQuery?.error : observerError } - if (client?._isSsrStreamingEnabled() && !queryHash) { - const reason = identityError - ? `${identityError.reason} at ${identityError.path}` - : `the query has no stable identity` - throw new Error( - `Cannot stream this live query during SSR because ${reason}. Provide an explicit serializable queryKey.`, - ) - } const initialRenderError = initialRenderErrors .get(collection) - ?.get(client ?? unscopedErrorScope) + ?.get(client ?? unscopedClient) // A replacement client query must not inherit the prior request's error. if ( initialRenderError?.clientQuery !== undefined && initialRenderError.clientQuery !== clientQuery ) { clearInitialRenderError(collection, client) - } else if (initialRenderError && !hasBeenReadyRef.current) { - promiseRef.current = null + } else if (initialRenderError && !binding.ready) { throw toBoundaryError(initialRenderError.error) } @@ -283,21 +436,20 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { // A crash throws at any time: the Collection can no longer serve reads. if ( collectionStatus === `crashed` || - (collectionStatus === `error` && !hasBeenReadyRef.current) + (collectionStatus === `error` && !binding.ready) ) { - promiseRef.current = null throw toBoundaryError(collection.health.error?.cause) } if ( - !hasBeenReadyRef.current && + !binding.ready && (result.isLoading || result.isIdle || result.isLoadingError || result.isCrashed) ) { // Create or reuse promise for current collection - if (!promiseRef.current) { + if (!binding.promise) { let active = true const stopWatchingCleanup = collection.once(`status:cleaned-up`, () => { active = false @@ -311,7 +463,7 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { } const preloadClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : undefined - promiseRef.current = preload + binding.promise = preload .catch((error: unknown) => { const latestClientQuery = client && queryHash ? client._getLiveQuery(queryHash) : undefined @@ -327,7 +479,7 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { .finally(stopWatchingCleanup) } // React Suspense catches this promise and retries after preload settles. - throw promiseRef.current + throw binding.promise } // Return data without status/loading flags (handled by Suspense/ErrorBoundary) @@ -340,6 +492,7 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { } } catch (error) { if (error instanceof Promise) throw error + binding.promise = undefined return rejectLiveQueryInput(deferredCollections, error) } } diff --git a/packages/react-db/tests/client-window-oracle.test.tsx b/packages/react-db/tests/client-window-oracle.test.tsx new file mode 100644 index 0000000000..3496ca8ca1 --- /dev/null +++ b/packages/react-db/tests/client-window-oracle.test.tsx @@ -0,0 +1,431 @@ +/** + * Each client-backed hook owns its live-query Collection. Changing only + * its outer page retains that Collection; another reader and the client's + * transported page snapshots keep their own pages. Authority: issue #2345's + * requested extension of contract.ts, the maintainer's 2026-11-03 independent + * Suspense-output decision, and the SSR guide's preload/hydration identity law. + * + * This receiver reuses the window owner's independent sort/slice judgment. + * Server preload snapshots cross JSON into a real React hook while the browser + * source's readiness is held. A speculative offset/limit move suspends after + * the hook returns, then is cancelled. Its seed cannot replace the committed + * page, including on the next original-page render. Provider and explicit + * clients receive the same histories. Committed moves before source readiness + * must show the matching server page. After readiness, rows follow + * the browser source. A peer remains on its original page. No Electric SDK or + * arbitrary concurrent scheduler behavior is claimed by this finite host model. + */ +import { act, renderHook } from '@testing-library/react' +import { Suspense, startTransition } from 'react' +import { + BTreeIndex, + DbClient, + collectionOptions, + createCollection, +} from '@tanstack/db' +import fc from 'fast-check' +import { describe, expect, it } from 'vitest' +import { useLiveQuery } from '../src/useLiveQuery' +import { useLiveSuspenseQuery } from '../src/useLiveSuspenseQuery' +import { DbProvider } from '../src/DbProvider' +import { + checkWindowObservation, + checkWindowState, + windowRows, +} from '../../db/tests/conformance/window-history-oracle' +import { ScenarioLifetime } from '../../db/tests/conformance/scenario-lifetime' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { createOracleFailureFidelity } from '../../db/tests/oracle-failure-fidelity' +import { + declareOracle, + oraclePropertyOptions, + oracleReplayTest, + oracleRuns, +} from '../../db/tests/oracle-config' +import { + clientWindowArbitrary, + clientWindowExamples as examples, + clientWindowStates as states, +} from '../../db/tests/conformance/client-window-history-oracle' +import type { ReactNode } from 'react' +import type { InitialQueryBuilder } from '@tanstack/db' +import type { WindowRow } from '../../db/tests/conformance/window-history-oracle' + +import type { ClientWindowHistory as History } from '../../db/tests/conformance/client-window-history-oracle' + +// An abandoned render cannot change the committed page's transported view. +// These host cuts supplement the shared settled-page states: local rows remain +// held, so a wrong page hash cannot hide behind an already-ready Collection. +const reactStates = [...states, `suspended seed`, `cancelled seed`] as const +type ClientMode = `provider` | `explicit` +type Move = `offset` | `limit` | `both` + +async function checkClientWindowHistory( + history: History, + keyed: boolean, + suspense: boolean, + clientMode: ClientMode, + move: Move, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + const unmounts: Array<() => void> = [] + lifetime.defer(() => unmounts.forEach((unmount) => unmount())) + const server = new DbClient() + const browser = new DbClient() + lifetime.defer(() => server.cleanup()) + lifetime.defer(() => browser.cleanup()) + await lifetime.run(async () => { + const serverRows: Array = Array.from( + { length: history.count }, + (_, id) => ({ id: `s${id}`, rank: id }), + ) + const browserRows = serverRows.map((row) => ({ + ...row, + id: `b${row.rank}`, + })) + let ready: (() => void) | undefined + const descriptor = collectionOptions(`client-window-source`, (owner) => ({ + getKey: (row: WindowRow) => row.id, + autoIndex: `eager`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (const value of owner === server ? serverRows : browserRows) + write({ type: `insert`, value: { ...value } }) + commit() + if (owner === server) markReady() + else ready = markReady + }, + }, + })) + const first = { offset: history.offset, limit: history.width } + const next = { offset: first.offset + history.jump, limit: first.limit + 1 } + const abandoned = { + offset: move === `limit` ? first.offset : next.offset, + limit: move === `offset` ? first.limit : next.limit, + } + const build = (q: InitialQueryBuilder, page: typeof first) => + q + .from({ row: descriptor }) + .orderBy(({ row }) => row.rank) + .limit(page.limit) + .offset(page.offset) + .select(({ row }) => ({ id: row.id, rank: row.rank })) + const config = (page: typeof first) => ({ + query: (q: InitialQueryBuilder) => build(q, page), + ...(keyed + ? { queryKey: [`client-window`, page.offset, page.limit] } + : {}), + }) + const hookConfig = (page: typeof first) => ({ + ...config(page), + ...(clientMode === `explicit` ? { client: browser } : {}), + }) + for (const page of [first, next, abandoned]) + await server.preloadLiveQuery(config(page)) + const transport = JSON.parse(JSON.stringify(server.dehydrate())) + browser.hydrate(transport) + const expected = (rows: Array, page: typeof first) => + windowRows( + rows, + { ...page, minimum: -1, descending: false, nestedLimit: rows.length }, + false, + ) + let firstRender: { data: unknown; collection: unknown } | undefined + const never = new Promise(() => {}) + let suspended = 0 + const usePage = suspense ? useLiveSuspenseQuery : useLiveQuery + const hook = renderHook( + ({ page, suspend }) => { + const result = usePage(hookConfig(page)) + firstRender ??= result + if (suspend) { + suspended++ + throw never + } + return result + }, + { + initialProps: { page: first, suspend: false }, + wrapper: ({ children }: { children: ReactNode }) => { + const boundary = {children} + return clientMode === `provider` ? ( + {boundary} + ) : ( + boundary + ) + }, + }, + ) + unmounts.push(() => hook.unmount()) + checkWindowObservation( + { rows: firstRender?.data, collection: firstRender?.collection }, + { rows: expected(serverRows, first), retain: true }, + `first preloaded page render`, + ) + reach.reach(`first seed`) + const collection = hook.result.current.collection + const compare = ( + rows: Array, + page: typeof first, + checkpoint: string, + retain = true, + ) => { + checkWindowObservation( + { + rows: hook.result.current.data, + collection: hook.result.current.collection, + }, + { rows: expected(rows, page), previousCollection: collection, retain }, + checkpoint, + ) + checkWindowState( + hook.result.current.state, + expected(rows, page), + checkpoint, + ) + } + const peer = renderHook(() => usePage(hookConfig(first)), { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }) + unmounts.push(() => peer.unmount()) + // A correct data array cannot establish that the peer's keyed view stayed + // on the same transported page. Judge both at every existing peer cut. + const comparePeer = (rows: Array, cut: string) => { + checkWindowObservation( + { + rows: peer.result.current.data, + collection: peer.result.current.collection, + }, + { + rows: expected(rows, first), + previousCollection: collection, + retain: false, + }, + cut, + ) + checkWindowState(peer.result.current.state, expected(rows, first), cut) + } + comparePeer(serverRows, `peer before move`) + await act(() => + startTransition(() => hook.rerender({ page: abandoned, suspend: true })), + ) + expect(suspended, `React attempted the abandoned page`).toBeGreaterThan(0) + compare(serverRows, first, `committed seed during suspended page`) + reach.reach(`suspended seed`) + await act(() => hook.rerender({ page: first, suspend: false })) + compare(serverRows, first, `matching seed after cancelled page`) + reach.reach(`cancelled seed`) + // Both hook kinds own independent Collections. Moving one hook leaves + // the other hook on its original page. + await act(() => { + hook.rerender({ page: next, suspend: false }) + }) + compare( + serverRows, + next, + `moved preloaded page before local readiness`, + true, + ) + reach.reach(`moved seed`) + comparePeer(serverRows, `peer while local readiness is held`) + reach.reach(`held peer keyed state`) + for (let index = 0; index < history.latePeers; index++) { + const late = renderHook(() => usePage(hookConfig(first)), { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }) + unmounts.push(() => late.unmount()) + checkWindowObservation( + { + rows: late.result.current.data, + collection: late.result.current.collection, + }, + { + rows: expected(serverRows, first), + previousCollection: collection, + retain: false, + }, + `fresh old-page lookup after move before local readiness`, + ) + checkWindowState( + late.result.current.state, + expected(serverRows, first), + `fresh old-page keyed view`, + ) + expect(late.result.current.collection).not.toBe( + peer.result.current.collection, + ) + } + reach.reach(`fresh old-page lookup`) + expect(ready, `browser sync started at commit`).toBeDefined() + await act(() => { + ready!() + }) + compare(browserRows, next, `local handoff`) + reach.reach(`local handoff`) + comparePeer(browserRows, `peer after handoff`) + reach.reach(`peer isolation`) + reach.reach(`local peer keyed state`) + await act(() => { + hook.rerender({ page: first, suspend: false }) + }) + compare(browserRows, first, `return to first page`) + reach.reach(`returned page`) + // Ordinary moves do not rewrite the server's immutable page records. + expect(JSON.parse(JSON.stringify(server.dehydrate()))).toEqual(transport) + }) +} + +// An explicit client switch must replace the binding even when both clients read +// exactly the same concrete source object and authored query identity. +async function checkClientSwitch( + history: History, + keyed: boolean, + suspense: boolean, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + let unmount: (() => void) | undefined + lifetime.defer(() => unmount?.()) + await lifetime.run(async () => { + const source = createCollection({ + getKey: (row: WindowRow) => row.id, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (let rank = 0; rank < history.count; rank++) + write({ type: `insert`, value: { id: String(rank), rank } }) + commit() + markReady() + }, + }, + }) + lifetime.defer(() => source.cleanup({ dependents: true })) + const first = new DbClient() + const second = new DbClient() + lifetime.defer(() => first.cleanup()) + lifetime.defer(() => second.cleanup()) + const usePage = suspense ? useLiveSuspenseQuery : useLiveQuery + const hook = renderHook( + ({ client, offset }) => + usePage({ + ...(keyed ? { queryKey: [`scope-switch`, offset] } : {}), + client, + query: (q) => + q + .from({ row: source }) + .orderBy(({ row }) => row.rank) + .limit(history.width) + .offset(offset) + .select(({ row }) => ({ id: row.id, rank: row.rank })), + }), + { initialProps: { client: first, offset: history.offset } }, + ) + unmount = () => hook.unmount() + for (const [client, offset] of [ + [second, history.offset], + [first, history.offset + history.jump], + ] as const) { + const previous = hook.result.current.collection + await act(() => { + hook.rerender({ client, offset }) + }) + checkWindowObservation( + { + rows: hook.result.current.data, + collection: hook.result.current.collection, + }, + { + rows: windowRows( + Array.from({ length: history.count }, (_, rank) => ({ + id: String(rank), + rank, + })), + { + offset: offset, + limit: history.width, + minimum: -1, + descending: false, + nestedLimit: history.count, + }, + false, + ), + previousCollection: previous, + retain: false, + }, + `client scope switch`, + ) + } + reach.reach(`client scope switch`) + }) +} + +describe(`client page identity`, () => { + for (const keyed of [false, true]) + for (const suspense of [false, true]) + for (const clientMode of [`provider`, `explicit`] as const) + for (const move of [`offset`, `limit`, `both`] as const) + for (const campaign of [`fixed`, `fresh`] as const) { + const property = `react.client-window` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`${keyed ? `keyed` : `derived`}/${suspense ? `suspense` : `ordinary`}/${clientMode}/${move}/${campaign}`, async () => { + const reach = createOracleReach( + `${property}/${keyed}/${suspense}/${clientMode}/${move}/${campaign}`, + reactStates, + { fixedHistories: examples.length }, + ) + let index = 0 + const fidelity = createOracleFailureFidelity() + const propertyUnderTest = fc.asyncProperty( + clientWindowArbitrary, + async (history) => { + reach.beginHistory({ fixed: index++ < examples.length }) + await fidelity.run(history, async () => { + await checkClientWindowHistory( + history, + keyed, + suspense, + clientMode, + move, + reach, + ) + await checkClientSwitch(history, keyed, suspense, reach) + }) + }, + ) + try { + await fidelity.assert(() => + fc.assert( + propertyUnderTest, + campaign === `fixed` + ? { + numRuns: oracleRuns(6) + examples.length, + seed: 2345, + examples, + } + : oraclePropertyOptions(6, property, { examples }), + ), + ) + } finally { + console.info( + `client window reach ${keyed}/${suspense}/${clientMode}/${move}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + reach.assertReached() + }) + } +}) +declareOracle({ + grammar: [clientWindowArbitrary], + check: [checkClientWindowHistory, checkClientSwitch], +}) diff --git a/packages/react-db/tests/conformance.test.tsx b/packages/react-db/tests/conformance.test.tsx index 70eb208559..c98fd21844 100644 --- a/packages/react-db/tests/conformance.test.tsx +++ b/packages/react-db/tests/conformance.test.tsx @@ -8,10 +8,12 @@ * All registered laws must pass; the driver has no whole-test waivers. */ import { act, renderHook } from '@testing-library/react' -import { useLayoutEffect } from 'react' +import { Suspense, useLayoutEffect } from 'react' import { BTreeIndex, BasicIndex, + DbClient, + Query, coalesce, count, createCollection, @@ -22,12 +24,14 @@ import { gt, sum, } from '@tanstack/db' -import { expect, it } from 'vitest' +import { afterAll, expect, it } from 'vitest' import { mockSyncCollectionOptions, mockSyncCollectionOptionsNoInitialState, } from '../../db/tests/utils' import { useLiveQuery } from '../src/useLiveQuery' +import { useLiveSuspenseQuery } from '../src/useLiveSuspenseQuery' +import { registerWindowHistoryOracle } from '../../db/tests/conformance/window-history-oracle' import { runSuite } from '../../db/tests/conformance/suite' import { createPublicationLog } from '../../db/tests/conformance/publication-log' import { makeLaggingSource } from '../../db/tests/conformance/lagging-source' @@ -170,38 +174,71 @@ function mountDisabled() { // labels do not represent separate production paths. type IdentityPath = `derived` | `dependency list` -const mountControllableWith = (path: IdentityPath) => +const mountControllableWith = ( + path: IdentityPath, + withClient: boolean | DbClient = false, + suspense = false, +) => function mountControllable

( build: (q: any, param: P) => any, initial: P, opts?: { pool?: PooledLiveQueries }, ): ControllableHandle

{ + const client = + withClient instanceof DbClient + ? withClient + : withClient + ? new DbClient() + : undefined const published = createPublicationLog() + const usePage = suspense ? useLiveSuspenseQuery : useLiveQuery + let suspended = false + function Fallback() { + suspended = true + return null + } const hook = renderHook( ({ param }: { param: P }) => { const query = (q: any) => build(q, param) + const input = client ? build(new Query(), param) : undefined // Both retained registrations use the config form for a pooled query. const result = opts?.pool !== undefined ? path === `derived` - ? useLiveQuery({ query, pool: opts.pool }) - : useLiveQuery({ query, pool: opts.pool }) - : path === `derived` - ? // The callback captures param, so its IR identity changes with it. - useLiveQuery(query) - : useLiveQuery(query) + ? usePage({ query, pool: opts.pool, client }) + : usePage({ query, pool: opts.pool, client }) + : client + ? usePage( + input instanceof Query + ? { query: input, client } + : { ...input, client }, + ) + : path === `derived` + ? // The callback captures param, so its IR identity changes with it. + usePage(query) + : usePage(query) // A layout effect records each committed value. useLayoutEffect(() => { + suspended = false published.record(result.data, result.state) }) return result }, - { initialProps: { param: initial } }, + { + initialProps: { param: initial }, + wrapper: ({ children }) => + suspense ? ( + }>{children} + ) : ( + children + ), + }, ) const guard = rejectReadsAfterRenderError(hook) - const handle = makeHandle(hook) + const handle = makeHandle(hook, suspense) return { ...handle, + isSuspended: () => suspended, publications: published.values, statePublications: published.stateValues, async setParam(param: P, observe?: () => void) { @@ -213,11 +250,26 @@ const mountControllableWith = (path: IdentityPath) => } } -function makeHandle(hook: RenderHookResult) { +function makeHandle(hook: RenderHookResult, suspense = false) { return { collection: () => hook.result.current?.collection ?? null, current(): ConformanceResult { const r: any = hook.result.current + // Suspense exposes readiness by returning successfully, not via flags. + // No result is fabricated while it shows a fallback; the shared pending + // receiver observes that fallback through isSuspended instead. + if (suspense) { + if (!r) throw new Error(`Suspense has no committed result`) + return { + ...r, + status: `ready`, + isReady: true, + isLoadingError: false, + isCrashed: false, + isSyncError: false, + isEnabled: true, + } + } return expectResultSurface({ data: r?.data, state: r?.state, @@ -251,32 +303,58 @@ function makeHandle(hook: RenderHookResult) { // The config-object form with a queryKey chosen per step; an undefined key // leaves the hook to derive the identity. -const mountKeyedControllableWith = (path: IdentityPath) => +const mountKeyedControllableWith = ( + path: IdentityPath, + withClient: boolean | DbClient = false, + suspense = false, +) => function mountKeyedControllable

( build: (q: any, param: P) => any, key: (param: P) => ReadonlyArray | undefined, initial: P, ): ControllableHandle

{ + const client = + withClient instanceof DbClient + ? withClient + : withClient + ? new DbClient() + : undefined const published = createPublicationLog() + const usePage = suspense ? useLiveSuspenseQuery : useLiveQuery + let suspended = false + function Fallback() { + suspended = true + return null + } const hook = renderHook( ({ param }: { param: P }) => { const config = { query: (q: any) => build(q, param), queryKey: key(param), + client, } as any - const result = - path === `derived` ? useLiveQuery(config) : useLiveQuery(config) + const result = path === `derived` ? usePage(config) : usePage(config) useLayoutEffect(() => { + suspended = false published.record(result.data, result.state) }) return result }, - { initialProps: { param: initial } }, + { + initialProps: { param: initial }, + wrapper: ({ children }) => + suspense ? ( + }>{children} + ) : ( + children + ), + }, ) const guard = rejectReadsAfterRenderError(hook) - const handle = makeHandle(hook) + const handle = makeHandle(hook, suspense) return { ...handle, + isSuspended: () => suspended, publications: published.values, statePublications: published.stateValues, async setParam(param: P, observe?: () => void) { @@ -321,6 +399,26 @@ const reactDriver: LiveQueryDriver = { features: { serverSnapshot: true, suspense: true, pooledEqFilters: true }, } +// DbClient and Suspense are receiving modes of the shared page law. Other +// adapters register client-backed drivers against the same model and histories. +for (const client of [false, true]) + for (const suspense of [false, true]) { + if (!client && !suspense) continue // registered by runSuite below + const scope = client ? new DbClient() : false + if (scope) afterAll(() => scope.cleanup()) + registerWindowHistoryOracle({ + ...reactDriver, + name: `react${suspense ? ` Suspense` : ``}${client ? ` (DbClient)` : ``}`, + features: { ...reactDriver.features, suspendsUntilReady: suspense }, + mountControllable: mountControllableWith(`derived`, scope, suspense), + mountKeyedControllable: mountKeyedControllableWith( + `derived`, + scope, + suspense, + ), + }) + } + runSuite(reactDriver) runSuite({ ...reactDriver, diff --git a/packages/react-db/tests/source-id-reuse-oracle.test.tsx b/packages/react-db/tests/source-id-reuse-oracle.test.tsx index 8f684bb64b..e46540e88c 100644 --- a/packages/react-db/tests/source-id-reuse-oracle.test.tsx +++ b/packages/react-db/tests/source-id-reuse-oracle.test.tsx @@ -13,8 +13,8 @@ * model; switching clients selects that client's remembered bindings. * A render that starts a live query claims its source before a descendant * suspends, because the live query may still own that source until cleanup. - * Separate hooks can use same-ID objects: their client-scoped Suspense cache - * entries must remain distinct, including before the first hook commits. + * Separate hooks can use same-ID objects. Their client-scoped Suspense preload + * resources must distinguish source objects, including before the first commit. * A rejected render must release any sync-start deferral it acquired for a * shared client descriptor, so a later direct reader can start that Collection. * The release history grammar crosses every rejection cause (a source ID diff --git a/packages/react-db/tests/suspense-indexing-config-oracle.test.tsx b/packages/react-db/tests/suspense-indexing-config-oracle.test.tsx index b18428f106..42baa9c54b 100644 --- a/packages/react-db/tests/suspense-indexing-config-oracle.test.tsx +++ b/packages/react-db/tests/suspense-indexing-config-oracle.test.tsx @@ -3,8 +3,8 @@ * settings, even when a peer already reads the same source and query. The * shared handoff owner's explicit-output law applies to each binding; cache * reuse cannot replace one binding's declaration with its peer's declaration. - * Compatible declarations under the same query identity retain existing - * Collection sharing. Mounted bindings retain their first options until query identity changes, as + * Equal declarations still produce independent output Collections + * (maintainer decision, 2026-11-03). Mounted bindings retain their first options until query identity changes, as * required by the approved first-options rule. * * The shared grammar exhausts both configurations and three identity relations: @@ -40,11 +40,10 @@ import { afterAll, expect, test, vi } from 'vitest' import { useLiveSuspenseQuery } from '../src/useLiveSuspenseQuery' import { DbProvider } from '../src/DbProvider' import { + expectIndependentIndexingOutputs, expectIndexingAdmission, expectIndexingConfiguration, - expectIndexingReuse, expectedIndexing, - expectedIndexingReuse, indexConfigurationPairs, indexConfigurations, indexOptionAbsencePairs, @@ -60,11 +59,11 @@ import type { Row } from '../../db/tests/conformance/contract' declareOracle({ grammar: [indexConfigurationPairs, indexOptionAbsencePairs], - model: [expectedIndexing, expectedIndexingReuse], + model: [expectedIndexing], check: [ expectIndexingAdmission, expectIndexingConfiguration, - expectIndexingReuse, + expectIndependentIndexingOutputs, ], }) @@ -128,8 +127,8 @@ const pendingReaches = Object.fromEntries( scope, createOracleReach(`Suspense indexing pending/${scope}`, [ `both-suspended`, - `shared-output`, - `separate-outputs`, + `equal-options`, + `different-options`, `mode-change`, `constructor-change`, ]), @@ -304,11 +303,12 @@ test.each(histories)( const firstOutput = readyFirst ?? (await readFirst?.()) const secondOutput = await readSecond?.() if (firstOutput) expectIndexingConfiguration(firstOutput, first) - expectIndexingReuse(firstOutput, secondOutput, first, second, identity) + expectIndependentIndexingOutputs(firstOutput, secondOutput, first, second) pendingReach?.reach( - expectedIndexingReuse(first, second, identity) - ? `shared-output` - : `separate-outputs`, + first.autoIndex === second.autoIndex && + first.defaultIndexType === second.defaultIndexType + ? `equal-options` + : `different-options`, ) reach.reach(`retained-first`) if (firstOutput) { diff --git a/packages/react-db/tests/suspense-lifecycle-oracle.test.tsx b/packages/react-db/tests/suspense-lifecycle-oracle.test.tsx new file mode 100644 index 0000000000..609b39a692 --- /dev/null +++ b/packages/react-db/tests/suspense-lifecycle-oracle.test.tsx @@ -0,0 +1,1676 @@ +/** + * A Suspense binding owes the current initial attempt's fallback, rows or error. + * Its first source failure reaches the ErrorBoundary as an Error (original Error + * identity, otherwise wrapped cause); a failure after readiness keeps the data. + * Cleanup/restart and a replacement client request start a new attempt: an old + * rejection cannot poison it. A settled initial load must make retry progress, + * even when React discarded the first suspended render's refs. Successful query + * inputs own distinct Collections. Authority: error-handling.md, “Using Suspense + * and Error Boundaries”, hook JSDoc, shared conformance contract, and the existing + * useLiveSuspenseQuery-recovery.test.tsx replacement/cleanup promises. + * Copied ready rows remain provisional until the owned query's initial load + * succeeds. The observer hydration oracle's failure-ending-seed law also applies + * here: a failed first load ends that view, while later source problems are health. + * + * The model retains the newest attempt, its public view and initial readiness. It has no + * cache, resolver token, observer or subscription state. Real React boundaries + * receive controlled source-demand and client-request histories. Query-only + * preload failures use the public Collection preload seam; no external provider, + * native hydration, arbitrary React scheduler or nonfinite window claim follows. + * Error messages remain with the supplied-query boundary contract examples; a + * source problem message is not the derived live-query problem message. + */ +import { Component, Suspense, useLayoutEffect } from 'react' +import { act, render } from '@testing-library/react' +import { + BTreeIndex, + DbClient, + createCollection, + createLiveQueryCollection, + getStableValueHash, +} from '@tanstack/db' +import fc from 'fast-check' +import { describe, expect, it, vi } from 'vitest' +import { DbProvider } from '../src/DbProvider' +import { useLiveSuspenseQuery } from '../src/useLiveSuspenseQuery' +import { + checkWindowObservation, + checkWindowState, + windowRows, +} from '../../db/tests/conformance/window-history-oracle' +import { ScenarioLifetime } from '../../db/tests/conformance/scenario-lifetime' +import { + expectKeyedRows, + expectUnorderedRows, +} from '../../db/tests/conformance/result-laws' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { + atOracleCheckpoint, + createOracleFailureFidelity, +} from '../../db/tests/oracle-failure-fidelity' +import { + declareOracle, + oraclePropertyOptions, + oracleReplayTest, + oracleRuns, +} from '../../db/tests/oracle-config' +import type { WindowRow } from '../../db/tests/conformance/window-history-oracle' +import type { ReactNode } from 'react' +import type { HealthNode, SyncConfig } from '@tanstack/db' + +type Row = { id: string; version: number } +type CauseKind = `Error` | `record` | `undefined` +type Scope = `none` | `provider` | `explicit` +type View = { + phase: `pending` | `ready` | `error` + rows: Array + cause?: unknown + wrapCause: boolean + healthError: boolean +} +// `attempt` is a model-only initial-view obligation. It combines a boundary +// reset with the source sync run or client request it now waits for. It does +// not represent an acquisition attempt, observer, or internal resolver token. +// A copied result can be visible before this query's initial readiness. This +// splits the public view from the initial-query gate (hydration oracle laws 1–2). +// The next first-load rejection distinguishes that state from owned readiness. +type Model = View & { attempt: number; queryReady: boolean } +type Event = + | { type: `start`; attempt: number } + | { type: `ready`; attempt: number; rows: Array } + | { type: `seed`; attempt: number; rows: Array } + | { + type: `failure` + attempt: number + cause: unknown + origin: `source` | `preload` | `client` + } + +// A new attempt replaces the visible obligation. A stale event has no authority. +// Source fail after readiness is health, not a new initial-load gate (glossary). +function advance(model: Model, event: Event): Model { + if (event.type === `start`) + return { + attempt: event.attempt, + phase: `pending`, + rows: [], + wrapCause: false, + healthError: false, + queryReady: false, + } + if (event.attempt !== model.attempt) return model + if (event.type === `ready` || event.type === `seed`) + return { + ...model, + phase: `ready`, + rows: event.rows, + cause: undefined, + queryReady: event.type === `ready`, + } + if (model.queryReady && event.origin === `source`) + return { ...model, cause: event.cause, healthError: true } + return { + ...model, + phase: `error`, + rows: [], + cause: event.cause, + wrapCause: event.origin !== `client`, + } +} +const initial = (): Model => ({ + attempt: 0, + phase: `pending`, + rows: [], + wrapCause: false, + healthError: false, + queryReady: false, +}) +function makeCause(kind: CauseKind, version: number): unknown { + return kind === `Error` + ? new Error(`load ${version} failed`) + : kind === `record` + ? { code: `offline`, version } + : undefined +} +function rowsFor(count: number, version: number): Array { + return Array.from({ length: count }, (_, id) => ({ id: String(id), version })) +} + +type Result = { + data: unknown + state: ReadonlyMap + collection: object + health: HealthNode | undefined +} +type Recorded = { result?: Result; error?: unknown } +class Boundary extends Component< + { children: ReactNode; record: Recorded }, + { failed: boolean } +> { + state = { failed: false } + static getDerivedStateFromError() { + return { failed: true } + } + componentDidCatch(error: unknown) { + this.props.record.error = error + } + render() { + return this.state.failed ? ( + + ) : ( + this.props.children + ) + } +} +function frame( + child: ReactNode, + record: Recorded, + key: number, + client?: DbClient, +) { + const content = ( + + }>{child} + + ) + return client ? {content} : content +} + +// Compare the actual committed host surface; a previously returned result cannot +// disguise a current fallback/error. Empty results remain distinguishable from it. +function compare( + host: HTMLElement, + record: Recorded, + model: Model, + cut: string, +) { + atOracleCheckpoint(`current Suspense attempt`, cut, () => { + expect(host.querySelector(`output`)?.getAttribute(`data-phase`)).toBe( + model.phase, + ) + if (model.phase === `ready`) { + expect(record.result, `a successful binding committed`).toBeDefined() + expectUnorderedRows(record.result!.data, model.rows) + expectKeyedRows(record.result!.state, model.rows) + if (model.healthError) { + const problem = record.result!.health?.sources[0]?.error + expect(problem, `a stopped source has a health problem`).toBeTruthy() + expect(problem?.cause).toBe(model.cause) + } + } + if (model.phase === `error`) { + if (model.wrapCause && !(model.cause instanceof Error)) { + expect(record.error).toBeInstanceOf(Error) + expect((record.error as Error).cause).toBe(model.cause) + } else expect(record.error).toBe(model.cause) + } + }) +} +async function settle() { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)) + }) +} + +// Each history fails, crashes, or abandons its initial load, then cleans up +// before a new sync run. Only that new run may become ready. Bounded rows and +// pending rerenders vary the public result and discarded-render retry path. +const retryArbitrary = fc.record({ + count: fc.integer({ min: 0, max: 3 }), + version: fc.integer({ min: 0, max: 3 }), + rerenders: fc.integer({ min: 0, max: 2 }), + cause: fc.constantFrom(`Error`, `record`, `undefined`), + first: fc.constantFrom(`fail` as const, `crash` as const, `abandon` as const), + remount: fc.boolean(), +}) +type HistoryOf = T extends fc.Arbitrary ? History : never +type RetryHistory = HistoryOf +const retryExamples: Array<[RetryHistory]> = [ + [ + { + count: 0, + version: 0, + rerenders: 2, + cause: `Error`, + first: `fail`, + remount: false, + }, + ], + [ + { + count: 2, + version: 1, + rerenders: 0, + cause: `undefined`, + first: `crash`, + remount: true, + }, + ], + [ + { + count: 3, + version: 2, + rerenders: 1, + cause: `record`, + first: `abandon`, + remount: false, + }, + ], +] +const retryStates = [ + `fail`, + `crash`, + `abandon`, + `root remount`, + `boundary reset`, + `initial fallback`, + `first failure`, + `pending retirement`, + `new attempt pending`, + `retry progress`, + `independent peer`, + `post-ready failure`, +] as const + +async function checkRetry( + history: RetryHistory, + scope: Scope, + keyed: boolean, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + const record: Recorded = {} + const peerRecord: Recorded = {} + let mounted: ReturnType | undefined + let peer: ReturnType | undefined + lifetime.defer(() => { + mounted?.unmount() + peer?.unmount() + }) + const client = scope === `none` ? undefined : new DbClient() + if (client) lifetime.defer(() => client.cleanup()) + const diagnostics = vi.spyOn(console, `error`).mockImplementation(() => {}) + lifetime.defer(() => { + diagnostics.mockRestore() + }) + await lifetime.run(async () => { + const cause = makeCause(history.cause, history.version) + let run = 0 + let current!: Parameters[`sync`]>[0] + const demands: Array<{ + run: number + resolve: () => void + reject: (error: unknown) => void + }> = [] + const source = createCollection({ + getKey: (row) => row.id, + syncMode: `on-demand`, + sync: { + sync: (params) => { + const thisRun = ++run + current = params + if (thisRun === 1 && history.first === `crash`) throw cause + return { + loadSubset: () => + new Promise((resolve, reject) => { + demands.push({ run: thisRun, resolve, reject }) + }), + } + }, + }, + }) + lifetime.defer(() => source.cleanup({ dependents: true })) + function Consumer({ target }: { target: Recorded }) { + const result = useLiveSuspenseQuery({ + query: (q) => + q + .from({ row: source }) + .select(({ row }) => ({ id: row.id, version: row.version })), + ...(scope === `explicit` ? { client } : {}), + ...(keyed ? { queryKey: [`retry-source`] } : {}), + gcTime: 0, + }) + useLayoutEffect(() => { + target.result = result + }) + return + } + const tree = (key: number) => + frame( + , + record, + key, + scope === `provider` ? client : undefined, + ) + let model = initial() + mounted = render(tree(0)) + await settle() + if (history.first === `crash`) { + model = advance(model, { + type: `failure`, + attempt: 0, + cause, + origin: `source`, + }) + compare(mounted.container, record, model, `initial crash`) + reach.reach(`first failure`) + reach.reach(`crash`) + } else { + compare(mounted.container, record, model, `initial pending`) + expect(demands.length, `real query demand started`).toBeGreaterThan(0) + reach.reach(`initial fallback`) + for (let n = 0; n < history.rerenders; n++) { + mounted.rerender(tree(0)) + await settle() + compare(mounted.container, record, model, `pending rerender`) + } + if (history.first === `fail`) { + await act(() => current.fail(cause)) + await settle() + model = advance(model, { + type: `failure`, + attempt: 0, + cause, + origin: `source`, + }) + compare(mounted.container, record, model, `first load failure`) + reach.reach(`first failure`) + reach.reach(`fail`) + } else { + reach.reach(`pending retirement`) + reach.reach(`abandon`) + } + } + // Retire the failed/pending source run before resetting its boundary. Its + // unresolved work may reject later; that event has no authority over run2. + await act(() => source.cleanup({ dependents: true })) + if (history.remount) { + mounted.unmount() + mounted = render(tree(1)) + } else mounted.rerender(tree(1)) + await settle() + model = advance(model, { type: `start`, attempt: 1 }) + compare(mounted.container, record, model, `new attempt pending`) + expect(run, `cleanup was followed by a new source sync run`).toBe(2) + reach.reach(`new attempt pending`) + reach.reach(history.remount ? `root remount` : `boundary reset`) + await act(() => { + for (const demand of demands.filter((d) => d.run === 1)) + demand.reject(cause) + }) + await settle() + model = advance(model, { + type: `failure`, + attempt: 0, + cause, + origin: `source`, + }) + compare( + mounted.container, + record, + model, + `retired source rejects during new attempt`, + ) + const rows = rowsFor(history.count, history.version + 1) + await act(() => { + current.begin() + for (const value of rows) current.write({ type: `insert`, value }) + current.commit() + current.markReady() + for (const demand of demands.filter((d) => d.run === 2)) demand.resolve() + }) + await settle() + model = advance(model, { + type: `ready`, + attempt: 1, + rows: rowsFor(history.count, history.version + 1), + }) + compare( + mounted.container, + record, + model, + `settled load makes retry progress`, + ) + reach.reach(`retry progress`) + peer = render( + frame( + , + peerRecord, + 0, + scope === `provider` ? client : undefined, + ), + ) + await act(() => { + for (const demand of demands.filter((d) => d.run === 2)) demand.resolve() + }) + await settle() + compare(peer.container, peerRecord, model, `successful peer`) + atOracleCheckpoint(`exclusive successful outputs`, `peer ownership`, () => { + expect(peerRecord.result!.collection).not.toBe(record.result!.collection) + }) + reach.reach(`independent peer`) + const stopped = makeCause(history.cause, history.version + 2) + await act(() => current.fail(stopped)) + await settle() + model = advance(model, { + type: `failure`, + attempt: 1, + cause: stopped, + origin: `source`, + }) + compare(mounted.container, record, model, `source stops after readiness`) + compare( + peer.container, + peerRecord, + model, + `peer keeps data after source stops`, + ) + reach.reach(`post-ready failure`) + }) +} + +// These request-only histories keep query preload pending. A client snapshot +// supplies the replacement rows; source writes cannot accidentally rescue it. +const replacementArbitrary = fc.record({ + count: fc.integer({ min: 0, max: 3 }), + version: fc.integer({ min: 0, max: 3 }), + cause: fc.constantFrom(`Error`, `record`, `undefined`), + schedule: fc.constantFrom( + `failure-first` as const, + `replace-first` as const, + `cleanup-first` as const, + `client-first` as const, + ), +}) +type ReplacementHistory = HistoryOf +const replacementExamples: Array<[ReplacementHistory]> = [ + [{ count: 0, version: 0, cause: `undefined`, schedule: `failure-first` }], + [{ count: 1, version: 1, cause: `Error`, schedule: `replace-first` }], + [{ count: 2, version: 2, cause: `record`, schedule: `cleanup-first` }], + [{ count: 3, version: 3, cause: `undefined`, schedule: `client-first` }], +] +const replacementStates = [ + `failure-first`, + `replace-first`, + `cleanup-first`, + `client-first`, + `first request pending`, + `first request failure`, + `replacement pending`, + `late rejection ignored`, + `replacement ready`, +] as const +async function checkReplacement( + history: ReplacementHistory, + reach: ReturnType< + typeof createOracleReach<(typeof replacementStates)[number]> + >, +) { + const lifetime = new ScenarioLifetime() + let mounted: ReturnType | undefined + lifetime.defer(() => mounted?.unmount()) + const clients = [new DbClient(), new DbClient()] + lifetime.defer(async () => { + for (const client of clients) await client.cleanup() + }) + const diagnostics = vi.spyOn(console, `error`).mockImplementation(() => {}) + lifetime.defer(() => { + diagnostics.mockRestore() + }) + await lifetime.run(async () => { + const source = createCollection({ + getKey: (row) => row.id, + sync: { sync: () => ({}) }, + }) + const query = createLiveQueryCollection((q) => + q + .from({ row: source }) + .select(({ row }) => ({ id: row.id, version: row.version })), + ) + const preload = vi + .spyOn(query, `preload`) + .mockReturnValue(new Promise(() => {})) + lifetime.defer(() => { + preload.mockRestore() + }) + lifetime.defer(() => query.cleanup()) + lifetime.defer(() => source.cleanup({ dependents: true })) + const hash = getStableValueHash([`collection`, query.id], `queryKey`) + const cause = makeCause(history.cause, history.version) + let rejectOld!: (error: unknown) => void + const old = new Promise<{ rows: Array<{ key: string; value: Row }> }>( + (_, reject) => { + rejectOld = reject + }, + ) + void clients[0]!._registerLiveQuery(hash, old).catch(() => {}) + const record: Recorded = {} + function Consumer() { + const result = useLiveSuspenseQuery(query) + useLayoutEffect(() => { + record.result = result + }) + return + } + let model = initial() + mounted = render(frame(, record, 0, clients[0])) + compare(mounted.container, record, model, `first client request`) + reach.reach(`first request pending`) + if (history.schedule === `failure-first`) { + await act(async () => { + rejectOld(cause) + await old.catch(() => {}) + }) + await settle() + model = advance(model, { + type: `failure`, + attempt: 0, + cause, + origin: `client`, + }) + compare( + mounted.container, + record, + model, + `client failure before replacement`, + ) + reach.reach(`first request failure`) + } + if (history.schedule === `cleanup-first`) { + mounted.unmount() + await query.cleanup() + query.startSyncImmediate() + } + const client = clients[history.schedule === `client-first` ? 1 : 0]! + let finish!: (snapshot: { + rows: Array<{ key: string; value: Row }> + }) => void + const newer = new Promise<{ rows: Array<{ key: string; value: Row }> }>( + (resolve) => { + finish = resolve + }, + ) + const register = () => { + void client._registerLiveQuery(hash, newer).catch(() => {}) + } + const reject = async () => { + await act(async () => { + rejectOld(cause) + await old.catch(() => {}) + }) + } + // Pending records deduplicate. Replacement on the same client is legal + // only after its old record fails. This reaction registers the next record + // before the old preload rejection reaches the suspended wrapper. + if (history.schedule === `replace-first`) { + void old.catch(register) + await reject() + } else if (history.schedule === `cleanup-first`) { + await reject() + register() + } else register() + expect(client._getLiveQuery(hash)?.status, `new request admitted`).toBe( + `pending`, + ) + const app = frame(, record, 1, client) + if (history.schedule === `cleanup-first`) mounted = render(app) + else mounted.rerender(app) + await settle() + model = advance(model, { type: `start`, attempt: 1 }) + compare(mounted.container, record, model, `replacement request is pending`) + reach.reach(`replacement pending`) + reach.reach(history.schedule) + if (history.schedule === `client-first`) { + await reject() + await settle() + } + if ( + history.schedule === `replace-first` || + history.schedule === `client-first` + ) { + model = advance(model, { + type: `failure`, + attempt: 0, + cause, + origin: `client`, + }) + compare(mounted.container, record, model, `obsolete client rejection`) + reach.reach(`late rejection ignored`) + } + const rows = rowsFor(history.count, history.version + 1) + await act(() => + finish({ rows: rows.map((value) => ({ key: value.id, value })) }), + ) + await settle() + model = advance(model, { + type: `ready`, + attempt: 1, + rows: rowsFor(history.count, history.version + 1), + }) + compare(mounted.container, record, model, `replacement request settled`) + reach.reach(`replacement ready`) + }) +} + +// Query-only preload failure has no source status transition. Keep this lane +// separate from source failure so source status cannot hide a stale wrapper error. +const preloadArbitrary = fc.record({ + count: fc.integer({ min: 0, max: 3 }), + version: fc.integer({ min: 0, max: 3 }), + cause: fc.constantFrom(`Error`, `record`, `undefined`), + rejectAfterCleanup: fc.boolean(), + rerenders: fc.integer({ min: 0, max: 2 }), +}) +type PreloadHistory = HistoryOf +const preloadExamples: Array<[PreloadHistory]> = [ + [ + { + count: 0, + version: 0, + cause: `undefined`, + rejectAfterCleanup: false, + rerenders: 2, + }, + ], + [ + { + count: 2, + version: 1, + cause: `record`, + rejectAfterCleanup: true, + rerenders: 1, + }, + ], +] +const preloadStates = [ + `pending`, + `failure before cleanup`, + `rejection after cleanup`, + `restart pending`, + `restart ready`, +] as const +async function checkPreload( + history: PreloadHistory, + scope: Exclude, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + let mounted: ReturnType | undefined + lifetime.defer(() => mounted?.unmount()) + const client = scope === `none` ? undefined : new DbClient() + if (client) lifetime.defer(() => client.cleanup()) + const diagnostics = vi.spyOn(console, `error`).mockImplementation(() => {}) + lifetime.defer(() => { + diagnostics.mockRestore() + }) + await lifetime.run(async () => { + let sync!: Parameters[`sync`]>[0] + const source = createCollection({ + getKey: (row) => row.id, + sync: { + sync: (params) => { + sync = params + return {} + }, + }, + }) + const query = createLiveQueryCollection((q) => + q + .from({ row: source }) + .select(({ row }) => ({ id: row.id, version: row.version })), + ) + lifetime.defer(() => query.cleanup()) + lifetime.defer(() => source.cleanup({ dependents: true })) + // The preload seam holds query work; start its source independently. + source.startSyncImmediate() + let rejectOld!: (error: unknown) => void + const old = new Promise((_, reject) => { + rejectOld = reject + }) + const preload = vi.spyOn(query, `preload`).mockReturnValue(old) + lifetime.defer(() => { + preload.mockRestore() + }) + const record: Recorded = {} + function Consumer() { + const result = useLiveSuspenseQuery(query) + useLayoutEffect(() => { + record.result = result + }) + return + } + const tree = (key: number) => + frame( + , + record, + key, + scope === `provider` ? client : undefined, + ) + let model = initial() + mounted = render(tree(0)) + compare(mounted.container, record, model, `query preload pending`) + expect(preload).toHaveBeenCalled() + reach.reach(`pending`) + for (let n = 0; n < history.rerenders; n++) { + mounted.rerender(tree(0)) + await settle() + compare(mounted.container, record, model, `preload pending rerender`) + } + const cause = makeCause(history.cause, history.version) + if (!history.rejectAfterCleanup) { + await act(() => rejectOld(cause)) + await settle() + model = advance(model, { + type: `failure`, + attempt: 0, + cause, + origin: `preload`, + }) + compare(mounted.container, record, model, `query-only preload failure`) + reach.reach(`failure before cleanup`) + } + mounted.unmount() + await query.cleanup() + query.startSyncImmediate() + let resolveNew!: () => void + preload.mockReturnValue( + new Promise((resolve) => { + resolveNew = resolve + }), + ) + mounted = render(tree(1)) + model = advance(model, { type: `start`, attempt: 1 }) + compare(mounted.container, record, model, `query preload restart pending`) + reach.reach(`restart pending`) + if (history.rejectAfterCleanup) { + await act(() => rejectOld(cause)) + await settle() + // The old root is gone. Force a new render while its replacement is + // still pending; waiting for readiness would clear and hide stale errors. + mounted.rerender(tree(1)) + await settle() + model = advance(model, { + type: `failure`, + attempt: 0, + cause, + origin: `preload`, + }) + compare(mounted.container, record, model, `retired query preload rejects`) + reach.reach(`rejection after cleanup`) + } + const rows = rowsFor(history.count, history.version + 1) + await act(() => { + sync.begin() + for (const value of rows) sync.write({ type: `insert`, value }) + sync.commit() + sync.markReady() + resolveNew() + }) + await settle() + model = advance(model, { + type: `ready`, + attempt: 1, + rows: rowsFor(history.count, history.version + 1), + }) + compare(mounted.container, record, model, `query preload restart ready`) + reach.reach(`restart ready`) + }) +} + +declareOracle({ + model: [advance], + grammar: [retryArbitrary, replacementArbitrary, preloadArbitrary], + check: [compare, checkRetry, checkReplacement, checkPreload], +}) + +describe(`Suspense lifecycle histories`, () => { + for (const scope of [`none`, `provider`, `explicit`] as const) + for (const keyed of [false, true]) + for (const campaign of [`fixed`, `fresh`] as const) { + const property = `react.suspense-retry-history` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`${scope}/${keyed ? `keyed` : `derived`} retry (${campaign})`, async () => { + const reach = createOracleReach( + `${property}/${scope}/${keyed}/${campaign}`, + retryStates, + { fixedHistories: retryExamples.length }, + ) + let index = 0 + const fidelity = createOracleFailureFidelity() + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(retryArbitrary, async (history) => { + reach.beginHistory({ fixed: index++ < retryExamples.length }) + await fidelity.run(history, () => + checkRetry(history, scope, keyed, reach), + ) + }), + campaign === `fixed` + ? { + numRuns: oracleRuns(24) + retryExamples.length, + seed: 2345, + examples: retryExamples, + } + : oraclePropertyOptions(24, property, { + examples: retryExamples, + }), + ), + ) + reach.assertReached() + } finally { + console.info( + `retry lifecycle reach ${scope}/${keyed}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }, 60_000) + } + for (const campaign of [`fixed`, `fresh`] as const) { + const property = `react.suspense-replacement-history` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`client request replacement (${campaign})`, async () => { + const reach = createOracleReach( + `${property}/${campaign}`, + replacementStates, + { fixedHistories: replacementExamples.length }, + ) + let index = 0 + const fidelity = createOracleFailureFidelity() + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(replacementArbitrary, async (history) => { + reach.beginHistory({ + fixed: index++ < replacementExamples.length, + }) + await fidelity.run(history, () => + checkReplacement(history, reach), + ) + }), + campaign === `fixed` + ? { + numRuns: oracleRuns(32) + replacementExamples.length, + seed: 2345, + examples: replacementExamples, + } + : oraclePropertyOptions(32, property, { + examples: replacementExamples, + }), + ), + ) + reach.assertReached() + } finally { + console.info( + `replacement lifecycle reach ${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }, 60_000) + } + for (const scope of [`none`, `provider`] as const) + for (const campaign of [`fixed`, `fresh`] as const) { + const property = `react.suspense-preload-restart-history` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`${scope} preload restart (${campaign})`, async () => { + const reach = createOracleReach( + `${property}/${scope}/${campaign}`, + preloadStates, + { fixedHistories: preloadExamples.length }, + ) + const fidelity = createOracleFailureFidelity() + let index = 0 + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(preloadArbitrary, async (history) => { + reach.beginHistory({ fixed: index++ < preloadExamples.length }) + await fidelity.run(history, () => + checkPreload(history, scope, reach), + ) + }), + campaign === `fixed` + ? { + numRuns: oracleRuns(24) + preloadExamples.length, + seed: 2345, + examples: preloadExamples, + } + : oraclePropertyOptions(24, property, { + examples: preloadExamples, + }), + ), + ) + reach.assertReached() + } finally { + console.info( + `preload lifecycle reach ${scope}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }, 60_000) + } +}) + +/** Initial sibling histories cross boundary placement, query identity, and + * readiness order. The existing current-attempt model predicts each ready view; + * the shared window judgment checks rows and exclusive/retained Collections. + * A ready sibling in a shared boundary has not committed while its peer waits. + * Settling both obligations must permit the boundary to commit both outputs. + * Source Collection readiness can lag settled on-demand subsets. A private + * output keeps its completed seed's rows through a peer move and a later render + * until its own startup settles; source readiness cannot hide that handoff. + * A copied seed does not establish the private query's initial readiness. + * Its first-load rejection ends the provisional seed and reaches the boundary, + * as in the observer hydration oracle; failure after owned readiness is health. + * Unlike the old peer lane, both hooks are present before any successful commit. + * Source health reports and recovery must notify a seeded successful output + * while its private startup is held, without an unrelated host render. Both + * sides of a peer move receive that law (glossary health tree; hook health API). + * Request errors belong to the query owning the demand (glossary). A peer page + * rejection must not change a seeded output's own root health or source health. + */ +const siblingsArbitrary = fc.record({ + sharedBoundary: fc.boolean(), + holdCandidate: fc.boolean(), + peerFailure: fc.boolean(), + sourceReady: fc.boolean(), + sameQuery: fc.boolean(), + first: fc.integer({ min: 0, max: 1 }), + simultaneous: fc.boolean(), + width: fc.integer({ min: 1, max: 3 }), + offset: fc.integer({ min: 1, max: 4 }), + startup: fc.constantFrom(`fulfill` as const, `reject` as const), + rejectBeforeMove: fc.boolean(), + cause: fc.constantFrom(`Error`, `record`, `undefined`), +}) +type SiblingsHistory = HistoryOf +// Each generated family includes the reported shapes plus its unconstrained +// history. Structural reach does not depend on random selection frequency. +function siblingHistories(history: SiblingsHistory): Array { + return [ + history, + { + ...history, + holdCandidate: false, + peerFailure: false, + sharedBoundary: true, + sameQuery: true, + first: 0, + sourceReady: false, + startup: `fulfill`, + }, + { + ...history, + holdCandidate: false, + peerFailure: false, + sharedBoundary: true, + sameQuery: true, + first: 0, + sourceReady: false, + startup: `reject`, + rejectBeforeMove: true, + }, + { + ...history, + holdCandidate: false, + peerFailure: false, + sharedBoundary: true, + sameQuery: true, + first: 0, + sourceReady: false, + startup: `reject`, + rejectBeforeMove: false, + }, + { + ...history, + holdCandidate: false, + peerFailure: false, + sharedBoundary: true, + sameQuery: false, + simultaneous: false, + }, + { + ...history, + holdCandidate: false, + peerFailure: false, + sharedBoundary: true, + sameQuery: true, + first: 0, + sourceReady: true, + startup: `fulfill`, + }, + { + ...history, + holdCandidate: false, + peerFailure: false, + sharedBoundary: false, + sameQuery: true, + first: 1, + sourceReady: true, + }, + { + ...history, + holdCandidate: false, + peerFailure: true, + sharedBoundary: true, + sameQuery: true, + first: 0, + sourceReady: false, + startup: `fulfill`, + }, + ...[true, false].map((sharedBoundary) => ({ + ...history, + holdCandidate: true, + peerFailure: false, + sharedBoundary, + sameQuery: true, + first: 0, + sourceReady: false, + startup: `fulfill` as const, + })), + ] +} +function checkSiblingOwnership(left: object, right: object, cut: string) { + atOracleCheckpoint(`independent successful outputs`, cut, () => { + expect(left, `every successful sibling owns its output`).not.toBe(right) + }) +} +// A successful view must expose its source's current failure reason and its +// removal. This judges input reports, independently of observer/cache mechanics. +function checkSiblingHealth( + health: HealthNode | undefined, + cause: unknown, + cut: string, +) { + atOracleCheckpoint(`seeded source health notification`, cut, () => { + const problem = health?.sources[0]?.failureReason + if (cause === undefined) expect(problem).toBeNull() + else { + expect(problem, `source report reaches the committed hook`).toBeTruthy() + expect(problem!.cause).toBe(cause) + } + }) +} +function checkSiblingRequestIsolation(health: HealthNode | undefined) { + atOracleCheckpoint( + `query-local request health`, + `seed after peer request failure`, + () => { + expect(health, `successful seed exposes health`).toBeDefined() + expect( + health!.error, + `peer query does not own this output's demand`, + ).toBeNull() + expect(health!.sources[0]?.error, `source did not fail`).toBeNull() + }, + ) +} +const siblingStates = [ + `co-mounted pending peers`, + `shared boundary`, + `separate boundaries`, + `same query`, + `different queries`, + `staggered readiness`, + `successful candidate while peer pending`, + `same-pass readiness`, + `both committed`, + `exclusive outputs`, + `retained peer move`, + `seed isolated during peer move`, + `private startup handoff`, + `lagging source readiness`, + `ready source`, + `private startup pending after success`, + `private startup rejection after success`, + `private rejection before peer move`, + `private rejection after peer move`, + `private error notification before peer move`, + `private error notification after peer move`, + `seed source health before peer move`, + `seed source health after peer move`, + `seed source health recovery`, + `post-hook discarded candidate`, + `peer commits before candidate`, + `discarded candidate resumes`, + `peer request error isolated from seed`, +] as const +async function checkSiblings( + history: SiblingsHistory, + scope: Scope, + keyed: boolean, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + let mounted: ReturnType | undefined + lifetime.defer(() => mounted?.unmount()) + const client = scope === `none` ? undefined : new DbClient() + if (client) lifetime.defer(() => client.cleanup()) + await lifetime.run(async () => { + const rows: Array = Array.from({ length: 12 }, (_, rank) => ({ + id: String(rank), + rank, + })) + const releases: Array<() => void> = [] + const loads = [0, 0] + const reports: Array<{ + reportError: (cause: unknown) => void + reportHealthy: () => void + }> = [] + const demands: Array< + Array<{ fulfill: () => void; reject: (cause: unknown) => void }> + > = [[], []] + const sources = [0, 1].map((slot) => + createCollection({ + getKey: (row) => row.id, + syncMode: `on-demand`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ + begin, + write, + commit, + markReady, + reportError, + reportHealthy, + }) => { + reports[slot] = { reportError, reportHealthy } + begin() + for (const value of rows) + write({ type: `insert`, value: { ...value } }) + commit() + releases[slot] = () => { + if (history.sourceReady) markReady() + for (const demand of demands[slot]!.splice(0)) demand.fulfill() + } + return { + loadSubset: () => { + loads[slot]!++ + return new Promise((resolve, reject) => { + demands[slot]!.push({ fulfill: resolve, reject }) + }) + }, + } + }, + }, + }), + ) + lifetime.defer(async () => { + for (const source of sources) await source.cleanup({ dependents: true }) + }) + const records: Array<{ + result?: { + data: unknown + state: ReadonlyMap + collection: { status: string; health: HealthNode } + health: HealthNode | undefined + } + }> = [{}, {}] + const rendered = [0, 0] + const successfulReturns = [0, 0] + let holdCandidate = history.holdCandidate + const blocked = new Promise(() => {}) + let discardedCandidates = 0 + const candidates = new Set() + const failure: Recorded = {} + const diagnostics = vi.spyOn(console, `error`).mockImplementation(() => {}) + lifetime.defer(() => diagnostics.mockRestore()) + function Consumer({ slot, offset }: { slot: number; offset: number }) { + rendered[slot]!++ + const sourceSlot = history.sameQuery ? 0 : slot + const result = useLiveSuspenseQuery({ + ...(scope === `explicit` ? { client } : {}), + ...(keyed ? { queryKey: [`siblings`, sourceSlot, offset] } : {}), + query: (q) => + q + .from({ row: sources[sourceSlot]! }) + .orderBy(({ row }) => row.rank) + .limit(history.width) + .offset(offset) + .select(({ row }) => ({ id: row.id, rank: row.rank })), + }) + successfulReturns[slot]!++ + candidates.add(result.collection) + useLayoutEffect(() => { + const peer = records[1 - slot]!.result + if (peer) + checkSiblingOwnership( + result.collection, + peer.collection, + `successful layout commit`, + ) + records[slot]!.result = result + }) + if (slot === 0 && holdCandidate) { + discardedCandidates++ + throw blocked + } + return + } + const fallback = + const tree = (offset: number) => { + const children = [0, 1].map((slot) => ( + + )) + const body = history.sharedBoundary ? ( + {children} + ) : ( + children.map((child, slot) => ( + + {child} + + )) + ) + const guarded = {body} + return scope === `provider` ? ( + {guarded} + ) : ( + guarded + ) + } + mounted = render(tree(0)) + await settle() + expect( + rendered.every((n) => n > 0), + `React attempted both co-mounted peers`, + ).toBe(true) + expect( + mounted.container.querySelectorAll('[data-phase="ready"]'), + ).toHaveLength(0) + reach.reach(`co-mounted pending peers`) + reach.reach( + history.sharedBoundary ? `shared boundary` : `separate boundaries`, + ) + reach.reach(history.sameQuery ? `same query` : `different queries`) + if (!history.sameQuery && !history.simultaneous) { + await act(() => releases[history.first]!()) + await settle() + // One query can need several acquisitions. Drain only that query and + // retry the host until its hook returns; the peer's work stays held. + for ( + let turn = 0; + turn < 4 && successfulReturns[history.first] === 0; + turn++ + ) { + await act(() => releases[history.first]!()) + await settle() + await act(() => mounted!.rerender(tree(0))) + await settle() + } + expect( + successfulReturns[history.first], + `first candidate returned while its peer remains pending`, + ).toBeGreaterThan(0) + expect(successfulReturns[1 - history.first]).toBe(0) + expect(demands[1 - history.first]!.length).toBeGreaterThan(0) + if (history.sharedBoundary) { + expect( + records.every((record) => record.result === undefined), + `no sibling has committed through the shared pending boundary`, + ).toBe(true) + } + reach.reach(`staggered readiness`) + reach.reach(`successful candidate while peer pending`) + } + await act(() => { + for (const release of releases) release() + }) + await settle() + for ( + let turn = 0; + turn < 4 && + mounted.container.querySelectorAll('[data-phase="ready"]').length < 2; + turn++ + ) { + await act(() => { + for (const release of releases) release() + }) + await settle() + } + if (holdCandidate) { + expect( + discardedCandidates, + `React discarded a candidate after its hook returned`, + ).toBeGreaterThan(0) + reach.reach(`post-hook discarded candidate`) + if (!history.sharedBoundary) { + expect(records[0]!.result).toBeUndefined() + expect( + records[1]!.result, + `independent boundary let the peer commit first`, + ).toBeDefined() + reach.reach(`peer commits before candidate`) + } + holdCandidate = false + await act(() => mounted!.rerender(tree(0))) + await settle() + for ( + let turn = 0; + turn < 4 && + mounted.container.querySelectorAll('[data-phase="ready"]').length < 2; + turn++ + ) { + await act(() => { + for (const release of releases) release() + }) + await settle() + } + reach.reach(`discarded candidate resumes`) + } + // This bounded cut asks for host progress, not a particular retry count. + // A fallback after every requested source is ready violates the model's view. + expect( + mounted.container.querySelectorAll('[data-phase="ready"]'), + `both initial obligations made retry progress`, + ).toHaveLength(2) + reach.reach(`both committed`) + if (history.holdCandidate || history.sameQuery || !history.simultaneous) + console.info(`sibling initial work`, { + scope, + keyed, + shared: history.sharedBoundary, + sameQuery: history.sameQuery, + simultaneous: history.simultaneous, + holdCandidate: history.holdCandidate, + loads, + returnedCollections: candidates.size, + discardedCandidates, + }) + reach.reach( + history.sourceReady ? `ready source` : `lagging source readiness`, + ) + if (history.sameQuery || history.simultaneous) + reach.reach(`same-pass readiness`) + const expected = (offset: number) => + windowRows( + rows, + { + offset, + limit: history.width, + minimum: -1, + descending: false, + nestedLimit: rows.length, + }, + false, + ) + const before = records.map((record) => record.result!.collection) + const comparePeers = (offset: number, cut: string) => { + for (const slot of [0, 1]) { + const record = records[slot]!.result! + const page = slot === history.first ? offset : 0 + checkWindowObservation( + { rows: record.data, collection: record.collection }, + { + rows: expected(page), + previousCollection: before[slot], + retain: true, + }, + `${cut}/${slot}`, + ) + checkWindowState(record.state, expected(page), `${cut}/${slot}`) + } + checkSiblingOwnership( + records[0]!.result!.collection, + records[1]!.result!.collection, + cut, + ) + } + comparePeers(0, `co-mounted exclusive outputs`) + reach.reach(`exclusive outputs`) + const rejectPrivate = async ( + held: { reject: (cause: unknown) => void }, + offset: number, + ) => { + const privateResult = records[1]!.result! + expect( + privateResult.collection.status, + `private output startup is held`, + ).toBe(`loading`) + reach.reach(`private startup pending after success`) + const cause = makeCause(history.cause, history.offset) + await act(() => held.reject(cause)) + await settle() + expect(privateResult.collection.status, `private first load failed`).toBe( + `error`, + ) + reach.reach(`private startup rejection after success`) + // Subset acquisition normalizes provider rejections in core. This + // receiver judges the adapter against the Collection's public failure; + // it does not duplicate core's normalization of non-Error values. + const problem = privateResult.collection.health.error + expect(problem, `the private Collection exposes its failure`).toBeTruthy() + const model = advance( + advance(initial(), { type: `seed`, attempt: 0, rows: [] }), + { + type: `failure`, + attempt: 0, + origin: `source`, + cause: problem!.cause, + }, + ) + compare( + mounted!.container, + failure, + model, + `private startup rejects after seed commit`, + ) + // The error must publish from the observer notification. A host reread + // could otherwise repair a missing notification before this comparison. + reach.reach( + history.rejectBeforeMove + ? `private error notification before peer move` + : `private error notification after peer move`, + ) + await act(() => mounted!.rerender(tree(offset))) + await settle() + compare( + mounted!.container, + failure, + model, + `private startup error after host rerender`, + ) + reach.reach( + history.rejectBeforeMove + ? `private rejection before peer move` + : `private rejection after peer move`, + ) + } + // The source, not the peer's query-local request, owns these reports. + // Only the sync report drives React; no rerender repairs a missed notify. + const reportSeedHealth = async (offset: number, afterMove: boolean) => { + if ( + history.holdCandidate || + !history.sameQuery || + history.first !== 0 || + !history.sourceReady + ) + return + const privateResult = records[1]!.result! + expect( + privateResult.collection.status, + `private startup held for health`, + ).toBe(`loading`) + const cause = new Error(`source health at ${offset}`) + await act(() => reports[0]!.reportError(cause)) + await settle() + checkSiblingHealth( + records[1]!.result!.health, + cause, + `reported source problem`, + ) + comparePeers(offset, `source health preserves page`) + reach.reach( + afterMove + ? `seed source health after peer move` + : `seed source health before peer move`, + ) + await act(() => reports[0]!.reportHealthy()) + await settle() + checkSiblingHealth( + records[1]!.result!.health, + undefined, + `source recovery`, + ) + comparePeers(offset, `source recovery preserves page`) + reach.reach(`seed source health recovery`) + } + await reportSeedHealth(0, false) + if ( + !history.holdCandidate && + history.sameQuery && + history.first === 0 && + history.startup === `reject` && + history.rejectBeforeMove + ) { + const pending = demands[0]![0] + expect(pending, `private startup has outstanding work`).toBeDefined() + await rejectPrivate(pending!, 0) + return + } + await act(() => mounted!.rerender(tree(history.offset))) + // A same-query peer may still be acquiring its private output's first + // subset. Keep that oldest demand held while the first output moves. + const held = + !history.holdCandidate && history.sameQuery && history.first === 0 + ? demands[0]![0] + : undefined + let rejectedPeerMove = false + for (let turn = 0; turn < 4; turn++) { + await settle() + await act(() => { + for (const pending of demands) { + const deliver = pending.filter((release) => release !== held) + pending.splice( + 0, + pending.length, + ...pending.filter((release) => release === held), + ) + for (const demand of deliver) { + if (held && history.peerFailure && !rejectedPeerMove) { + demand.reject(new Error(`peer page request failed`)) + rejectedPeerMove = true + } else demand.fulfill() + } + } + }) + } + await settle() + if (held && history.peerFailure) { + expect( + rejectedPeerMove, + `peer's changed-window acquisition rejected`, + ).toBe(true) + await act(() => mounted!.rerender(tree(history.offset))) + await settle() + const peer = records[0]!.result! + expect(peer.collection.status, `peer passed initial readiness`).toBe( + `ready`, + ) + expect( + peer.health?.error, + `failed demand belongs to peer query`, + ).toBeTruthy() + expect( + records[1]!.result!.collection.status, + `private initial acquisition still held`, + ).toBe(`loading`) + // The glossary assigns request errors to the query that owns the demand. + // Source health is shared; this query-local problem is not source health. + checkSiblingRequestIsolation(records[1]!.result!.health) + reach.reach(`peer request error isolated from seed`) + return + } + if (held) { + // Re-read both hooks after the moved output publishes, while the private + // startup demand remains held. An unchanged-input host render must not + // replace the peer's copied rows with the moved output's live snapshot. + await act(() => mounted!.rerender(tree(history.offset))) + await settle() + comparePeers(history.offset, `seed while successful peer has moved`) + reach.reach(`seed isolated during peer move`) + await reportSeedHealth(history.offset, true) + const privateResult = records[1]!.result! + expect( + privateResult.collection.status, + `private output startup is held`, + ).toBe(`loading`) + reach.reach(`private startup pending after success`) + if (history.startup === `reject`) { + await rejectPrivate(held, history.offset) + return + } + await act(() => { + for (const release of releases) release() + }) + await settle() + comparePeers(history.offset, `private startup handoff`) + reach.reach(`private startup handoff`) + } + comparePeers(history.offset, `co-mounted page independence`) + expect( + mounted.container.querySelectorAll('[data-phase="ready"]'), + ).toHaveLength(2) + reach.reach(`retained peer move`) + }) +} +for (const scope of [`none`, `provider`, `explicit`] as const) + for (const keyed of [false, true]) + for (const campaign of [`fixed`, `fresh`] as const) { + const property = `react.suspense-siblings` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`co-mounted siblings/${scope}/${keyed}/${campaign}`, async () => { + const reach = createOracleReach( + `${property}/${scope}/${keyed}/${campaign}`, + siblingStates, + ) + const fidelity = createOracleFailureFidelity() + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(siblingsArbitrary, async (history) => { + for (const member of siblingHistories(history)) { + reach.beginHistory() + await fidelity.run(member, () => + checkSiblings(member, scope, keyed, reach), + ) + } + }), + campaign === `fixed` + ? { numRuns: oracleRuns(8), seed: 2348 } + : oraclePropertyOptions(8, property), + ), + ) + reach.assertReached() + } finally { + console.info( + `sibling reach ${scope}/${keyed}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }, 60_000) + } +declareOracle({ + grammar: [siblingsArbitrary, siblingHistories], + check: [ + checkSiblings, + checkSiblingOwnership, + checkSiblingHealth, + checkSiblingRequestIsolation, + ], +}) diff --git a/packages/react-db/tests/suspense-local-options-oracle.test.tsx b/packages/react-db/tests/suspense-local-options-oracle.test.tsx index 36195c75fe..dfa865d074 100644 --- a/packages/react-db/tests/suspense-local-options-oracle.test.tsx +++ b/packages/react-db/tests/suspense-local-options-oracle.test.tsx @@ -26,8 +26,8 @@ import { afterAll, expect, test } from 'vitest' import { DbProvider } from '../src/DbProvider' import { useLiveSuspenseQuery } from '../src/useLiveSuspenseQuery' import { - checkLocalOptionSharing, - compatibleLocalOptions, + checkLocalOutputOwnership, + equalLocalDeclarations, localOptionPairs, } from '../../db/tests/live-query-cache-options-oracle' import { declareOracle } from '../../db/tests/oracle-declaration' @@ -39,8 +39,7 @@ import type { ReactNode } from 'react' declareOracle({ grammar: [localOptionPairs], - model: [compatibleLocalOptions], - check: [checkLocalOptionSharing], + check: [checkLocalOutputOwnership], }) const histories = localOptionPairs.flatMap((history, cell) => // Callback/reference and scalar equality are distinct premises. Keep the @@ -68,8 +67,8 @@ const pendingReaches = Object.fromEntries( scope, createOracleReach(`Suspense local options pending/${scope}`, [ `both-suspended`, - `shared-output`, - `separate-outputs`, + `equal-options`, + `different-options`, `callback-options`, `scalar-options`, ]), @@ -252,9 +251,9 @@ test.each(histories)( reach.reach(`first-output`) const second = await readSecond() reach.reach(`second-output`) - checkLocalOptionSharing(first, second, history) + checkLocalOutputOwnership(first, second, history) pendingReach?.reach( - compatibleLocalOptions(history) ? `shared-output` : `separate-outputs`, + equalLocalDeclarations(history) ? `equal-options` : `different-options`, ) expect(first.config).toEqual(retainedOptions) reach.reach(`retained-first`) diff --git a/packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx b/packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx index 09de1a71a6..2a680c080a 100644 --- a/packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx +++ b/packages/react-db/tests/useLiveQuery.uncommitted-render.test.tsx @@ -262,7 +262,7 @@ describe(`live queries across uncommitted renders`, () => { expect(loadCount).toBe(1) }) - it(`retains a shared precommit collection until every suspense consumer commits`, async () => { + it(`finishes independent suspense queries when peers commit at different times`, async () => { let resolveLoad!: () => void let releaseSecondRender!: () => void let loadSettled = false @@ -275,7 +275,6 @@ describe(`live queries across uncommitted renders`, () => { const secondRender = new Promise((resolve) => { releaseSecondRender = resolve }) - const never = new Promise(() => {}) const source = createCollection({ id: `two-uncommitted-async-on-demand-consumers`, getKey: (person) => person.id, @@ -291,7 +290,9 @@ describe(`live queries across uncommitted renders`, () => { return { loadSubset: () => { loadCount++ - return loadSettled ? never : load + // Each independent output can acquire the already loaded rows. + // Only the initial acquisition waits for provider delivery. + return loadSettled ? true : load }, } }, @@ -302,12 +303,15 @@ describe(`live queries across uncommitted renders`, () => { query: (q: InitialQueryBuilder) => q.from({ person: source }).where(({ person }) => eq(person.id, `1`)), } + const outputs = new Map() const First = () => { - const { data } = useLiveSuspenseQuery(config) + const { data, collection } = useLiveSuspenseQuery(config) + outputs.set(`First`, collection) return
First: {data[0]?.name}
} const Second = () => { - const { data } = useLiveSuspenseQuery(config) + const { data, collection } = useLiveSuspenseQuery(config) + outputs.set(`Second`, collection) if (secondRenderBlocked) throw secondRender return
Second: {data[0]?.name}
} @@ -341,10 +345,14 @@ describe(`live queries across uncommitted renders`, () => { }) await advanceTime(1) expect(view.getByText(`Second: Alice`)).toBeDefined() - expect(loadCount).toBe(1) + expect(outputs.get(`Second`) === outputs.get(`First`)).toBe(false) + expect(loadCount).toBeGreaterThan(1) + view.unmount() + await advanceTime(5100) + expect(source.subscriberCount).toBe(0) }) - it(`shares an active suspense collection and retires it after cleanup`, async () => { + it(`keeps suspense outputs independent and retires them after cleanup`, async () => { let releaseSecondRender!: () => void let secondRenderBlocked = true const secondRender = new Promise((resolve) => { @@ -406,12 +414,15 @@ describe(`live queries across uncommitted renders`, () => { await advanceTime(1) expect(thirdView.getByText(`Third: A 0`)).toBeDefined() - expect(liveCollections.get(`Third`)).toBe(secondCollection) + expect(secondCollection === firstCollection).toBe(false) + expect(liveCollections.get(`Third`) === secondCollection).toBe(false) + expect(liveCollections.get(`Third`) === firstCollection).toBe(false) firstView.unmount() secondView.unmount() thirdView.unmount() await advanceTime(5100) + expect(source.subscriberCount).toBe(0) const fourthView = render( Fourth loading}> @@ -424,7 +435,7 @@ describe(`live queries across uncommitted renders`, () => { expect(liveCollections.get(`Fourth`)).not.toBe(secondCollection) }) - it(`retains a ready collection while its next suspense consumer is pending commit`, async () => { + it(`keeps a pending suspense peer usable after the first binding unmounts`, async () => { let releaseSecondRender!: () => void let secondRenderBlocked = true const secondRender = new Promise((resolve) => { @@ -462,10 +473,11 @@ describe(`live queries across uncommitted renders`, () => { , ) expect(secondView.getByText(`Second loading`)).toBeDefined() - expect(collectionsByLabel.get(`Second`)).toBe(firstCollection) + expect(collectionsByLabel.get(`Second`) === firstCollection).toBe(false) firstView.unmount() await advanceTime(2) + expect(source.subscriberCount).toBeGreaterThan(0) await act(async () => { secondRenderBlocked = false @@ -475,7 +487,11 @@ describe(`live queries across uncommitted renders`, () => { await advanceTime(1) expect(secondView.getByText(`Second: A`)).toBeDefined() - expect(collectionsByLabel.get(`Second`)).toBe(firstCollection) + expect(collectionsByLabel.get(`Second`) === firstCollection).toBe(false) + expect(source.subscriberCount).toBeGreaterThan(0) + secondView.unmount() + await advanceTime(5100) + expect(source.subscriberCount).toBe(0) }) it(`does not share precommit queries across distinct source objects with the same id`, async () => { diff --git a/packages/react-db/tests/window-render-oracle.test.tsx b/packages/react-db/tests/window-render-oracle.test.tsx index 75d658d388..60c1f8cb34 100644 --- a/packages/react-db/tests/window-render-oracle.test.tsx +++ b/packages/react-db/tests/window-render-oracle.test.tsx @@ -5,22 +5,28 @@ * It uses the shared recomputation and comparison, with synchronous eager and * finite on-demand sources. Async settlement belongs to the core window owner. * Offset-only, limit-only and combined moves have separate receiving campaigns. + * Client scopes cover provider, explicit client, and descriptor sources. * Their speculative proposals may differ, but only committed windows are public. */ import { Suspense, startTransition, useLayoutEffect, useState } from 'react' -import { act, render } from '@testing-library/react' +import { act, render, renderHook, screen } from '@testing-library/react' import { describe, expect, it } from 'vitest' import fc from 'fast-check' import { BTreeIndex, + DbClient, + collectionOptions, createCollection, localOnlyCollectionOptions, } from '@tanstack/db' import { useLiveQuery } from '../src/useLiveQuery' +import { useLiveSuspenseQuery } from '../src/useLiveSuspenseQuery' +import { DbProvider } from '../src/DbProvider' import { makeInfiniteOnDemandSource } from '../../db/tests/conformance/infinite-on-demand' import { ScenarioLifetime } from '../../db/tests/conformance/scenario-lifetime' import { checkWindowObservation, + checkWindowState, windowRows, } from '../../db/tests/conformance/window-history-oracle' import { createOracleReach } from '../../db/tests/oracle-reach' @@ -68,12 +74,15 @@ type RenderHistory = { } type PageWindow = { offset: number; limit: number } type Move = `offset` | `limit` | `both` +type ClientScope = `none` | `provider` | `explicit` | `descriptor` async function checkRenderHistory( history: RenderHistory, remote: boolean, keyed: boolean, move: Move, + scope: ClientScope, + suspense: boolean, reach: ReturnType>, ) { const lifetime = new ScenarioLifetime() @@ -84,6 +93,8 @@ async function checkRenderHistory( id: String(id).padStart(2, `0`), rank: id, })) + const client = scope === `none` ? undefined : new DbClient() + if (client) lifetime.defer(() => client.cleanup()) const provider = remote ? makeInfiniteOnDemandSource({ createCollection, BTreeIndex }, rows) : undefined @@ -98,6 +109,20 @@ async function checkRenderHistory( }), ) lifetime.defer(() => source.cleanup({ dependents: true })) + const descriptor = + scope === `descriptor` + ? collectionOptions(source.id, () => ({ + ...source.config, + schema: undefined, + getKey: (row: WindowRow) => row.id, + })) + : undefined + // The same provider fixture supplies its sync configuration to the client. + // Only the descriptor is authored into this hook's query. + if (descriptor) + lifetime.defer(() => + client!.collection(descriptor).cleanup({ dependents: true }), + ) const initial = { offset: 0, limit: history.limit } const abandoned = { offset: move === `limit` ? 0 : history.abandoned, @@ -123,15 +148,20 @@ async function checkRenderHistory( function Page() { const [page, update] = useState({ ...initial, suspend: false }) setPage = update - const query = (q: InitialQueryBuilder) => - q - .from({ row: source }) + const query = (q: InitialQueryBuilder) => { + const from = descriptor + ? q.from({ row: descriptor }) + : q.from({ row: source }) + return from .orderBy(({ row }) => row.rank, `desc`) .limit(page.limit) .offset(page.offset) .select(({ row }) => ({ id: row.id, rank: row.rank })) - const result = useLiveQuery({ + } + const usePage = suspense ? useLiveSuspenseQuery : useLiveQuery + const result = usePage({ query, + ...(scope === `explicit` ? { client } : {}), ...(keyed ? { queryKey: [`window-render`, page.offset, page.limit] } : {}), @@ -148,7 +178,13 @@ async function checkRenderHistory( } const view = render( - + {client && scope !== `explicit` ? ( + + + + ) : ( + + )} , ) unmount = () => view.unmount() @@ -178,7 +214,7 @@ async function checkRenderHistory( let interruption: { error: unknown } | undefined try { compare(initial, `initial`) - await act(async () => + await act(() => startTransition(() => setPage({ ...abandoned, suspend: true })), ) atOracleCheckpoint(`render attempted`, `suspended`, () => @@ -215,10 +251,10 @@ async function checkRenderHistory( collection: initialCollection, }), ).toThrow() - await act(async () => setPage({ ...initial, suspend: false })) + await act(() => setPage({ ...initial, suspend: false })) reach.reach(`cancelled move`) capture(() => compare(initial, `cancelled`)) - await act(async () => setPage({ ...accepted, suspend: false })) + await act(() => setPage({ ...accepted, suspend: false })) reach.reach(`committed move`) capture(() => compare(accepted, `committed`)) } catch (error) { @@ -233,40 +269,375 @@ async function checkRenderHistory( } describe(`window changes at React commit`, () => { - for (const remote of [false, true]) - for (const keyed of [false, true]) { - for (const move of [`offset`, `limit`, `both`] as const) + for (const suspense of [false, true]) + for (const scope of [`none`, `provider`, `explicit`, `descriptor`] as const) + for (const remote of [false, true]) + for (const keyed of [false, true]) { + for (const move of [`offset`, `limit`, `both`] as const) + for (const campaign of [`fixed`, `fresh`] as const) { + const property = `react.window-render` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`${suspense ? `Suspense` : `ordinary`}/${scope}/${remote ? `on-demand` : `eager`}/${keyed ? `keyed` : `derived`}/${move}/${campaign}`, async () => { + const reach = createOracleReach( + `${property}/${suspense}/${scope}/${remote}/${keyed}/${move}/${campaign}`, + states, + { fixedHistories: examples.length }, + ) + let index = 0 + const fidelity = createOracleFailureFidelity() + const propertyUnderTest = fc.asyncProperty( + renderHistoryArbitrary, + async (history) => { + reach.beginHistory({ fixed: index++ < examples.length }) + await fidelity.run(history, () => + checkRenderHistory( + history, + remote, + keyed, + move, + scope, + suspense, + reach, + ), + ) + }, + ) + try { + await fidelity.assert(() => + fc.assert( + propertyUnderTest, + campaign === `fixed` + ? { + numRuns: oracleRuns(6) + examples.length, + seed: 2291, + examples: [...examples].map(([value]) => [value]), + } + : oraclePropertyOptions(6, property, { + examples: [...examples].map(([value]) => [value]), + }), + ), + ) + } finally { + console.info( + `render reach ${scope}/${remote}/${keyed}/${move}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + reach.assertReached() + }) + } + } +}) +declareOracle({ + grammar: [renderHistoryArbitrary], + check: [checkRenderHistory], +}) + +/** A mounted binding can suspend on a replacement while another hook finishes + * that same preload first. React preserves the mounted binding's refs in this + * case. On resumption it still owes its own output, not the peer's mutable page. + * The before-hook hold forces that ordering instead of assuming React's retry + * order. Peer-first admission also crosses a mounted replacement with a peer + * already pending under the target plan. A post-hook hold discards a successful + * candidate after resolution. Neither schedule changes the shared page law. + * Work counters are diagnostics; uncommitted Collection retention is not owed. + * Expected rows and identity use the shared page comparison above. + */ +const retryStates = [ + `replacement suspended`, + `peer ready`, + `replacement resumed`, + `page isolated`, + `borrower moves after compiler retries`, + `original page retained after borrower move`, + `peer pending before mounted replacement`, + `successful replacement held after hook`, +] as const +async function checkRetryOwnershipHistory( + history: RenderHistory, + withClient: boolean, + keyed: boolean, + simultaneous: boolean, + order: `compiler-first` | `peer-first` | `post-hook`, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + const rows = Array.from({ length: 24 }, (_, rank) => ({ + id: String(rank), + rank, + })) + const client = withClient ? new DbClient() : undefined + if (client) lifetime.defer(() => client.cleanup()) + const firstSource = createCollection( + localOnlyCollectionOptions({ + getKey: (row: WindowRow) => row.id, + initialData: rows, + }), + ) + let release: (() => void) | undefined + let replacementRendersHeld = 0 + const replacement = createCollection({ + getKey: (row: WindowRow) => row.id, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (const value of rows) write({ type: `insert`, value: { ...value } }) + commit() + release = markReady + }, + }, + }) + lifetime.defer(() => firstSource.cleanup({ dependents: true })) + lifetime.defer(() => replacement.cleanup({ dependents: true })) + const props = { replace: false, block: false, offset: 0 } + const query = (replace: boolean, offset: number) => ({ + client, + ...(keyed ? { queryKey: [`retry-output`, replace, offset] } : {}), + query: (q: InitialQueryBuilder) => + q + .from({ row: replace ? replacement : firstSource }) + .orderBy(({ row }) => row.rank) + .limit(history.limit) + .offset(offset) + .select(({ row }) => ({ id: row.id, rank: row.rank })), + }) + const blocked = new Promise(() => {}) + const hook = renderHook( + ({ replace, block, offset }) => { + if (block && order !== `post-hook`) throw blocked + const result = useLiveSuspenseQuery(query(replace, offset)) + if (block && order === `post-hook`) { + replacementRendersHeld++ + throw blocked + } + return result + }, + { + initialProps: props, + wrapper: ({ children }) => ( + }> + {children} + + ), + }, + ) + lifetime.defer(() => hook.unmount()) + await lifetime.run(async () => { + await act(async () => { + await Promise.resolve() + }) + expect(hook.result.current.data).toHaveLength(history.limit) + let peerOffset = 0 + const mountPeer = () => + renderHook(() => useLiveSuspenseQuery(query(true, peerOffset)), { + wrapper: ({ children }) => ( + }> + {children} + + ), + }) + const earlyPeer = order === `peer-first` ? mountPeer() : undefined + if (earlyPeer) { + expect(screen.getByTestId(`retry-peer`)).toBeTruthy() + reach.reach(`peer pending before mounted replacement`) + } + await act(() => hook.rerender({ ...props, replace: true })) + expect(screen.getByTestId(`retry-primary`)).toBeTruthy() + expect(release, `replacement source was requested`).toBeDefined() + reach.reach(`replacement suspended`) + await act(() => hook.rerender({ ...props, replace: true, block: true })) + const peer = earlyPeer ?? mountPeer() + lifetime.defer(() => peer.unmount()) + if (!earlyPeer) + expect( + replacement.subscriberCount, + `both pending bindings adopted one preload`, + ).toBe(1) + else + console.info(`mounted replacement pending work`, { + withClient, + keyed, + subscriptions: replacement.subscriberCount, + }) + await act(() => { + if (simultaneous && order !== `post-hook`) + hook.rerender({ ...props, replace: true }) + release!() + }) + expect(peer.result.current.data).toHaveLength(history.limit) + if (!simultaneous || order === `post-hook`) + expect(screen.getByTestId(`retry-primary`)).toBeTruthy() + if (order === `post-hook`) { + expect( + replacementRendersHeld, + `React attempted the ready hook before the hold`, + ).toBeGreaterThan(0) + reach.reach(`successful replacement held after hook`) + } + reach.reach(`peer ready`) + const peerCollection = peer.result.current.collection + await act(() => hook.rerender({ ...props, replace: true })) + const expected = (offset: number) => + windowRows( + rows, + { + offset, + limit: history.limit, + minimum: -1, + descending: false, + nestedLimit: rows.length, + }, + false, + ) + checkWindowObservation( + { + rows: hook.result.current.data, + collection: hook.result.current.collection, + }, + { rows: expected(0), previousCollection: peerCollection, retain: false }, + `resumed binding owns its output`, + ) + reach.reach(`replacement resumed`) + const collection = hook.result.current.collection + checkWindowState( + hook.result.current.state, + expected(0), + `resumed keyed view`, + ) + checkWindowState(peer.result.current.state, expected(0), `peer keyed view`) + // The borrower adopted pending work compiled by the original binding. + // Its next use must retain its own Collection even after that compiler retries. + peerOffset = history.abandoned + await act(() => peer.rerender()) + checkWindowObservation( + { + rows: peer.result.current.data, + collection: peer.result.current.collection, + }, + { + rows: expected(peerOffset), + previousCollection: peerCollection, + retain: true, + }, + `borrower page after original compiler retries`, + ) + reach.reach(`borrower moves after compiler retries`) + checkWindowState( + peer.result.current.state, + expected(peerOffset), + `borrower keyed view after compiler retries`, + ) + // Observe the unchanged binding before its own next move can repair it. + checkWindowObservation( + { + rows: hook.result.current.data, + collection: hook.result.current.collection, + }, + { rows: expected(0), previousCollection: collection, retain: true }, + `original page after borrower move`, + ) + checkWindowState( + hook.result.current.state, + expected(0), + `original keyed view after borrower move`, + ) + checkWindowObservation( + { rows: collection.toArray, collection }, + { rows: expected(0), previousCollection: collection, retain: true }, + `original Collection page after borrower move`, + ) + reach.reach(`original page retained after borrower move`) + await act(() => + hook.rerender({ ...props, replace: true, offset: history.committed }), + ) + checkWindowObservation( + { + rows: hook.result.current.data, + collection: hook.result.current.collection, + }, + { + rows: expected(history.committed), + previousCollection: collection, + retain: true, + }, + `resumed binding page move`, + ) + checkWindowObservation( + { + rows: peer.result.current.data, + collection: peer.result.current.collection, + }, + { + rows: expected(peerOffset), + previousCollection: peerCollection, + retain: true, + }, + `peer after resumed binding page move`, + ) + checkWindowState( + hook.result.current.state, + expected(history.committed), + `resumed binding keyed view after page move`, + ) + checkWindowState( + peer.result.current.state, + expected(peerOffset), + `peer keyed view after resumed binding page move`, + ) + reach.reach(`page isolated`) + }) +} +for (const client of [false, true]) + for (const keyed of [false, true]) + for (const simultaneous of [false, true]) + for (const order of [ + `compiler-first`, + `peer-first`, + `post-hook`, + ] as const) for (const campaign of [`fixed`, `fresh`] as const) { - const property = `react.window-render` + const property = `react.window-retry-ownership` const test = campaign === `fixed` ? oracleReplayTest(it) : oracleReplayTest(it, property) - test(`${remote ? `on-demand` : `eager`}/${keyed ? `keyed` : `derived`}/${move}/${campaign}`, async () => { + test(`retry ownership/${client ? `client` : `unscoped`}/${keyed ? `keyed` : `derived`}/${simultaneous ? `co-ready` : `borrower-first`}/${order}/${campaign}`, async () => { const reach = createOracleReach( - `${property}/${remote}/${keyed}/${move}/${campaign}`, - states, + `${property}/${client}/${keyed}/${simultaneous}/${order}/${campaign}`, + retryStates.filter( + (state) => + (state !== `peer pending before mounted replacement` || + order === `peer-first`) && + (state !== `successful replacement held after hook` || + order === `post-hook`), + ), { fixedHistories: examples.length }, ) let index = 0 const fidelity = createOracleFailureFidelity() - const propertyUnderTest = fc.asyncProperty( - renderHistoryArbitrary, - async (history) => { - reach.beginHistory({ fixed: index++ < examples.length }) - await fidelity.run(history, () => - checkRenderHistory(history, remote, keyed, move, reach), - ) - }, - ) try { await fidelity.assert(() => fc.assert( - propertyUnderTest, + fc.asyncProperty(renderHistoryArbitrary, async (history) => { + reach.beginHistory({ fixed: index++ < examples.length }) + await fidelity.run(history, () => + checkRetryOwnershipHistory( + history, + client, + keyed, + simultaneous, + order, + reach, + ), + ) + }), campaign === `fixed` ? { numRuns: oracleRuns(6) + examples.length, - seed: 2291, + seed: 2345, examples: [...examples].map(([value]) => [value]), } : oraclePropertyOptions(6, property, { @@ -276,20 +647,14 @@ describe(`window changes at React commit`, () => { ) } finally { console.info( - `render reach ${remote}/${keyed}/${move}/${campaign}`, + `retry reach ${client}/${keyed}/${simultaneous}/${order}/${campaign}`, Object.fromEntries(reach.counts()), ) } reach.assertReached() - console.info( - `window render reach ${remote}/${keyed}/${move}/${campaign}`, - Object.fromEntries(reach.counts()), - ) }) } - } -}) declareOracle({ grammar: [renderHistoryArbitrary], - check: [checkRenderHistory], + check: [checkRetryOwnershipHistory], }) diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index 939b289544..acbfe9bb56 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -6,6 +6,7 @@ import { UnhashableQueryIRError, createLiveQueryObserver, createLiveQuerySourceIdClaims, + deepEquals, getLiveQueryHash, getPublicCollection, getStableValueHash, @@ -334,7 +335,19 @@ export function useLiveQuery( // it unmounts. const sourceIdClaims = createLiveQuerySourceIdClaims() - let previousCollection: ReturnType = null + // A stable client key keeps the resolved binding; page changes keep only + // its Collection when the resolver confirms that the plan is unchanged. + let previous: + | { + collection: ReturnType + client?: DbClient + queryKey?: LiveQueryKey + queryHash?: string + move?: () => void + ownsCollection?: boolean + onPreload: () => void + } + | undefined const resolved = $derived.by(() => { let commitWindow: (() => void) | undefined // Try the getter form first; a query callback needs the builder supplied @@ -354,7 +367,6 @@ export function useLiveQuery( contextDbClient) if (inputIsCollection) { - previousCollection = null // Warn when passing a collection directly with on-demand sync mode // In on-demand mode, data is only loaded when queries with predicates request it // Passing the collection directly doesn't provide any predicates, so no data loads @@ -374,7 +386,7 @@ export function useLiveQuery( if (unwrappedParam.status === `idle`) { unwrappedParam.startSyncImmediate() } - return { + return (previous = { collection: unwrappedParam, client: dbClient, queryHash: getStableValueHash( @@ -383,7 +395,7 @@ export function useLiveQuery( ), onPreload: () => {}, move: undefined, - } + }) } // Reference deps to make computed reactive to them @@ -392,17 +404,25 @@ export function useLiveQuery( // The compile boundary: a query rejected anywhere after preparation // releases the sync starts it deferred, so a later reader can start them. try { - const preparedValue = prepareLiveQueryValue( - unwrappedParam, - dbClient, - deferredCollections, - ) const configuredQueryKey = ( unwrappedParam as { queryKey?: MaybeGetter } | null )?.queryKey const queryKey = configuredQueryKey ? toValue(configuredQueryKey) : undefined + if ( + dbClient && + queryKey?.length && + previous?.collection && + previous.client === dbClient && + deepEquals(previous.queryKey, queryKey) + ) + return previous + const preparedValue = prepareLiveQueryValue( + unwrappedParam, + dbClient, + deferredCollections, + ) // A DbClient matches preloaded and hydrated queries by identity hash, so // a client-backed query without queryKey needs a stable IR identity and @@ -442,17 +462,18 @@ export function useLiveQuery( } let collection: Collection | null - if (preparedValue === undefined || preparedValue === null) { - collection = null - } else if (isCollection(preparedValue)) { + if (isCollection(preparedValue)) { collection = preparedValue } else { - // Hydration keys the live-query Collection by identity, so it never - // pools, even when the config passes a pool. + // Client observers consume page snapshots independently of the local + // Collection, so this path does not use equality-partition pooling. collection = resolveLiveQueryValue(preparedValue, { gcTime: DEFAULT_GC_TIME_MS, pool: dbClient ? false : undefined, - previousCollection: dbClient ? undefined : previousCollection, + previousCollection: + previous?.ownsCollection && previous.client === dbClient + ? previous.collection + : null, windowOwner: sourceIdClaims, onWindowChange: (move) => { commitWindow = move @@ -460,9 +481,6 @@ export function useLiveQuery( }) } - previousCollection = - dbClient || isCollection(preparedValue) ? null : collection - // A setup that never subscribes releases its deferred sync starts when // garbage collection reclaims the live query it built. A Collection // the input supplied is not this setup's to reclaim. @@ -470,8 +488,10 @@ export function useLiveQuery( collection && !isCollection(preparedValue) ? releaseDeferredOnReclaim(collection, deferredCollections) : undefined - return { + return (previous = { collection, + queryKey: queryKey && [...queryKey], + ownsCollection: !isCollection(preparedValue), move: commitWindow, client: dbClient, queryHash, @@ -481,7 +501,7 @@ export function useLiveQuery( cancelReclaimRelease?.() resumeDeferredLiveQueryCollections(deferredCollections) }, - } + }) } catch (error) { return rejectLiveQueryInput(deferredCollections, error) } @@ -538,7 +558,8 @@ export function useLiveQuery( } // Source notifications write Svelte state. Apply moves outside the derived - // input and retain the observer when only its window changes. + // input. Client-backed moves replace the observer for the new page hash; + // other window moves retain it. $effect.pre(() => { const next = resolved untrack(() => next.move?.()) @@ -547,7 +568,7 @@ export function useLiveQuery( const next = resolved return next.client ? next : next.collection }) - // Watch for collection changes and subscribe to updates + // Subscribe when the Collection or client page identity changes. $effect(() => { observerInput const nextResolved = untrack(() => resolved) diff --git a/packages/svelte-db/tests/client-window-oracle.svelte.test.ts b/packages/svelte-db/tests/client-window-oracle.svelte.test.ts new file mode 100644 index 0000000000..efe69197d7 --- /dev/null +++ b/packages/svelte-db/tests/client-window-oracle.svelte.test.ts @@ -0,0 +1,482 @@ +/** + * Svelte receiving driver for the shared transported-page/window laws. + * It supplies real runes and flushSync, JSON server preloads, held browser + * readiness and fresh old-page readers. The independent sort/slice judgment + * and history grammar are shared with React; this does not claim hydrate(). + */ +import { + BTreeIndex, + DbClient, + collectionOptions, + createCollection, +} from '@tanstack/db' +import { flushSync } from 'svelte' +import fc from 'fast-check' +import { describe, expect, it, vi } from 'vitest' +import { useLiveQuery } from '../src/useLiveQuery.svelte' +import { + checkWindowObservation, + checkWindowState, + windowRows, +} from '../../db/tests/conformance/window-history-oracle' +import { + clientWindowArbitrary, + clientWindowStates, + clientWindowExamples as examples, +} from '../../db/tests/conformance/client-window-history-oracle' +import { ScenarioLifetime } from '../../db/tests/conformance/scenario-lifetime' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { createOracleFailureFidelity } from '../../db/tests/oracle-failure-fidelity' +import { + declareOracle, + oraclePropertyOptions, + oracleReplayTest, + oracleRuns, +} from '../../db/tests/oracle-config' +import type { + InitialQueryBuilder, + LiveQueryCollectionUtils, +} from '@tanstack/db' +import type { WindowRow } from '../../db/tests/conformance/window-history-oracle' +import type { ClientWindowHistory as History } from '../../db/tests/conformance/client-window-history-oracle' +// Client switching has its own React scope driver; this receiver measures only +// the states it actually supplies. Every keyed/derived campaign has its own reach. +const states = clientWindowStates.filter( + (state) => state !== `client scope switch`, +) +async function checkClientWindowHistory( + history: History, + keyed: boolean, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + const unmounts: Array<() => void> = [] + lifetime.defer(() => unmounts.forEach((unmount) => unmount())) + const server = new DbClient() + const browser = new DbClient() + lifetime.defer(() => server.cleanup()) + lifetime.defer(() => browser.cleanup()) + await lifetime.run(async () => { + const serverRows: Array = Array.from( + { length: history.count }, + (_, id) => ({ id: `s${id}`, rank: id }), + ) + const browserRows = serverRows.map((row) => ({ + ...row, + id: `b${row.rank}`, + })) + let ready: (() => void) | undefined + const descriptor = collectionOptions(`client-window-source`, (owner) => ({ + getKey: (row: WindowRow) => row.id, + autoIndex: `eager`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (const value of owner === server ? serverRows : browserRows) + write({ type: `insert`, value: { ...value } }) + commit() + if (owner === server) markReady() + else ready = markReady + }, + }, + })) + const first = { offset: history.offset, limit: history.width } + const next = { offset: first.offset + history.jump, limit: first.limit + 1 } + const build = (q: InitialQueryBuilder, page: typeof first) => + q + .from({ row: descriptor }) + .orderBy(({ row }) => row.rank) + .limit(page.limit) + .offset(page.offset) + .select(({ row }) => ({ id: row.id, rank: row.rank })) + const config = (page: typeof first) => ({ + query: (q: InitialQueryBuilder) => build(q, page), + ...(keyed + ? { queryKey: [`client-window`, page.offset, page.limit] } + : {}), + }) + for (const page of [first, next]) + await server.preloadLiveQuery(config(page)) + const transport = JSON.parse(JSON.stringify(server.dehydrate())) + browser.hydrate(transport) + const expected = (rows: Array, page: typeof first) => + windowRows( + rows, + { ...page, minimum: -1, descending: false, nestedLimit: rows.length }, + false, + ) + // The first view is captured during rune construction, before effects. + // Page updates are observed at Svelte's native flushSync cut. + const mountPage = (initial: typeof first) => { + let page = $state(initial) + let current!: () => { + data: unknown + collection: unknown + state: ReadonlyMap + } + let initialView!: { data: unknown; collection: unknown } + const dispose = $effect.root(() => { + const result = useLiveQuery({ + query: (q) => build(q, page), + queryKey: () => + keyed ? [`client-window`, page.offset, page.limit] : [], + client: browser, + }) + current = () => result + initialView = { data: [...result.data], collection: result.collection } + }) + unmounts.push(dispose) + return { + current, + initialView, + move(requested: typeof first) { + page = requested + flushSync() + }, + } + } + const hook = mountPage(first) + const firstRender = hook.initialView + flushSync() + checkWindowObservation( + { rows: firstRender.data, collection: firstRender.collection }, + { rows: expected(serverRows, first), retain: true }, + `first preloaded page render`, + ) + reach.reach(`first seed`) + const collection = hook.current().collection + const compare = ( + rows: Array, + page: typeof first, + checkpoint: string, + retain = true, + ) => { + checkWindowObservation( + { + rows: hook.current().data, + collection: hook.current().collection, + }, + { rows: expected(rows, page), previousCollection: collection, retain }, + checkpoint, + ) + checkWindowState(hook.current().state, expected(rows, page), checkpoint) + } + const peer = mountPage(first) + flushSync() + // Judge the existing peer's keyed view as well as its rows while the + // transported seed is held and when the local snapshot takes over. + const comparePeer = (rows: Array, cut: string) => { + checkWindowObservation( + { + rows: peer.current().data, + collection: peer.current().collection, + }, + { + rows: expected(rows, first), + previousCollection: collection, + retain: false, + }, + cut, + ) + checkWindowState(peer.current().state, expected(rows, first), cut) + } + comparePeer(serverRows, `peer before move`) + // Independent query inputs own distinct Collections. Moving one hook leaves + // the other hook on its original page. + hook.move(next) + compare( + serverRows, + next, + `moved preloaded page before local readiness`, + true, + ) + reach.reach(`moved seed`) + comparePeer(serverRows, `peer while local readiness is held`) + reach.reach(`held peer keyed state`) + for (let index = 0; index < history.latePeers; index++) { + const late = mountPage(first) + flushSync() + checkWindowObservation( + { rows: late.current().data, collection: late.current().collection }, + { + rows: expected(serverRows, first), + previousCollection: collection, + retain: false, + }, + `fresh old-page lookup after move before local readiness`, + ) + checkWindowState( + late.current().state, + expected(serverRows, first), + `fresh old-page keyed view`, + ) + expect(late.current().collection).not.toBe(peer.current().collection) + } + reach.reach(`fresh old-page lookup`) + expect(ready, `browser sync started at commit`).toBeDefined() + ready!() + flushSync() + // Native flush does not settle the observer's queued readiness handoff. + // Require the public local result within the bounded receiving cut. + await vi.waitFor(() => { + flushSync() + compare(browserRows, next, `local handoff`) + comparePeer(browserRows, `peer after handoff`) + }) + reach.reach(`local handoff`) + comparePeer(browserRows, `peer after handoff`) + reach.reach(`peer isolation`) + reach.reach(`local peer keyed state`) + hook.move(first) + await vi.waitFor(() => { + flushSync() + compare(browserRows, first, `return to first page`) + }) + reach.reach(`returned page`) + // Ordinary moves do not rewrite the server's immutable page records. + expect(JSON.parse(JSON.stringify(server.dehydrate()))).toEqual(transport) + }) +} + +describe(`Svelte client page identity`, () => { + for (const keyed of [false, true]) + for (const campaign of [`fixed`, `fresh`] as const) { + const property = `svelte.client-window` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`${keyed ? `keyed` : `derived`}/${campaign}`, async () => { + const reach = createOracleReach( + `${property}/${keyed}/${campaign}`, + states, + { fixedHistories: examples.length }, + ) + let index = 0 + const fidelity = createOracleFailureFidelity() + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(clientWindowArbitrary, async (history) => { + reach.beginHistory({ fixed: index++ < examples.length }) + await fidelity.run(history, () => + checkClientWindowHistory(history, keyed, reach), + ) + }), + campaign === `fixed` + ? { + numRuns: oracleRuns(6) + examples.length, + seed: 2345, + examples, + } + : oraclePropertyOptions(6, property, { examples }), + ), + ) + reach.assertReached() + } finally { + console.info( + `Svelte client window reach ${keyed}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }, 30_000) + } +}) +declareOracle({ check: [checkClientWindowHistory] }) + +/** An unchanged explicit client key retains the authored input, including after + * a rejected key read. Recovery must not replay a prior declarative move over + * an imperative window change. The shared page model judges that retained view. + * Changing the key instead admits the proposed query. Body/deps proposals + * behind an equal key intentionally retain the authored plan. + */ +const recoveryArbitrary = fc.record({ + width: fc.integer({ min: 1, max: 3 }), + offset: fc.integer({ min: 1, max: 3 }), + imperative: fc.integer({ min: 4, max: 6 }), +}) +type RecoveryHistory = { + width: number + offset: number + imperative: number + changeKey: boolean + proposal: `body` | `deps` +} +// Cross both proposal paths with key equality in every generated family. +// Separate marginal reaches could otherwise miss same-key dependency recovery. +function recoveryHistories( + values: Omit, +): Array { + return ([`body`, `deps`] as const).flatMap((proposal) => + [false, true].map((changeKey) => ({ ...values, proposal, changeKey })), + ) +} +const recoveryStates = [ + `committed move`, + `imperative move`, + `rejected read`, + `same-key recovery`, + `changed-key recovery`, + `body`, + `deps`, + `same-key body`, + `same-key deps`, + `changed-key body`, + `changed-key deps`, +] as const +async function checkRecovery( + history: RecoveryHistory, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + let dispose: (() => void) | undefined + lifetime.defer(() => dispose?.()) + const client = new DbClient() + lifetime.defer(() => client.cleanup()) + await lifetime.run(async () => { + const rows: Array = Array.from({ length: 12 }, (_, rank) => ({ + id: String(rank), + rank, + })) + const source = createCollection({ + getKey: (row: WindowRow) => row.id, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (const value of rows) + write({ type: `insert`, value: { ...value } }) + commit() + markReady() + }, + }, + }) + lifetime.defer(() => source.cleanup({ dependents: true })) + let offset = $state(0) + let key = $state(0) + let failed = $state(false) + let dep = $state(0) + const failure = new Error(`key read rejected`) + let result!: { + data: unknown + state: ReadonlyMap + collection: { utils: object } + } + dispose = $effect.root(() => { + result = useLiveQuery( + { + client, + queryKey: () => { + if (failed) throw failure + return [`recovery`, key] + }, + query: (q) => + q + .from({ row: source }) + .orderBy(({ row }) => row.rank) + .limit(history.width) + .offset(offset + dep) + .select(({ row }) => ({ id: row.id, rank: row.rank })), + }, + [() => dep], + ) + }) + const settle = async () => { + await Promise.resolve() + flushSync() + await new Promise((resolve) => setTimeout(resolve, 0)) + flushSync() + } + flushSync() + await settle() + const first = result.collection + offset = history.offset + key = 1 + flushSync() + await settle() + const expected = (page: number) => + windowRows( + rows, + { + offset: page, + limit: history.width, + minimum: -1, + descending: false, + nestedLimit: rows.length, + }, + false, + ) + const compare = (page: number, cut: string) => { + checkWindowObservation( + { rows: result.data, collection: result.collection }, + { rows: expected(page), previousCollection: first, retain: true }, + cut, + ) + checkWindowState(result.state, expected(page), cut) + } + compare(history.offset, `Svelte committed declarative move`) + reach.reach(`committed move`) + await (result.collection.utils as LiveQueryCollectionUtils).setWindow({ + offset: history.imperative, + limit: history.width, + }) + await settle() + compare(history.imperative, `imperative window before rejected input`) + reach.reach(`imperative move`) + failed = true + // Read the rejected lazy value twice, then run the throwing native effect. + expect(() => result.data).toThrow(failure) + expect(() => result.collection).toThrow(failure) + expect(() => flushSync()).toThrow(failure) + reach.reach(`rejected read`) + failed = false + if (history.proposal === `body`) offset = 9 + else dep = 9 - history.offset + if (history.changeKey) key = 2 + flushSync() + await settle() + compare(history.changeKey ? 9 : history.imperative, `recovered client key`) + reach.reach( + history.changeKey ? `changed-key recovery` : `same-key recovery`, + ) + reach.reach(history.proposal) + reach.reach( + `${history.changeKey ? `changed-key` : `same-key`} ${history.proposal}`, + ) + }) +} +for (const campaign of [`fixed`, `fresh`] as const) { + const property = `svelte.client-window-recovery` + const test = + campaign === `fixed` ? oracleReplayTest(it) : oracleReplayTest(it, property) + test(`client key recovery/${campaign}`, async () => { + const reach = createOracleReach(`${property}/${campaign}`, recoveryStates) + const fidelity = createOracleFailureFidelity() + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(recoveryArbitrary, async (history) => { + for (const member of recoveryHistories(history)) { + reach.beginHistory() + await fidelity.run(member, () => checkRecovery(member, reach)) + } + }), + campaign === `fixed` + ? { numRuns: oracleRuns(24), seed: 2348 } + : oraclePropertyOptions(24, property), + ), + ) + reach.assertReached() + } finally { + console.info( + `Svelte recovery reach/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }) +} +declareOracle({ + grammar: [recoveryArbitrary, recoveryHistories], + check: [checkRecovery], +}) diff --git a/packages/svelte-db/tests/conformance.svelte.test.ts b/packages/svelte-db/tests/conformance.svelte.test.ts index fa7143ec6f..b2d3e81fec 100644 --- a/packages/svelte-db/tests/conformance.svelte.test.ts +++ b/packages/svelte-db/tests/conformance.svelte.test.ts @@ -11,6 +11,8 @@ import { BTreeIndex, BasicIndex, + DbClient, + Query, coalesce, count, createCollection, @@ -22,13 +24,14 @@ import { sum, } from '@tanstack/db' import { flushSync } from 'svelte' -import { expect, it } from 'vitest' +import { afterAll, expect, it } from 'vitest' import { mockSyncCollectionOptions, mockSyncCollectionOptionsNoInitialState, } from '../../db/tests/utils' import { useLiveQuery } from '../src/useLiveQuery.svelte.js' import { runSuite } from '../../db/tests/conformance/suite' +import { registerWindowHistoryOracle } from '../../db/tests/conformance/window-history-oracle' import { createPublicationLog } from '../../db/tests/conformance/publication-log' import { makeLaggingSource } from '../../db/tests/conformance/lagging-source' import { makeSyncSource } from '../../db/tests/conformance/sync-source' @@ -193,42 +196,68 @@ function mountDisabled() { return makeHandle(() => query, dispose) } -function mountControllable

( +const mountControllableWith = (withClient: boolean | DbClient = false) => + function mountControllable

( + build: (q: any, param: P) => any, + initial: P, + opts?: { pool?: PooledLiveQueries }, + ): ControllableHandle

{ + const client = + withClient instanceof DbClient + ? withClient + : withClient + ? new DbClient() + : undefined + let param = $state(initial) + let query: any + const published = createPublicationLog() + const dispose = $effect.root(() => { + query = client + ? useLiveQuery(() => { + const input = build(new Query(), param) + return input instanceof Query + ? { query: input, client } + : { ...input, client } + }) + : opts?.pool !== undefined + ? useLiveQuery( + { query: (q: any) => build(q, param), pool: opts.pool } as any, + [() => param], + ) + : useLiveQuery((q: any) => build(q, param), [() => param]) + // Svelte renders the value each pre-effect run reads. + $effect.pre(() => { + published.record(query.data, query.state) + }) + }) + const handle = makeHandle(() => query, dispose) + return { + ...handle, + publications: published.values, + statePublications: published.stateValues, + async setParam(next: P, observe?: () => void) { + param = next + // The first paint after the change: the framework has flushed its + // reactive work, but no later task has run. + flushSync() + observe?.() + published.record(query.data, query.state) + await settle() + }, + } + } + +const pageClient = new DbClient() +afterAll(() => pageClient.cleanup()) +function mountKeyedControllable

( build: (q: any, param: P) => any, + key: (param: P) => ReadonlyArray | undefined, initial: P, - opts?: { pool?: PooledLiveQueries }, ): ControllableHandle

{ - let param = $state(initial) - let query: any - const published = createPublicationLog() - const dispose = $effect.root(() => { - query = - opts?.pool !== undefined - ? useLiveQuery( - { query: (q: any) => build(q, param), pool: opts.pool } as any, - [() => param], - ) - : useLiveQuery((q: any) => build(q, param), [() => param]) - // Svelte renders the value each pre-effect run reads. - $effect.pre(() => { - published.record(query.data, query.state) - }) - }) - const handle = makeHandle(() => query, dispose) - return { - ...handle, - publications: published.values, - statePublications: published.stateValues, - async setParam(next: P, observe?: () => void) { - param = next - // The first paint after the change: the framework has flushed its - // reactive work, but no later task has run. - flushSync() - observe?.() - published.record(query.data, query.state) - await settle() - }, - } + return mountControllableWith(pageClient)( + (q, param: P) => ({ query: build(q, param), queryKey: key(param) }), + initial, + ) } const svelteDriver: LiveQueryDriver = { @@ -255,7 +284,7 @@ const svelteDriver: LiveQueryDriver = { makePrecreated, makeErrorSource, mount, - mountControllable, + mountControllable: mountControllableWith(), mountCollection, mountConfig, mountDisabled, @@ -263,6 +292,13 @@ const svelteDriver: LiveQueryDriver = { features: { serverSnapshot: false, suspense: false, pooledEqFilters: true }, } +registerWindowHistoryOracle({ + ...svelteDriver, + name: `svelte (DbClient)`, + comparesQueryIdentity: true, + mountControllable: mountControllableWith(pageClient), + mountKeyedControllable, +}) runSuite(svelteDriver) it(`preserves raw result types through the actual driver reader`, () => { const raw: Record = {