diff --git a/.changeset/fix-adapter-page-windows.md b/.changeset/fix-adapter-page-windows.md new file mode 100644 index 0000000000..430b1450fd --- /dev/null +++ b/.changeset/fix-adapter-page-windows.md @@ -0,0 +1,12 @@ +--- +'@tanstack/db': patch +'@tanstack/db-ivm': patch +'@tanstack/react-db': patch +'@tanstack/preact-db': patch +'@tanstack/vue-db': patch +'@tanstack/solid-db': patch +'@tanstack/angular-db': patch +'@tanstack/svelte-db': patch +--- + +Keep adapter-owned live queries and their source subscriptions when only the ordered query's finite `.limit()` or `.offset()` changes. Preserve exact requested bounds when moving page windows, and avoid React 18 layout-effect warnings during server rendering. diff --git a/docs/contributing/glossary.md b/docs/contributing/glossary.md index 61eb31cd5c..abe5dedc3b 100644 --- a/docs/contributing/glossary.md +++ b/docs/contributing/glossary.md @@ -20,7 +20,7 @@ production queues, caches, or semantic helpers merely to share their names. | source Collection | A Collection read by a query or adapter. | Source relation when the value is a public Collection. | | live-query Collection | A Collection whose rows are produced by a live query. | Query, observer, or result set. | | framework binding | What one mounted framework hook or injector keeps between updates: its compiled live query or window binding, its observer or controller, and its source Collection ID claims. | Subscription, observer, or live-query Collection. | -| query identity | The value that decides whether a framework binding compiles its query input again: a nonempty `queryKey`, or the identity derived from the query IR. A `DbClient` matches preloaded and hydrated queries by its hash. React and Preact compare it on every render; reactive adapters compile again when their reads change and compare none. | Query hash when the value is not hashed, or dependency list. | +| query identity | The value that invalidates a framework binding's authored query input: a nonempty `queryKey`, or the identity derived from the query IR. A `DbClient` matches preloaded and hydrated queries by its hash. React and Preact compare it on every render; reactive adapters resolve again when their reads change and compare none. An adapter-owned finite outer-window change can retain its compiled plan and Collection despite a changed result identity. | Query hash when the value is not hashed, or dependency list. | | source Collection ID claim | A framework binding's record of which source Collection object each Collection ID named, per `DbClient` scope, kept while the binding is mounted. A binding that compares query identity rejects a different object under a claimed ID. | Collection registry or source identity. | | window binding | The infinite-query state a framework binding keeps between updates: the input, its identity dependencies, the page shape, the window collection, and its controller. `bindLiveQueryWindow` decides whether to keep it, replace its controller, or bind a new window collection. | Window controller or live-query Collection. | | pooled live query | A live query on one source Collection whose `where` has at least one `eq(field, literal)` conjunct and otherwise reads only the row, optionally ordered by the row's own fields without a limit, served from an equality partition; its live-query Collection is built only when read. | Cached query or shared live-query Collection. | diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 19b5e0cee8..ec93ea152c 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -1393,6 +1393,21 @@ is not browser evidence. `hydration.svelte.test.ts` separately receives normalized transport and normal warm-browser initialization through its native scheduler. +The [React SSR diagnostic receiver](../../packages/react-db/tests/ssr-effects-oracle.test.ts) +extends that preparation grammar and judgment with real matched React/ReactDOM +18.3.1 and 19.2.4 development server renderers. Separate paths cover provider +and explicit-client ordinary hooks, completed non-streaming Suspense through +both client forms, standalone builders and supplied Collections. A disabled +input replays the same diagnostic comparison. Each checks return and the raw +React client-layout diagnostic after server render and cleanup; ordinary +descriptor paths also compare admission, selected rows and status, while +prepared Suspense compares rows. Standalone/supplied paths make no hydration +claim. Fixed/fresh campaigns retain per-runtime/path reach and direct replay; +this does not establish streaming Suspense, actual browser hydration, arbitrary +provider delivery or every React version. Client commit and abandonment remain +with the window receiving owner. [SSR effect evidence](oracle-reviews/2026-10-30-react-ssr-effects.md) +records pre-edit RED, repaired GREEN and hostile rejection. + The shared admission owner also defines runtime ready/error outcomes, browser explicit preparation and defined/undefined causes. Its receiving drivers are `packages/react-db/tests/ssr-runtime-error-handoff-oracle.test.tsx` and @@ -3649,6 +3664,21 @@ target execution, not reproduction of a particular bug. Local IVM and offline properties have separate environment variables; inspect their test headers. Do not assume the core multiplier reaches them. +The adapter-window properties have named replay routes in +`packages/db/tests/oracle-replay-manifest.ts`. Each shared history property +includes its production driver: `framework..window-history`, +`framework..pending-window-history`, or +`framework..publication-window-history`. The drivers are `react`, +`react-dependency-list`, `preact`, `vue`, `solid`, `angular`, and `svelte`. +Each route selects its own conformance file and names the shared model module +as its source. React and Preact render witnesses, all twelve React SSR +runtime/path cells, and the IVM numeric-window lane retain their own routes. +Selectors choose only the fresh or random campaign. The earlier three shared +`framework.*` names are retired because they lost the failing driver's identity. +The [replay review](oracle-reviews/2026-10-30-adapter-page-window-replay.md) +records the 21 driver/history witnesses and correction of the earlier canonical +React routing claim in the preparation record. + The Node expression-index oracle runs matching fixed-seed and seedless-random campaigns by default. A path replay of its property selects only the requested seed and shrink path: @@ -6273,3 +6303,16 @@ useful extension: the hash identity oracle owns those semantic distinctions, but its helper-level evidence does not by itself exercise distinct storage. The separate Index structural-collision obligation above remains open. See the appended [review evidence](oracle-reviews/2026-10-30-distinct-collisions.md). + + +### 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 [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. + +The existing [top-K support-window owner](../../packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts) also has a numeric-window lane for safe-integer maximum offsets and widths, exact omitted-bound inheritance on partial moves, and required demand messages when installed ranges are empty. It permits additional messages. It observes one-group settled operator rows, size and demand signals; real framework source growth belongs to the shared receiving owner above. [Numeric evidence](oracle-reviews/2026-10-30-topk-numeric-windows.md) records RED, GREEN, reach, replay and mutants. diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-window-replay-manifest.json b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-window-replay-manifest.json new file mode 100644 index 0000000000..4ae0fa29af --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-window-replay-manifest.json @@ -0,0 +1,1337 @@ +{ + "reviewed_parent": "cc058b802ed6921f692f234399437c5a4e663991", + "review_id": 5718063990, + "source_sha256": { + "packages/db/tests/conformance/window-history-oracle.ts": "8643275378fdbd648b095fd6088b78b07ee97794d92ce059b24f99bc3d5a80ca", + "packages/db/tests/oracle-config.ts": "25adaf6ef56c1cdcb75f8b4b240974e2e6df698e3331e5deb2f297c62f76b478", + "packages/db/tests/oracle-replay-manifest.ts": "41dcfce6a0022435d29b4b9232d93c27a0acfa5a2596993df079c0f4ca95c70e", + "packages/db/tests/oracle-replay.test.ts": "2c9de0c64aa62f6941c1efc878cc979ac8997df8760cf978b1041e291bf86a06" + }, + "routes": [ + { + "property": "framework.react.window-history", + "status": 0, + "owner": "packages/react-db/tests/conformance.test.tsx", + "executions": 8, + "log_sha256": "7383741fde5159c580564352b6933dfc43f0dfbef690eb27f90e664488593486", + "seed": 2291, + "path": "0", + "num_runs": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "failed": [ + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + { + "property": "framework.react.pending-window-history", + "status": 0, + "owner": "packages/react-db/tests/conformance.test.tsx", + "executions": 1, + "log_sha256": "38d1d961bc39be8e4be01f73e9483ef34ac5b67d091bd7e8963e6cb581c6e70d", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.react.publication-window-history", + "status": 0, + "owner": "packages/react-db/tests/conformance.test.tsx", + "executions": 1, + "log_sha256": "42e65e0b8b75679f9eed16aa6fed08568c68248eeac4d27343a33a01ec52d0e9", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.react-dependency-list.window-history", + "status": 0, + "owner": "packages/react-db/tests/conformance.test.tsx", + "executions": 8, + "log_sha256": "f9d96c3d797b441bc3a3c9ec5afde37b7a2c3f78fb7134984da19dcd41630eff", + "seed": 2291, + "path": "0", + "num_runs": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "failed": [ + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + { + "property": "framework.react-dependency-list.pending-window-history", + "status": 0, + "owner": "packages/react-db/tests/conformance.test.tsx", + "executions": 1, + "log_sha256": "89030f3b92c5eb3f9a92fe620ab5b0c5ea995432e26a01c59db44ba23950d358", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.react-dependency-list.publication-window-history", + "status": 0, + "owner": "packages/react-db/tests/conformance.test.tsx", + "executions": 1, + "log_sha256": "7f4b313a1ce1097c5a8c1af606f90fb17644c5e1c8db1d9151c98234ce13ae04", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.preact.window-history", + "status": 0, + "owner": "packages/preact-db/tests/conformance.test.ts", + "executions": 8, + "log_sha256": "91d8b0e6819bbab86612ea6e27d94eedbc0de691a627e5e68f16a7bd88861cf0", + "seed": 2291, + "path": "0", + "num_runs": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "failed": [ + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + { + "property": "framework.preact.pending-window-history", + "status": 0, + "owner": "packages/preact-db/tests/conformance.test.ts", + "executions": 1, + "log_sha256": "08a2f7c9605651f27ddf2104cc8b7d9ebb00813c1ab0d19d02a08dbb2d469f4a", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.preact.publication-window-history", + "status": 0, + "owner": "packages/preact-db/tests/conformance.test.ts", + "executions": 1, + "log_sha256": "6fc526fe33095525942de865e04b360d83425589c9100bd9a23356d5f0608873", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.vue.window-history", + "status": 0, + "owner": "packages/vue-db/tests/conformance.test.ts", + "executions": 2, + "log_sha256": "9b3f739dd067f7b987eb041c39109523d4e7b73b61a284a410254345a1904383", + "seed": 2291, + "path": "0", + "num_runs": [ + 1, + 1 + ], + "failed": [ + false, + false + ] + }, + { + "property": "framework.vue.pending-window-history", + "status": 0, + "owner": "packages/vue-db/tests/conformance.test.ts", + "executions": 1, + "log_sha256": "9e539fd0c48b1db9fa6454442b44fd88be95688c00a2dff4890bde8fb0704388", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.vue.publication-window-history", + "status": 0, + "owner": "packages/vue-db/tests/conformance.test.ts", + "executions": 1, + "log_sha256": "f4b1b8e32290d4542ca78ac0993fff85db5df19c929ebd7a9f08f66233b4313b", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.solid.window-history", + "status": 0, + "owner": "packages/solid-db/tests/conformance.test.tsx", + "executions": 2, + "log_sha256": "581043fd1f97f5acc03c249257f2b691c7fe67dc4eb70991726e60f029214dd2", + "seed": 2291, + "path": "0", + "num_runs": [ + 1, + 1 + ], + "failed": [ + false, + false + ] + }, + { + "property": "framework.solid.pending-window-history", + "status": 0, + "owner": "packages/solid-db/tests/conformance.test.tsx", + "executions": 1, + "log_sha256": "ea8f4cf84fb840664796905542aa3904cc043b99573339fe817b23e44edca93a", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.solid.publication-window-history", + "status": 0, + "owner": "packages/solid-db/tests/conformance.test.tsx", + "executions": 1, + "log_sha256": "df0e25ab2a1a0c7052dd6e6c3056a13d476df50aed26be09fe47dc3b9fa08eca", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.angular.window-history", + "status": 0, + "owner": "packages/angular-db/tests/conformance.test.ts", + "executions": 2, + "log_sha256": "b91a8939774b86cc78069e017a57496767972d376d26eaf329f33aafc5522989", + "seed": 2291, + "path": "0", + "num_runs": [ + 1, + 1 + ], + "failed": [ + false, + false + ] + }, + { + "property": "framework.angular.pending-window-history", + "status": 0, + "owner": "packages/angular-db/tests/conformance.test.ts", + "executions": 1, + "log_sha256": "6152d92a93b35d1c8edb5696f9e6a5aa80f1b6740ad975222b2ad3f430c28463", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.angular.publication-window-history", + "status": 0, + "owner": "packages/angular-db/tests/conformance.test.ts", + "executions": 1, + "log_sha256": "578b71ea47e8c903a5ac026920ff60d91de87876135ed34713bf9810185e1cf9", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.svelte.window-history", + "status": 0, + "owner": "packages/svelte-db/tests/conformance.svelte.test.ts", + "executions": 2, + "log_sha256": "be74fa20129e36cb54ae703d4aa809d4da4beacc68ccb1e98c483be872057383", + "seed": 2291, + "path": "0", + "num_runs": [ + 1, + 1 + ], + "failed": [ + false, + false + ] + }, + { + "property": "framework.svelte.pending-window-history", + "status": 0, + "owner": "packages/svelte-db/tests/conformance.svelte.test.ts", + "executions": 1, + "log_sha256": "a288bc4550c6f8743fada715e203062cd2dcd27753e0fac6bb8fd65e020f84c3", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + }, + { + "property": "framework.svelte.publication-window-history", + "status": 0, + "owner": "packages/svelte-db/tests/conformance.svelte.test.ts", + "executions": 1, + "log_sha256": "0b591e3cf58d71f1521e07ad380d01c0ee4fc07b992d2ce406f68c492f61889d", + "seed": 2291, + "path": "0", + "num_runs": [ + 1 + ], + "failed": [ + false + ] + } + ], + "protocol_suite": { + "passed": 63 + }, + "red": { + "missing_exact_driver_owners": 21 + }, + "mutant": { + "react_for_solid_owner_assertion_failures": 3 + }, + "normal_campaigns": [ + { + "driver": "react", + "status": 0, + "passed": 44, + "log_sha256": "35d1b8ddd9baa50c17bb27ec62f8e0afe064fb6719a1d7532ddeb3a08ff833cf", + "reach": { + "pending reach react/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach react/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach react/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach react/fresh": { + "publication callback entered": 9, + "offset requested in callback": 5, + "limit requested in callback": 6, + "callback update settled": 9 + }, + "window reach react/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/changing/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/changing/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/stable/direct/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach react/stable/direct/fresh": { + "source write": 13, + "stable key": 13 + }, + "window reach react/opaque/direct/fixed": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react/opaque/direct/fresh": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/changing/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/changing/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/stable/nested/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach react/stable/nested/fresh": { + "source write": 9, + "stable key": 13 + }, + "window reach react/opaque/nested/fixed": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react/opaque/nested/fresh": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "pending reach react (dependency list)/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach react (dependency list)/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach react (dependency list)/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach react (dependency list)/fresh": { + "publication callback entered": 9, + "offset requested in callback": 7, + "limit requested in callback": 4, + "callback update settled": 9 + }, + "window reach react (dependency list)/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/changing/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/changing/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/stable/direct/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach react (dependency list)/stable/direct/fresh": { + "source write": 13, + "stable key": 13 + }, + "window reach react (dependency list)/opaque/direct/fixed": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react (dependency list)/opaque/direct/fresh": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react (dependency list)/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/changing/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/changing/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/stable/nested/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach react (dependency list)/stable/nested/fresh": { + "source write": 11, + "stable key": 13 + }, + "window reach react (dependency list)/opaque/nested/fixed": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react (dependency list)/opaque/nested/fresh": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + } + }, + { + "driver": "preact", + "status": 0, + "passed": 22, + "log_sha256": "be11270d18e473ab2caebdd494398f087e162b2ab553c8f8d09951a6fc290066", + "reach": { + "pending reach preact/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach preact/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach preact/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach preact/fresh": { + "publication callback entered": 9, + "offset requested in callback": 5, + "limit requested in callback": 6, + "callback update settled": 9 + }, + "window reach preact/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/changing/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/changing/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/stable/direct/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach preact/stable/direct/fresh": { + "source write": 11, + "stable key": 13 + }, + "window reach preact/opaque/direct/fixed": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach preact/opaque/direct/fresh": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach preact/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/changing/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/changing/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/stable/nested/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach preact/stable/nested/fresh": { + "source write": 10, + "stable key": 13 + }, + "window reach preact/opaque/nested/fixed": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach preact/opaque/nested/fresh": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + } + }, + { + "driver": "vue", + "status": 0, + "passed": 10, + "log_sha256": "a3effa927fc2cd9a17a0f5363411cef22bee877bedc3d8d1b5730f1116f45ada", + "reach": { + "pending reach vue/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach vue/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach vue/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach vue/fresh": { + "publication callback entered": 9, + "offset requested in callback": 7, + "limit requested in callback": 4, + "callback update settled": 9 + }, + "window reach vue/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach vue/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach vue/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach vue/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + } + }, + { + "driver": "solid", + "status": 0, + "passed": 10, + "log_sha256": "7bb5be2df9d0030b556ab4b94137fad3bc2836ece4cebce5d6e6601f63719f42", + "reach": { + "pending reach solid/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach solid/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach solid/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach solid/fresh": { + "publication callback entered": 9, + "offset requested in callback": 7, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "window reach solid/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach solid/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach solid/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach solid/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + } + }, + { + "driver": "angular", + "status": 0, + "passed": 10, + "log_sha256": "bbae6299216f62e690b419330168d8aaf210dd8bfee9d6b084422e5c775b602a", + "reach": { + "pending reach angular/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach angular/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach angular/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach angular/fresh": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "window reach angular/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach angular/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach angular/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach angular/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + } + }, + { + "driver": "svelte", + "status": 0, + "passed": 10, + "log_sha256": "bc16f9eb3a60045bbcb9f3f030abbd336d8af46bc2b0772b37f7369b0688e30b", + "reach": { + "pending reach svelte/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach svelte/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach svelte/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach svelte/fresh": { + "publication callback entered": 9, + "offset requested in callback": 7, + "limit requested in callback": 3, + "callback update settled": 9 + }, + "window reach svelte/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach svelte/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach svelte/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach svelte/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + } + } + ], + "normal_passed": 106 +} diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-window-replay.md b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-window-replay.md new file mode 100644 index 0000000000..0da886647f --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-window-replay.md @@ -0,0 +1,138 @@ +# Preserve the framework driver during window replay + +CodeRabbit review `5718063990` examined parent +`cc058b802ed6921f692f234399437c5a4e663991` and supplied two valid minor +findings. Inline `4428012904` identified an overbroad unbounded-transition +sentence. Inline `4428012910` identified a replay-integrity defect. +The [manifest](2026-10-30-adapter-page-window-replay-manifest.json) records +source hashes, exact owners, replay coordinates, execution counts and log hashes. +This repair changes test machinery and evidence prose, with no product change. + +## Law and experiment + +ORC-007 requires a replay to run the original property, recorder and comparison. +The guarded replay contract also requires its exact production owner to execute. +Framework scheduling belongs to that path: a shared independent model does not +make React a substitute for a failing Solid, Vue or other framework driver. + +The earlier three `framework.*` property names erased the driver distinction. +The manifest assigned each to React. Its green checks covered registry +membership and source names, while allowing this host substitution. +The previous preparation record's canonical-React claim was therefore too broad. +Its eighteen successful commands remain historical target-execution evidence; +they do not establish replay of failures in the other frameworks. + +Before repairing routing, the protocol suite gained an independent finite +matrix of seven drivers and three history families. The drivers include all six +adapters and the separate React dependency-list entry point. Every cell requires +its exact owner. The unchanged routing failed all **21 comparisons**. +The command from `packages/db` was: + +```sh +pnpm exec vitest run tests/oracle-replay.test.ts \ + -t 'keeps the originating driver' --maxWorkers=1 --coverage.enabled=false +``` + +Each property now includes its driver, for example +`framework.solid.pending-window-history`. All three families use the scoped name +for registration and replay options. Each manifest entry selects the actual +conformance file and the shared model source. Fixed examples, run budgets, +generators, recorders, comparisons and reach declarations remain unchanged. +The two React entry points also stand aside during each other's path replay. + +The full protocol suite passes **63 tests**. All **21 real guarded commands** +pass with seed `2291`, path `0`, the exact property and owner, nonzero execution +counts and no failed witness. Ordinary-window replay executes eight comparisons +for each React/Preact driver and two for each reactive driver. Pending and +publication replay execute one each. These are **46 completed comparisons**. +For example, run this command from `packages/solid-db`: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=framework.solid.pending-window-history \ +TANSTACK_DB_ORACLE_SEED=2291 TANSTACK_DB_ORACLE_PATH=0 \ +node --import tsx ../db/tests/oracle-replay.ts tests/conformance.test.tsx \ + --maxWorkers=1 --coverage.enabled=false --typecheck.enabled=false +``` + +A hostile manifest that substitutes React for all three Solid owners fails +**three exact-owner assertions**. These are intended comparison failures, +not setup errors or timeouts. A permanent negative child-process check also +requests the Solid property through React's actual conformance file. It rejects +that invocation with `executions: 0` and `oracle replay target never executed`. +Existing protocol checks retain seed/path, nonzero-run, filter, property-error +and cleanup-error enforcement. + +Normal fixed and seedless-fresh campaigns also pass **106 tests**: React 44, +Preact 22 and ten each for Vue, Solid, Angular and Svelte. The manifest retains +all 92 per-lane reach dictionaries and their counts. Every declared state was +reached. Core TypeScript, changed-file lint and whitespace checks pass. The +first scratch campaign probe exceeded its output buffer because warnings filled +it; rerunning with file-backed output produced these completed results. That +probe error supplies no product RED evidence. + +## Documentation correction and review accounting + +The resolver requires an old explicit limit but validates finiteness only on +the target. Thus explicit `.limit(Infinity)` can retain a Collection when moving +to a finite safe-integer target. The original evidence now states this exception. +Its finite-window guarantee remains unchanged. Source inspection suffices for +this documentation mismatch; this review claims no Infinity receiving witness. + +The lossless scratch ledger accounts for **26 source occurrences**: two +`fixed-now` findings, fifteen duplicates and nine rejected optional +recommendations. The raw body, both inline comments, AI-prompt repetitions, +mutable summary, risk assessment, warnings, finishing suggestions and tips +were reread after repair. No finding remains confirmed-open or deferred. + +| Items | Judgment and destination | +| --- | --- | +| CR02, CR03 | Both actionable findings fixed as described above. | +| CR01, CR05–CR14, CR17–CR18, CR21–CR22 | Duplicate occurrences preserved and linked to their canonical findings or recommendation. | +| CR04 | Extra review CLI is optional; existing skills, executable checks and fresh independent review supply verification. | +| CR15 | Real-provider limits remain explicit. Issue #2118 concerns sync-adapter conformance and excludes framework render timing. Its related portfolio idea survives at that issue. | +| CR16 | Suggested human reviewers supply no technical finding; no unsolicited review request was sent. | +| CR19 | Low merge risk is a reviewer judgment, not permission to leave replay integrity broken. Both findings were repaired. | +| CR20 | The reported docstring percentage is unverified. No repository 80-percent threshold or concrete undocumented obligation was identified. Blanket generation is unnecessary; explanatory law and mechanism prose remain. | +| CR23 | Generic unit-test generation identifies no missing law. The primary protocol checks, real receiving commands and hostile controls cover the concrete gap. | +| CR24–CR26 | Bot autofix, social promotion and a bot help comment are optional workflow suggestions. They introduce no technical requirement. No such external action was taken. | + +Hire recommendation: **yes for this bounded review**. Both actionable findings +were accurate, low in scope and paired with suitable repairs. Static inspection +found a receiving-path distinction the earlier review missed. Generic tooling +and docstring suggestions add noise; the merge-risk label supplies no independent +readiness evidence. + +A fresh oracle reviewer inspected the repair, independently parsed all 21 +completed logs, and found no remaining source-level finding. It identified the +stale current coverage guidance, which this repair corrects. That review ran no +new tests and supplied no independent runtime result. + +## Evidence boundaries + +- **Law:** Preserve the original framework production driver, property, + seed/path and comparison during direct replay (ORC-007 and guarded replay). +- **Reach:** Every cell of the finite seven-driver × three-family routing + grammar completed its real guarded comparison. Existing product reach + declarations and all history axes remain in place. +- **RED:** The extended owner matrix failed all 21 comparisons on unchanged + parent routing before the identity repair. +- **GREEN:** The complete protocol suite passes 63 tests and all 21 exact-owner + guarded commands pass, with the execution counts recorded in the manifest. +- **Mutant:** React-for-Solid routing fails three owner comparisons; the real + wrong-driver invocation is rejected for nonexecution. +- **Limits:** This evidence establishes routing for these registered drivers and + families. It does not reproduce an arbitrary provider schedule or claim a + new product law. Seed/path zero is an execution witness, not a captured + product counterexample. The original R2-5/G1 oracle-first process gap remains + disclosed for maintainer disposition. + +ORC-001–007 apply through the law, independent owner matrix, finite grammar, +real drivers, negative controls and direct replays above. ORC-008 requires no +new state model; ORC-009 retains existing vocabulary. ORC-010's protocol +regressions remain green. No shared semantic fault requires an ORC-011 second +formulation. This versioned record supplies ORC-012 evidence. Distinct drivers +and the wrong-driver control distinguish the conditional replay rule for +ORC-013. No provider handoff is claimed under ORC-014. Existing product reach +is retained under ORC-015; the added routing matrix is bounded enumeration. +The ordinary protocol test file supplies supplemental checks rather than placing +unit tests inside the shared generated-history owner (ORC-016). diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-manifest.json b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-manifest.json new file mode 100644 index 0000000000..31ade162c0 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-manifest.json @@ -0,0 +1,143 @@ +{ + "base_head": "c351fc04d7f70dc59bf92ecbed310f899b9511cd", + "branch": "codex/evaluate-2291-window", + "sources_sha256": { + "packages/angular-db/src/index.ts": "b856d0a98c47071691583a370edffc25764d529fa33fecc28c383ed54deb3fe8", + "packages/db-ivm/src/operators/topKArray.ts": "ff21d0f944043db4d28c9be7c429594d668763f634443801a8dc65188ee8d9f1", + "packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts": "1ef076cdca0976323847511681d6d4c75a25bd2c345377a8df4b1d52a9625577", + "packages/db/src/live-query-options.ts": "0a049edd35b0b2f8c803aacc8457df9ff619c2f356f6a234660674487d03a368", + "packages/db/tests/conformance/contract.ts": "b6f345576497c94aeb2720b11ff188a55f27b74905ae2c69ec42e9421f232b41", + "packages/db/tests/conformance/lagging-source.ts": "19feb3b6f87e89fd60cf22605f81c755d94e43db39a11a19e1d49f42accc1097", + "packages/db/tests/conformance/suite.ts": "be7929e6d0d0838f4214c7af473c77b91d32741b347d91d20041a5f8c790ebd2", + "packages/db/tests/conformance/window-history-oracle.ts": "cff4381d7c38256e91b32c479d983a2e73bb03792dcbf9add09396cad3f58c65", + "packages/db/tests/oracle-config.ts": "3de8b1bbfdf9d99ed9c85f52f24411e49da5b9cdc99bee7e82912da4cdeac92a", + "packages/preact-db/src/useLiveQuery.ts": "fe381f9c87cfdd5b57e9cc9ff77a6d1dbfa1fc84191c69fa7247fa9953223f0d", + "packages/preact-db/tests/window-render-oracle.test.tsx": "b95c5ee25a56c94b3793fd0793adee878d06e0999f6f2b2f113f6a0674cea8fe", + "packages/react-db/src/useLiveQuery.ts": "46684d6f8365237d49656fe0cbfa87439178bf9c3edfa2468f56dbf26d16cb66", + "packages/react-db/tests/window-render-oracle.test.tsx": "f44f912565e6df40a7d2ccdb61beb102369fbd4a98c7fb11adf76c4d6c1cf41b", + "packages/solid-db/src/useLiveQuery.ts": "8e414683931992f72d3208d1f5952d2f33625eac7b0b88aaf5755a210d19ded2", + "packages/svelte-db/src/useLiveQuery.svelte.ts": "23e316c6515bcbdd159121c9497911075ed3d54670a6d7b19a76089eef2212bc", + "packages/vue-db/src/useLiveQuery.ts": "5c4d910952e54b58002459b21d86c9ba6c25fcc67e7cce677e0c6b66b7aecc3c" + }, + "final_test_counts": { + "react": 711, + "preact": 126, + "vue": 109, + "solid": 70, + "angular": 64, + "svelte": 270, + "core": 799, + "topk": 172 + }, + "total_tests": 2321, + "logs_sha256": { + "verified-react-green.log": "c4ce8ea1cc90a3dbf30a7c8be1c717baf9bb22326d68d6be6d4ccb4fed0e4c90", + "verified-preact-green.log": "30f186f40a014be1a12f1ea435fc62752a7b228f95818bae76c840016a6454be", + "verified-vue-green.log": "7bf0f62af44dec0beb5182629e91c3b541085d21227fb30eb9698553d43f1b8d", + "verified-solid-green.log": "8185a230f54443212ff95f60e52501b0fd2b35d2cd48237fbced011e2a2b2e01", + "verified-angular-green.log": "b6e105a54f86243107f9ad8bbc73d6abb494538bf308e68370015874b669e357", + "verified-svelte-green.log": "5499e0a4e381b3eab4bc3413d7ab03593f2866bbefa1aaeed38ac411b0254a72", + "verified-core-green.log": "debd69a68143ae5e2678f83ab6439c757ab9526a43211115a30001b1c9ce9571", + "verified-topk-green.log": "ebd96a069a3cf8c640e9db917a37ec5fa57092990b3dbae6eac63ac753759f68" + }, + "supporting_evidence_sha256": { + "review-2291/large-growth-angular-red.log": "5f0e34a865b9457eedd605a91e3e2cab2094ced522608938007d787eecf26b26", + "review-2291/large-growth-preact-red.log": "b7ba200e6030cbb8c3149d58a0ddae942535dfd2a40dc95e6ab767482b4a8d1f", + "review-2291/large-growth-react-red.log": "48c029fd094e0851dc5a9d8af393808da944c5fde7f72fc9514f409bff29808b", + "review-2291/large-growth-red-results.json": "95f1b89b8b1f01a608f2cae43495b4520fabd254ad47f8632015e247e1de4e60", + "review-2291/large-growth-solid-red.log": "805ac5cb1cd91ec6bb7ee5d8855e13fe075856ca9ec9babec7ae7a6d737bb1ca", + "review-2291/large-growth-svelte-red.log": "fcfabfd516561daed88d5410c265bc3a209a9eee5ac70e29e11bae86ce470c13", + "review-2291/large-growth-vue-red.log": "d7de936a03d2f6f2441ca7b43097feb599b6fc91be52f4ba5d57ef987a10ea3e", + "review-2291/mutant-clipped-demand.log": "4650e4c553a3bf8bf8ff56a7260eb74d598e69aef638ef70843f6e9516937f56", + "review-2291/mutant-ignore-options.log": "3aa8291bba95d4bbee7fe48b5fa4bef47de3a371be2ed096d42b78696a742b5d", + "review-2291/mutant-initial-ready-only.log": "e0c976483c002d6266bb38992f302e95a2a9c14fc40eeafc5620326f48f8eb97", + "review-2291/mutant-preact-speculative.log": "be07916f9163640b02a273ac8705bfc46d8f1a71566082ac1f238793c78c693c", + "review-2291/mutant-react-speculative-once.log": "62e4da1726067b3d265eee4b27ac6adeb3b635a59b114ebd488c42d8d28d4faf", + "review-2291/mutant-rebuild.log": "4b2319585c4b0c8d77c5b1c0ebcaa23e131ec49d156616edfe59370eaa6b6e90", + "review-2291/mutant-render-move.log": "b947c70555e863b390fae54c638970f8600ba35b5385ab4c1273ad805cccf21e", + "review-2291/mutant-results.json": "9a6b4cf09a67b9cf2be91a79df99aa2336872eab7db720cf8938b0fc673e56bd", + "review-2291/mutant-rounded-width.log": "eb252fff379981e21934f1f317af0953ac7ffac0b5d47cecc758c9025f95cbe9", + "review-2291/mutant-source-id.log": "7f459e1b5b943c1f3b2e2f7ad752e60af55a09dff2958528cff40bdea33fb415", + "review-2291/mutant-stored-clamp.log": "ebf2f491ab49d7281ab9e3f8286e57b6415f4d60b5bc14ed9801a2dbde99ad21", + "review-2291/mutant-synchronous-publication.log": "917734d04d70a2280e0ec422b062106a2022afe56539372299ec096ffdd41e9f", + "review-2291/pending-final-angular-red.log": "5b3a8f512327f6dff81d7ef8fc4cfd719dd8f5445446c865450e1dcc5752fcb1", + "review-2291/pending-final-preact-red.log": "2d98709912a4c92d289fb0ac84334a06b9488b39a22f2ced83667f907b9b9b57", + "review-2291/pending-final-react-red.log": "e571c76acc94a4d2805b5ea9acf569e5a76e1cffbc6a7edfb901ba7d1aa2c241", + "review-2291/pending-final-solid-red.log": "2033eedeb898aae6bf54241554ec26efdf62f74fb72b75fb0d4c581d5cbfdde5", + "review-2291/pending-final-svelte-red.log": "4b1bddda86032ef4cff20fd49c51f86200aca1fb0ae6afd94b69ceab96b67d29", + "review-2291/pending-final-vue-red.log": "ae374a47f03bba5804c6d30094146c6e644d21a452fa68ca3fb1a3e7e294014b", + "review-2291/preproduction-gate-final.json": "292decc56ee28fcd5c0ef445324524b90e90d38a274b86b4401deab4cdb866cb", + "review-2291/preproduction-gate.json": "76dd7b8c0addc57652e2edf9ecafce0e457c304fe9390f43bcdf653a80524e18", + "review-2291/raw-issue.json": "e7811b06d7172632a84630e4648f089b964441518de2446f19dac96abd8cd6f0", + "review-2291/replay-pending-green.log": "79d8be1d1721f23ae7493830c2ee6355b0fbd984b9d76dc24c355ec50eb27874", + "review-2291/replay-preact-render-green.log": "a82b24a14b8382eca59243a8b8bdb0a1ce56d66a0a462c43c9e29fc796245c03", + "review-2291/replay-react-render-green.log": "aded899f0ef58b985aa2f184f5df0255ad3502481a5d890a31fe48b6c14b6683", + "review-2291/replay-window-green.log": "88199a22715d81e2f2160cafb19f8982c713c8c4caafc08cc866882721f1bd2c", + "review-2291/replay-window-red.log": "bc7e4813aeb1e4b71e2be0cb4646aa2a68a615c7d6330613a24b05e6fa166905", + "review-2291/replay-window-repaired-green.log": "5c413b80062abe7d93e306b60491381a2a7f8ba6e15de0f6be249b3b0de94fd0", + "review-2291/topk-mutant-results.json": "4c0c1bfd3f649540a217fa9a85aa3cdc6f570a62842e49817ece061ca8b5771d", + "review-2291/topk-numeric-demand-array-red.log": "b4bed9650d46a915d11c8a7ca52ff5251bddf7fbac6b72f311005157495fedd8", + "review-2291/topk-numeric-demand-grouped-red.log": "e1dc20ad465ada69d469870560f4255d29252c0d05fa5ffd47898f7f0cc28e62", + "review-2291/topk-numeric-lint.log": "89d7da780deb04850db632f478bc4155d23659849c0e5bec6d114c74dc9d5168", + "review-2291/topk-numeric-offset-array-heap-failure.log": "9c0bfa5b0efb5b8a8f21f931f063eba8f9c5a1e0e628bdbaef0d8f2c4c563875", + "review-2291/topk-numeric-offset-array-red.log": "d3fdba2c577451a4a474a058b792564fc88d7bc29587bb7a90a22feaaf35b773", + "review-2291/topk-numeric-offset-grouped-red.log": "c1093cba625a08d4b5df53fb7e15d526b5ec8e9d3aa54d10966e2a58344173e6", + "review-2291/topk-numeric-offset-red.log": "55310c29d818a4e90d9d812ab30e8413839d453f2bbb11b1dad0064d57c1cecc", + "review-2291/topk-numeric-partial-array-red.log": "014f023f906c92d606c1ee79b58fe3bc8f9958e6c1055e7d44582759df825559", + "review-2291/topk-numeric-partial-grouped-red.log": "0b7d34a9c7a52301b5658c77cff297824ccfabb3925d669556be683bdee174d7", + "review-2291/topk-numeric-partial-replay-green.log": "87a0db19fc23d74e77dbe6b4fd21d25fee0539f2d3bda9b1d5d029e429ee0550", + "review-2291/topk-numeric-partial-replay-red.log": "ca4c9aad858fcd69b71ebf678aca4036cbea2c680452e4af776ba012dfcbe73f", + "review-2291/topk-numeric-width-array-heap-failure.log": "317f0417df10fc6fbed8797277bce822f1fe732a5a478c2420f510a3964e9bab", + "review-2291/topk-numeric-width-array-red.log": "e179270a5c3ab1f36ea97644c99bb72922f869bc8ed17e115f37f288ca155adb", + "review-2291/window-angular-gate-red.log": "39fef09b3ab984711856e83981495afde99a4f3fc6fc37174ecc98e7b8237513", + "review-2291/window-angular-green.log": "0bb9f4cb2b512f22da5f41d950e147cf8fdba6181085a927d864b05390e56562", + "review-2291/window-angular-red.log": "543d9c30f47d4c773b3b1fdaea42e0cc49d262af237c16b68d5342bc5da43361", + "review-2291/window-expanded-red.log": "4a935a58599718321e643145a693fdc95801b131dc6833e39198d9cff4478a80", + "review-2291/window-first-green.log": "a03bc9969bba404556a4b15153cca9686c7bae9f7c04bbcc40349feeaa34c82d", + "review-2291/window-framework-red.log": "d3c966f7472d640db582b387edf7b986d85c7def1c6c37f7d7765b3f397775fa", + "review-2291/window-gate-red.log": "4657e198299be5465f556abd4b7f510add0bd6c52598b69b4b3e297969566b14", + "review-2291/window-options-react-green.log": "be9cd05e24a97cf36bc935119ef2ef5c2a44339d138a1df3a8175a21e6cdf5e8", + "review-2291/window-oracle-red.log": "22ee4a98978445075ce20397f58a91fec25eea9d37c2ba57e94f73bef5a1cdfc", + "review-2291/window-preact-gate-red.log": "8f6932ccc96db7e5c2321134e629e49a34444ecf0b620f876248bf09da488df1", + "review-2291/window-preact-green.log": "7a2bd33ce9ba15e6d215ff4dd9487d0861eeae5e8bc56695fa4105d0378a0d36", + "review-2291/window-preact-red.log": "6275c72c6d006fecbda9fd308d37fb8a43a33b292387f3a3f8a1ca5697750b82", + "review-2291/window-preproduction-red.log": "6a221c080b4bed40fdef59b4a376cbd0a9b1da7102c1ed2fcc139d296d17e7be", + "review-2291/window-publication-callback-angular-receiver.log": "6fa75655300e070259b24184a7270e03d84405202b5b1f1a634788be03d8e970", + "review-2291/window-publication-callback-angular-red.log": "a6099dbe83bf8380bb687fa632bf9f2080d10c36d1224a751240411e01db1abd", + "review-2291/window-publication-callback-neighbors.log": "c6bc70835aa32ab29dda0a6efaf785df4bb20d036a364372edc1ca46fda6d6c9", + "review-2291/window-publication-callback-preact-receiver.log": "5ca07b3812484bcb3fc0524b8b2a8ba0dc875db855397a22fd6e602fefc320ab", + "review-2291/window-publication-callback-react-receiver.log": "f072236f03d8d749931b81bf570ef9f50fe036b74c3e56bef3eacf9d0a633986", + "review-2291/window-publication-callback-react-red.log": "595f8f74406e60bcebf5ac973f6d7b2565652419a1d1d4d58363295a06989965", + "review-2291/window-publication-callback-solid-receiver.log": "0a88343547c059380a11abc0e37a189b4cd5b91f1c692374b31bd8cc8fb42692", + "review-2291/window-publication-callback-solid-red.log": "4a952482f0faf5b5071f4ddf16d8bec5e79bd3bf4c73f640c4730915835d055c", + "review-2291/window-publication-callback-solid-replay.log": "a7b82d92146c679caead92759fdd4be256537aaf1a597e4e617fee94d27f04fa", + "review-2291/window-publication-callback-svelte-receiver.log": "49262de6ee1bb59263d2cdc3da8ecbd160bc79347526b480d380bc9c3345727a", + "review-2291/window-publication-callback-svelte-red.log": "180d772aaf54c8f5a344ea98c2f72d6c1fbeb63680115bde73503c4be37ce0fd", + "review-2291/window-publication-callback-vue-receiver.log": "6bd79754906659831f34a1939e8d11428ff2cd05fedf0f6da6e6692085dc6483", + "review-2291/window-publication-callback-vue-red.log": "fc2ed87b584943cdafbd157f4355f38f3b446d8b8c6e01eaec4b4b7b59bae38b", + "review-2291/window-publication-evidence.md": "2ce0dc16df399e77a79d43a4f09accea7e0fa9cf463b31ae79d6847bd9557397", + "review-2291/window-publication-format.log": "d009cb1d58a53551c58b48c37173571151c40de8c26865219f9671ce89addcec", + "review-2291/window-publication-lint-final.log": "89d7da780deb04850db632f478bc4155d23659849c0e5bec6d114c74dc9d5168", + "review-2291/window-publication-lint.log": "89d7da780deb04850db632f478bc4155d23659849c0e5bec6d114c74dc9d5168", + "review-2291/window-render-red.log": "687894f579cd1179716555b103dba3d7633c1552f7431075d5288b2e7fba5e91", + "review-2291/window-scheduled-react-green.log": "3107ca0e10a51f4cb5e98b99a4ebd2949290c7473cdbcae20412dbde867483fa", + "review-2291/window-solid-gate-red.log": "ff2c8a64620f80315afff1e33dbb3b5a868ddfd6827a6f9b3c459801a7317054", + "review-2291/window-solid-green.log": "40db19327cce5ad2afcb415300dd619d0471621d0902a815c252b0b7c4d9c5b8", + "review-2291/window-solid-red.log": "c3ba5cf3e5a180a047110b8da43782c9503f586623c3fa71ad52926bcd976102", + "review-2291/window-svelte-gate-red.log": "313672e08d83e2e4c3f1e185961397b61b4ad425343d4887c1d07e3af60174d7", + "review-2291/window-svelte-green.log": "f880cef3d0bf3ace6a3120837104215d3bf61565e54d3578129dc9bd409c225f", + "review-2291/window-svelte-red.log": "e0962c296702623c446810307022c742b079cc4410d80220d786a85f8f6708f8", + "review-2291/window-vue-gate-red.log": "ab741291c3a50aabaeb24d711feb8ed5ec5a50290b8c318b692bb25e347f3359", + "review-2291/window-vue-green.log": "7c3f07828b8909718fff198d9f896717eccea67e8c1cc09e5208d8f813a62e3b", + "review-2291/window-vue-red.log": "95d7f6b26904abcc52910ca7620ca1ee791aafcf2c364b848d6c3d40093ea635" + }, + "validation_logs_sha256": { + "review-2291/final-lint.log": "89d7da780deb04850db632f478bc4155d23659849c0e5bec6d114c74dc9d5168", + "review-2291/ivm-types-final.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "review-2291/implementation-types-final.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "review-2291/preact-types-final.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "review-2291/svelte-types-final.log": "65769855580cfe873365048f0bd4886306a9375e0f8754004497bd43e2fe5b82", + "review-2291/size-compare-final.log": "e41fbc742834a9f21270f2384996232fa21155a9f021b27c1bc5513108e94479", + "review-2291/svelte-size.log": "7819e68adc54ecb8dc0fc9e7dd706c01ff8bc7baeea37b21c9b06689cf1ca123" + } +} diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-prep-manifest.json b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-prep-manifest.json new file mode 100644 index 0000000000..4d374e4915 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-prep-manifest.json @@ -0,0 +1,59 @@ +{ + "head": "bfd1da1a7e752d66fd667adfd3cdf54f60732b22", + "state": "uncommitted task snapshot before optional simplification decision", + "sources": { + "docs/contributing/glossary.md": "962575e2ca0c357c81c2e505d98f78db79d44d43dc16de9187eb32658cf31f65", + "docs/contributing/oracle-coverage.md": "ce3bed9d55a4033f138b7e5da5aac87ba25e826417d1a1203058fee63f73c8bd", + "packages/angular-db/src/index.ts": "b856d0a98c47071691583a370edffc25764d529fa33fecc28c383ed54deb3fe8", + "packages/db-ivm/src/operators/topKArray.ts": "ff21d0f944043db4d28c9be7c429594d668763f634443801a8dc65188ee8d9f1", + "packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts": "1ef076cdca0976323847511681d6d4c75a25bd2c345377a8df4b1d52a9625577", + "packages/db/src/live-query-options.ts": "189a4f2ba266cf93a75d373b5fe115b4d940ca8cd404f5dec4c02f6ef25f6792", + "packages/db/src/query/effect.ts": "354bbbcf9683c91047c61da1ea2323ed89623628eb6059dacfbb79bd56b85ce4", + "packages/db/tests/conformance/contract.ts": "b6f345576497c94aeb2720b11ff188a55f27b74905ae2c69ec42e9421f232b41", + "packages/db/tests/conformance/lagging-source.ts": "19feb3b6f87e89fd60cf22605f81c755d94e43db39a11a19e1d49f42accc1097", + "packages/db/tests/conformance/suite.ts": "be7929e6d0d0838f4214c7af473c77b91d32741b347d91d20041a5f8c790ebd2", + "packages/db/tests/conformance/window-history-oracle.ts": "531dbc269de8663de5ba5c7eb4b70f94b8056e2d438ca0960122588991b25168", + "packages/db/tests/failure-fidelity.test.ts": "5072d609cde0a399f41e24e90929377497ec4a7f8276dfbab520ce3a4bd0bf64", + "packages/db/tests/oracle-config.ts": "c2c3269f5ce27662cf0eb8e5a2b972c6df307d33482b85b7abda0581e8432d63", + "packages/db/tests/oracle-failure-fidelity.ts": "f95203e1c3f7d9f256f10862dd7b9627e85879321fd82b214a3f40ad01ed6901", + "packages/db/tests/query/effect-callback-oracle.ts": "ca9e1336e7ceeb243a8e32a49ad2c3c881deaa38334540e85f848122cbaf2d38", + "packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts": "47e862b092e643309bd804acab7c61c036ed7a345c873ca480f63b807aa4f24d", + "packages/preact-db/src/useLiveQuery.ts": "fe381f9c87cfdd5b57e9cc9ff77a6d1dbfa1fc84191c69fa7247fa9953223f0d", + "packages/preact-db/tests/window-render-oracle.test.tsx": "994f5702a0ff4649369f44bc7ea8341519b19c0260c887b458b97b3f951c06a1", + "packages/react-db/package.json": "2136a1486e15558a8cd8ec57615db17f6aa5e4032b1afe803404bdd83d99ec0a", + "packages/react-db/src/useLiveQuery.ts": "ffa5a4fa3e68e9dfbfea2ff3b988db852ecc14f688aeeabbad5ac1a79a09b6e9", + "packages/react-db/tests/effect-callback-oracle.test.tsx": "9ed573ae14bf9bd87058c9136ef1de57f33422da235f6253a9c97f90599cf98e", + "packages/react-db/tests/ssr-effects-driver.tsx": "9cf27fdbe6f1fbd72629d9ba11880ab650421feb58d82ad174891043b53845c5", + "packages/react-db/tests/ssr-effects-oracle.test.ts": "4052260aa9b192c3773e3147a6d31b568152f563ea869f195f9b602ad66555ce", + "packages/react-db/tests/window-render-oracle.test.tsx": "819359c432c167bf2f23462c6fa8e63de443569aba66a6838def2183eca539cb", + "packages/solid-db/src/useLiveQuery.ts": "8e414683931992f72d3208d1f5952d2f33625eac7b0b88aaf5755a210d19ded2", + "packages/svelte-db/src/useLiveQuery.svelte.ts": "23e316c6515bcbdd159121c9497911075ed3d54670a6d7b19a76089eef2212bc", + "packages/vue-db/src/useLiveQuery.ts": "5c4d910952e54b58002459b21d86c9ba6c25fcc67e7cce677e0c6b66b7aecc3c", + "pnpm-lock.yaml": "aad3ef34e9f98129d6929b4b9c46880b8c9758971da14a5686f479824d1549f4" + }, + "evidenceLogs": { + "prep-ssr-red-final.log": "d2751221d8343e429308bd24e6e615768666b3b0656f65a6dc3a4fa771c773d2", + "prep-ssr-mutant-final.log": "7fb5e7cb2c63e9a41818cb39275acdc312291770b87af4f837b6e90e1aa5dd70", + "prep-ssr-replay-final.log": "4ad627db3deed1c885ab3012278303859942f2ac334fbeb5013c737f2bc22d46", + "prep-final-changed-lint.log": "ec4182d730ce311d890924a35a5e282747fa1350561b28cec7df03c773184dfd", + "prep-final-core-integration.log": "4b1f10dac222b39518b1b2779e267b5f8db626270e2d939443c13ec7478561b4", + "prep-final-db-types.log": "07a23be87a598a26bdfab6304f0ff2dfcd4ed39642e1b2c8214237565ca6790f", + "prep-final-react-integration.log": "7f848542f5ccd8e23fd923391d858df448acaa600667bf5b9141cddafdfce7c4", + "prep-final-react-shared-windows.log": "cdf5ff7645e1b9c4fdd75e83ea4880695dfd3ca6b31fab75dbed19585e355540", + "prep-final-react-types.log": "00cb5d0046a00ec92dadbe1fc7c10ac3080f8c5332605e5f3cfbcf59e374d1a8", + "prep-final-size.log": "49f32fe2e54d1625ea3a1d407efacd8eb6dfc6eabb96783169db6a4cdf2609f1", + "prep-final-whitespace.log": "3d4d50cd32af92c6d4df37c1df74a96622fca8cfb5643a155c91f034a42f2068" + }, + "tests": { + "coreIntegration": 391, + "reactIntegration": 154, + "sharedReactWindows": 44, + "totalDistinct": 589 + }, + "sizeGzip": { + "sevenPackages": 791, + "typicalReactApp": 561, + "sveltePriorUnchangedSource": 110 + }, + "limits": "Historical manifests remain historical. This is a local integrated-tree checkpoint; no pushed-head or CI claim." +} diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-prep.md b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-prep.md new file mode 100644 index 0000000000..cb1627b5c6 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-prep.md @@ -0,0 +1,496 @@ +# Page-window PR preparation review + +This addendum preserves the later preparation review separately from the +[original evidence](2026-10-30-adapter-page-windows.md) and +[second oracle review](2026-10-30-adapter-page-windows-round-2.md). +The reviewed starting HEAD was `c351fc04d7f70dc59bf92ecbed310f899b9511cd`, +with the page-window changes still uncommitted. Five independent code-review +angles, independent confidence checks, and a separate simplifier supplied the +raw material. Their source-inspection judgments did not establish new runtime +results. The preparation evaluation supplies executable evidence separately. + +## Accepted law and review findings + +The maintainer decision for #2291 is unchanged: an invalidated adapter-owned +ordered list query whose structured plan changes only its finite outer +`.limit()`/`.offset()` moves the same window. It retains its live-query +Collection and upstream subscriptions. No hook option or public API is added. + +| Item | Evaluation and action | +| --- | --- | +| H1 | Fixed oracle-first: a module-stable server/client effect choice removes the React 18 warning and retains client layout commit. The [SSR record](2026-10-30-react-ssr-effects.md) gives pre-edit RED, GREEN, hostile rejection and the three-reviewer reconciliation. | +| G1 | Duplicate of R2-5: initial-loading, Collection-option and Preact aborted-render coverage entered after the first production edit. Current coverage cannot repair that historical order. The qualification remains for maintainer disposition. | +| G2 | Required shipped-size rationale is given below; measurements and actual reductions are distinguished from suggestions. | +| B-L1 | The resolver checks finite target bounds, while its old limit only needs to be present. The comment and current coverage entry now state that distinction. | +| C1 | Independent duplicate of B-L1, retained separately in the raw audit. No replacement-identity law for explicit Infinity-to-finite moves was established. | +| S1 | Optional small Vue/Solid wrapper simplification: move the constant GC option into each wrapper. Angular has a varying pool option and is excluded. User disposition and validation belong to the subsequent preparation step. | +| I1 | Integrated main normally at `bfd1da1a7e752d66fd667adfd3cdf54f60732b22`. The combined tree passed the focused Effect, SSR and window checks below. | + +The finite generated window domain remains the scope of the retention +evidence. An explicit Infinity limit is present and can pass the resolver's +old-bound check before moving to a finite target. The prior phrase “unbounded +transitions keep their existing paths” was too broad. No public contract +requires replacement identity outside the guaranteed finite domain, and no +wrong rows were demonstrated by this review. This clarification changes no +guard, oracle expectation, or product behavior. It does not claim an executed +Infinity receiving witness. + +## Why shipped bytes still grow + +The original measurement added 766 gzip bytes across seven separately bundled +packages, including 377 in core and 22 in db-ivm. The typical tree-shaken React +app added 539 gzip bytes. Svelte's separate compiler pipeline added 110 gzip +bytes. These are the earlier measured snapshot, not measurements of later +effect or wrapper edits. + +The implementation adopted two reductions in the existing machinery. Plan, +source-object and Collection-option matching live in the shared resolver; +the six adapters retain only their native input and commit integration rather +than each implementing that comparison. Moves use the existing `setWindow` +path and preserve existing observers where the adapter supports that, instead +of adding another public window controller or compiling a parallel query. +TopK stores the authored limit in place of the stored end and computes the end +when needed. Its installed-range clamp replaces the earlier special Infinity +branches rather than adding a second range representation for that case. + +The remaining growth pays for distinguishing ownership and timing that the +previous rebuild path did not need. A binding must identify its own previous +Collection; shared cached results cannot be moved by peers. A speculative +render must retain a proposed move until commit. A committed callback must +wait until the current Collection publication ends. Exact authored bounds must +survive a partial move even when installed rows are empty. + +Actual simplification experiments also bound the available reductions. Applying +the move synchronously from the publication callback removed the scheduling +step but failed with `SetWindowReentrancyError`. Recovering an omitted width +from end minus offset removed exact width storage but failed the numeric +partial-move row comparison. Permanently clamping stored bounds to installed +rows failed when the source later grew. Deriving demand changes only from +installed deltas failed for changed empty ranges. These were executed hostile +reductions, not successful refactors or independent byte measurements; their +failures are recorded in the original and numeric evidence. Removing source +and option guards also failed the corresponding receiving comparisons. + +The simplifier did not recommend a broader resolver factory, allocating +object-rest comparisons, combined receiving drivers, or TopK representation +rewrites: none had a demonstrated local benefit, and the receiving drivers +encode different host cuts. Those were source-inspection decisions, not +claimed executed refactors. The optional constant-options cleanup is tracked +as S1. No bundle-cost claim depends on deleting useful comments or weakening +tests. Test and contract-document growth is separate from shipped production +bytes. + +## Reviewer assessment and loss accounting + +The code-review team found one concrete compatibility defect that a React 19 +test runtime could miss, distinguished a historical process failure from a new +product bug, and retained its two low-confidence scope observations without +inventing authority for replacement identity. Its source reasoning was +accurate and well prioritized. Hire recommendation: yes for this bounded +review; the source-only finding still needed independent executable evidence. + +The simplifier proposed one small, behavior-preserving cleanup and correctly +excluded Angular's varying options. It preserved independent oracle models, +receiving checkpoints and historical evidence. Hire recommendation: yes for +focused maintenance work; it did not claim to have executed validation. + +The scratch audit retains all raw items, duplicate sources, six considered +simplifications, four comments-angle nonfindings, and twelve prior-review +checks. Nonfindings retain their original source limits; they are not evidence +of exhaustive behavioral closure. No suggestion was silently applied and no +confidence threshold erased an item. Final dispositions and final-tree +validation are appended after the preparation decisions. + + +## Final local integration and evidence + +Main integrated by fast-forward with autostash `1bc0686d0`, which reapplied +cleanly. A pre-integration binary patch and SHA256 inventory protected the dirty +task files. Every inventoried file except the coverage map was unchanged; +that map gained the disjoint upstream Effect text. No unmerged file or reverse +deletion of the upstream work remains. No task implementation commit or push +has occurred at this preparation checkpoint. + +On `bfd1da1a7e752d66fd667adfd3cdf54f60732b22` plus the task edits: + +| Focused check | Result | +| --- | --- | +| Core Effect owner/ordinary Effect/disposal/failure-fidelity | 391 tests passed | +| React Effect/window/SSR effects/runtime-error handoff | 154 tests passed | +| Shared React window histories | 44 tests passed | +| Total distinct tests in this integration run | 589 passed | +| Core and React TypeScript | Both passed | +| Changed-file lint and whitespace | Passed | + +All Vitest commands used at most two workers with file parallelism disabled. +The SSR source, receiving program and oracle hashes match the frozen sources in +its evidence record. Its [measured reach](2026-10-30-react-ssr-effects-reach.json) +contains 24 runtime/path/campaign records, 35 render comparisons each (24 +generated plus 11 fixed), and two separately identified disabled replays. +The original-effect mutant fails all six React 18 fixed campaigns; seed 2291, +path 0 reproduces its ordinary-provider diagnostic failure directly. The +existing real client receiver preserves 24 committed/suspended/cancelled tests. +The prior all-framework and numeric evidence remains bounded by its original +source snapshots and recorded limits; this integration run does not pretend to +rerun all of those tests. + +The final size command was: + +```sh +pnpm size:compare --packages db-ivm,db,react-db,preact-db,vue-db,solid-db,angular-db --files +``` + +Against the new main merge-base, the seven package bundles add **791 gzip +bytes** (683 brotli), and the typical tree-shaken React app adds **561 gzip +bytes** (415 brotli). React's package contribution is 106 gzip bytes, including +the server-safe effect choice. These replace the earlier seven-package/app +numbers for this checkpoint. The separately measured Svelte result remains +110 gzip bytes; its adapter source and external-dependency compiler path have +not changed since that measurement. Svelte was not rebuilt by this final Vite +size command. The shared comparison, ownership and commit distinctions above +explain the residual growth; the unsupported reductions were tested rather +than silently removing those distinctions. + +Production shipped bytes are separate from the generated-history, receiving, +failure-fidelity and contract/evidence files. Exact React 18 aliases and their +32 lockfile lines are development-only receiving dependencies, not runtime API +or an installation workaround. No local override was introduced. + +The final loss audit accounts for 35 original preparation items and 13 new +SSR-oracle review/driver items: 17 fixed now, 27 bounded nonfindings or rejected +broader recommendations, 3 separately retained duplicates, and 1 optional +simplification awaiting user disposition. No confirmed product enforcement gap +remains open in this bounded audit. The canonical historical R2-5 process gap +remains disclosed for maintainer disposition; it is not repaired by these +results. The optional S1 cleanup still awaits the prep-pr walkthrough. + +Local implementation and integration checks are complete at this checkpoint. +The [checkpoint manifest](2026-10-30-adapter-page-windows-prep-manifest.json) +identifies 28 source hashes and 11 evidence-log hashes. +Whole-PR readiness remains unproven until the user preparation decisions, +commits, pushed-head checks and CI workflow are completed. No external PR +review or CI result is claimed here. + + +## Accepted simplification S1, 2026-10-30 + +The user accepted S1 after the technical evaluation. Vue and Solid now put +`gcTime: DEFAULT_GC_TIME_MS` inside their local `resolveNext` wrappers. The +wrappers no longer accept or spread an options argument, and their four call +sites pass only the query value. Every earlier call supplied that same constant. +The owner, previous Collection and resolver calls are unchanged. Angular keeps +its varying pool option. No product law, oracle model, history grammar, +comparison, run budget or public API changed. + +Validation used the existing real Vue and Solid shared window owners: + +```sh +pnpm --filter @tanstack/vue-db exec vitest run tests/conformance.test.ts \ + -t 'window histor' --maxWorkers=2 --fileParallelism=false \ + --coverage.enabled=false --typecheck.enabled=false \ + --disableConsoleIntercept --silent=false +pnpm --filter @tanstack/solid-db exec vitest run tests/conformance.test.tsx \ + -t 'window histor' --maxWorkers=2 --fileParallelism=false \ + --coverage.enabled=false --typecheck.enabled=false \ + --disableConsoleIntercept --silent=false +``` + +Both passed 10 tests, for 20 affected-path tests. Both package TypeScript checks, +changed-file lint and whitespace validation passed. The other 26 source hashes +in the preceding checkpoint manifest remain unchanged, so its earlier core, +React, SSR and numeric conclusions retain their recorded scope. The two updated +source hashes are: + +| File | SHA256 | +| --- | --- | +| `packages/solid-db/src/useLiveQuery.ts` | `69c375e78cbd034602a40c9024ac817dce9271fdcf44808933b99c66730b195a` | +| `packages/vue-db/src/useLiveQuery.ts` | `20ada3082bbc11d23abd0ea7c20ea878b17794b81f824c728f836f9c2c9b8bf9` | + +`pnpm size:compare --packages vue-db,solid-db --files` rebuilt both adapters +against the same main baseline. Vue adds 33 gzip bytes rather than 43; Solid +adds 36 rather than 46. This accepted refactor removes 20 gzip bytes from the +seven-package total, leaving **771 gzip bytes** (660 brotli) of total growth. +The typical React app remains **561 gzip bytes** (415 brotli) above main. +Svelte's separately measured 110 gzip bytes remain unchanged. This supplies an +additional adopted refactor and measured outcome for G2's size rationale. + +The seven validation logs have these hashes; verdict-critical results and +commands are stated above rather than depending on local log availability: + +| Log | SHA256 | +| --- | --- | +| `prep-s1-lint.log` | `ec4182d730ce311d890924a35a5e282747fa1350561b28cec7df03c773184dfd` | +| `prep-s1-size.log` | `c1a95124033cf8d99ae9fd5249bf1d8a7d3eca2367963031823d53a1bbf15935` | +| `prep-s1-solid-types.log` | `4abfd87bbb5257122676e2e2470cc8e23a96d6529112c225f186e0dfcdb06dbb` | +| `prep-s1-solid-windows.log` | `aa96acb23d5c853d10cd9a83720d1db6502ec5f0e64d3b0b814f284de1817b34` | +| `prep-s1-vue-types.log` | `bd345ce7c920924e75f922a89027b1fb57042b09687b3b281fd4fdd13d7ce00e` | +| `prep-s1-vue-windows.log` | `63f200cda8608d7df49d009068a6d87e50aaeecc08b45c295a12973549ee6c0f` | +| `prep-s1-whitespace.log` | `3d4d50cd32af92c6d4df37c1df74a96622fca8cfb5643a155c91f034a42f2068` | + +S1 is now fixed and accepted. The preparation audit totals are 18 fixed-now, +27 bounded refuted recommendations/concerns and 3 preserved duplicates: all +48 original/follow-up items are accounted for, with no remaining optional +suggestion. The historical R2-5 qualification remains disclosed. The next step +is the prep-pr workflow's final user confirmation before commits, changeset +and PR-body work. No task commit, push or PR has been created at this checkpoint. + + +## Approved publication checkpoint, 2026-10-30 + +The user approved commits, the changeset and PR publication after S1. Before +publication, main advanced to `0531c50ad419a1b51ad3e27e3632e70be3c6bdb3` +(the distinct hash-collision repair). A normal fast-forward with autostash +integrated that commit. The coverage-map append conflict retained both owners. +The task's tracked source bytes stayed unchanged except the combined coverage +map and oracle-config additions. Historical manifests above remain unchanged. + +Verification on this integrated tree passed 1,251 tests: the full IVM package +(657), six selected core files (396), five React files (154), and React shared +window histories (44). The selected core files include the upstream distinct +collision and Effect tests; the React files include the SSR runtime handoff +owner. Each Vitest command used at most two workers with file parallelism off. +IVM, core and React TypeScript checks, `pnpm lint:changed` and +`git diff --check` also passed. The full root monorepo test command was not run. + +The exact commands are the first lines of these logs. Hashes preserve this +checkpoint without changing older evidence: + +| Log | SHA256 | +| --- | --- | +| `publish-final-changed-lint.log` | `ec4182d730ce311d890924a35a5e282747fa1350561b28cec7df03c773184dfd` | +| `publish-final-core-integration.log` | `70b4e99973ad61f6e7bf8247d4a1cc444dd8572aa1ef02ba1e2426d060a29d00` | +| `publish-final-db-types.log` | `07a23be87a598a26bdfab6304f0ff2dfcd4ed39642e1b2c8214237565ca6790f` | +| `publish-final-ivm-integration.log` | `db7b49d0c99d61dc0a08e919cf037b00a3755ae97b4981d64f2ed9de24dd5bba` | +| `publish-final-ivm-types.log` | `7ae8481162b2401099262bce31f2e09ccbfd9b93060a1f6630c1c8638ef497a4` | +| `publish-final-react-integration.log` | `dfb17f1ecfd8660bf44e247ac070c14164d1b74d2de6b3d342571565314a5d76` | +| `publish-final-react-shared-windows.log` | `1fbb1880af2e184d5aeca80ecf5c8f6a81d3d9724d0a28d55df0e60e957ed3b3` | +| `publish-final-react-types.log` | `00cb5d0046a00ec92dadbe1fc7c10ac3080f8c5332605e5f3cfbcf59e374d1a8` | +| `publish-final-whitespace.log` | `3d4d50cd32af92c6d4df37c1df74a96622fca8cfb5643a155c91f034a42f2068` | + + +The final size comparison uses that same `0531c50ad` main baseline: + +```sh +pnpm size:compare --packages db-ivm,db,react-db,preact-db,vue-db,solid-db,angular-db --files +``` + +The seven-package sum grows by **775 gzip bytes** and **664 brotli bytes**. +The typical React app scenario grows by **543 gzip bytes** and **502 brotli +bytes**. These replace the prior checkpoint's totals for the final integration. +The unchanged Svelte source retains its separate compiler measurement of +110 gzip bytes; this command did not rebuild Svelte. Compression differences +from the preceding checkpoint follow the upstream IVM integration. No new +runtime branch was added for this measurement. The final log SHA256 is +`5bcb5bd9103154ce65824a39aaa754d80e635f0b691d03c84d7f32122cbcd2b8`. + + +### Commit-hook cleanup + +The normal pre-commit hook rejected a redundant `!owner` conditional: the +previous retained-owner check already excludes an absent owner. It also sorted +one import. Removing the redundant branch changes no reachable behavior. The +oracle laws, histories, comparisons and budgets are unchanged. Explicit ESLint, +core TypeScript and all 24 React window-render tests passed after this cleanup. +The final source SHA256 for `packages/db/src/live-query-options.ts` is +`7426405067121706cb02b382f658aa312668d55278378d41182f012d2d62cb8a`. + +A fresh size comparison on these final bytes reports **772 gzip / 626 brotli +bytes** of growth across the seven packages and **540 gzip / 515 brotli bytes** +for the typical React app. These supersede the preceding size totals. Svelte's +separate unchanged-source measurement remains 110 gzip bytes. + +| Final cleanup log | SHA256 | +| --- | --- | +| `publish-hook-eslint-fixed.log` | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | +| `publish-hook-types.log` | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | +| `publish-hook-window.log` | `46b22d5f974616b8b271ca236491ee19a96377787e17e90dc66c161109eac5a2` | +| `publish-committed-size.log` | `930c14c6a11d60a4e74d1d098b29933d7ee98db2ebfe939c951ae403532f53cf` | + + +## Published CI repairs, 2026-10-30 + +PR #2316 initially published implementation `38422ac61` and changeset +`9b09f1e7`. CI's IVM tests and test-source types passed; its final dependency +check failed because the two React 18 development aliases were not sorted. +`pnpm test:sherif` reproduced that metadata error. Commit `3ef2e5fe7` sorts the +aliases, and the same command passes. Dependency versions and the lockfile did +not change. CI then passed the IVM/offline job. + +The DB replay job exposed a second integration omission: all eighteen new +property names were registered but lacked manifest owners. On unchanged +`3ef2e5fe7`, the full replay suite returned one intended manifest assertion +failure and forty passes. This is a replay-routing defect, not a product row +mismatch. The existing guard correctly rejected the incomplete registry. + +The repair maps the shared framework properties to the React conformance +receiving file and names the shared model module as their source. React and +Preact render, twelve SSR runtime/path cells and numeric TopK have their own +owners. Direct selectors choose fresh/random campaigns. The source checker +recognizes SSR's computed property template and independently checks its +runtime and path literals. It keeps every prior manifest assertion. + +The full replay suite now passes all forty-one tests: + +```sh +pnpm --filter @tanstack/db exec vitest run tests/oracle-replay.test.ts \ + --maxWorkers=2 --fileParallelism=false --coverage.enabled=false \ + --typecheck.enabled=false +``` + +Every new entry's manifest command was also run from its declared directory +with `TANSTACK_DB_ORACLE_PROPERTY`, seed `2291` and shrink path `0`, one worker +and coverage/typecheck disabled. The guarded runner reported these exact, +nonfailed executions. Each witness reported `numRuns: 1`: + +| Property | Completed executions | Log SHA256 | +| --- | --- | --- | +| `framework.window-history` | 16 | `baeab34ac392593a1eac89f1eda36b9d8bb321f3e1ab4d1bee96897d9adbd7eb` | +| `framework.pending-window-history` | 2 | `0d580e6b6dcb23dabfee3a4dd355062bd101c7c9e71a8cdfea49bc769cc81eb1` | +| `framework.publication-window-history` | 2 | `80b14b353c2b0af0a1bbebe129ba6fc764907aba94d1694d86e1af450bec6096` | +| `react.window-render` | 12 | `53483a4300727b16a0a4fec7dd64cff9c75183ff748520c460d580472f7ace13` | +| `preact.window-render` | 12 | `3f7c4ed0cab91398e4db3ab41b72322ab2b8e7c9b1b5ae4f48b601e3aefbbad1` | +| `react.ssr-effects-react18-ordinary-provider` | 1 | `6502a2f6ed5d2b61ae93f88acbab3c222eac8dd083a925069553842dacf0a2c5` | +| `react.ssr-effects-react18-ordinary-explicit` | 1 | `ea264bf8907dc8ab1d3273850e7f0b6a145f78bf819f3148b2e7ef63626272ed` | +| `react.ssr-effects-react18-suspense-provider` | 1 | `7b01b01f59d1fa55a4ea1dfa2c7affbc61d2ad35e50c3be5b2147bdc9cc5e288` | +| `react.ssr-effects-react18-suspense-explicit` | 1 | `a2f60529df6cc88d4ae5182cd2a6516f502a264368ba924d80005522c5977655` | +| `react.ssr-effects-react18-standalone` | 1 | `d5173740c1d3eaee46e659c51835f82bb8efebe908e99956c3e64c382c2d2d2e` | +| `react.ssr-effects-react18-supplied` | 1 | `e07e0d08e5f001180dc2d1b76392e616aac84ed7eed1fc744028b0cb52c07616` | +| `react.ssr-effects-current-ordinary-provider` | 1 | `d186447a8d714d5ac5ff019f1960c91fc020058a8af502212ab67e9293366dbc` | +| `react.ssr-effects-current-ordinary-explicit` | 1 | `0b017317df2e0dd9b7b5fee1f00fa1e59caa22ef7fa3cc0d11156fef15f063c9` | +| `react.ssr-effects-current-suspense-provider` | 1 | `61beea4e2906b3d6ab82df42b4eb519f90cb37dbac4b9a62bb425483d2acf993` | +| `react.ssr-effects-current-suspense-explicit` | 1 | `e8657a5e80ffcae9d46e28c9704f3557b7b7eb22214b40e63212c929f19b52a3` | +| `react.ssr-effects-current-standalone` | 1 | `7df3090ad98872dd46cb56f5d42c96bd1810fa9c35f475d50d3e5d88563a9935` | +| `react.ssr-effects-current-supplied` | 1 | `2de45c888925e0f9a5b0340dc5763c75dac9ae00bc2b03d914f65cd12e0da763` | +| `ivm.topk-numeric-window` | 8 | `f3021933e75830ccf2089b027515701615a373d78dac45c8cf2d9940729c4e77` | + +The numeric owner's guarded runner first attempts asynchronous instrumentation, +then uses its established synchronous retry. Its final eight comparisons and +witnesses pass; the earlier mode failures are not product evidence. +A focused independent oracle review checked all mappings, source overrides, +selectors and eighteen raw witness summaries and found no actionable issue. +It made no edits and ran no tests concurrently with these checks. + +**Law:** ORC-007 and the existing guarded-replay protocol require a registered +property to route to its executing owner with the requested seed/path. +**Reach:** Eighteen requested property routes completed the counts above. +**RED:** The original missing-owner manifest failed its existing assertion, +with forty neighboring replay checks passing. +**GREEN:** All forty-one replay checks and all eighteen guarded commands pass. +**Mutant:** The original registry is the rejected wrong routing; the unchanged +suite also rejects wrong files, filters, skipped/zero runs and changed replay +coordinates. No new product mutant or product-law change is claimed. +**Limits:** These commands establish replay routing, not universal product +correctness. React is the canonical shared-framework guarded route; the other +framework drivers keep native seed/path replay. Existing model, grammar, +comparison, receiving and budget limits remain. No production source or oracle +run budget changed in this repair. + +Explicit ESLint, core TypeScript, changed-file lint and whitespace checks pass. +The original production chronology qualification remains unchanged. + +| Verification log | SHA256 | +| --- | --- | +| `ci-sherif-red.log` | `e7f30af9fc951e95404e2ceace024d177943170ca31075d012057a69b4120996` | +| `ci-sherif-green.log` | `3bf3b69d6ee18726ce8a3b964c7ad3c5593ca5e2391888ea440b909377c1b988` | +| `ci-db-replay-local.log` | `557f5c576a66994f2f75e89c6be187fce6e97f7e18b7953ef0bfc0fe9e00f1e1` | +| `ci-db-replay-green.log` | `1dd8ea18fa377e28889c300c0a5a15e62a570b38249feaf5106425c28703c513` | +| `ci-replay-manifest-green.log` | `4e791ad197488e5a8a454fb97f4d50aa9d6b6973d74e5d64d36b277daf8a3841` | +| `ci-replay-eslint.log` | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | +| `ci-replay-types.log` | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | +| `ci-replay-changed-lint.log` | `89d7da780deb04850db632f478bc4155d23659849c0e5bec6d114c74dc9d5168` | + +Final routing source hashes: + +| Source | SHA256 | +| --- | --- | +| `packages/db/tests/oracle-replay-manifest.ts` | `936a42065fb4031e67410dbb530e3d566a3c7ce75c7e739c199d69aa3d375901` | +| `packages/db/tests/oracle-replay.test.ts` | `c624c7cd6e58fff27e9e488af270887fc1bdf9de36913f3303005291b855dfd0` | + + +## Correction after the CodeRabbit replay review + +The canonical-React routing claim above described parent +`cc058b802ed6921f692f234399437c5a4e663991`. It did not preserve the originating +framework driver and is superseded by the +[driver replay review](2026-10-30-adapter-page-window-replay.md). The earlier +eighteen commands prove only their named target executions on that parent. +Current property IDs include one of seven driver identities, including React's +dependency-list path, with a separate owner for each of three history families. +The new independent matrix first failed all 21 comparisons before routing was +repaired. All 21 guarded commands then passed; a React-for-Solid mutant failed +three owner comparisons and a real wrong-driver invocation reported zero +executions. The complete protocol suite passes 63 tests. Source hashes and +per-route witnesses are in the linked review manifest. No shipped source, +product expectation or oracle run budget changed in this correction. + + +## Production simplification after the size review + +The user requested a net-negative implementation. This follow-up removes +intermediate rank arrays and the unused half-open-range helper, shares TopK's +insertion/deletion boundary calculation, and searches indexed rows without a +synthetic lookup tuple or a fresh comparator closure. The resolver now has one +builder/config compilation path and one query-config reader. Existing semantic +identity, exact-source, option, ownership, publication, and commit guards remain. +Vue, Solid, and Angular delegate callback preparation and disablement to that +resolver, and use their existing resolver closure as the binding identity. +Angular projects the shared observer snapshot instead of keeping parallel copies. +React uses one optional profiling timestamp and excludes development counters +from production; its environment probe preserves bundled development and +unbundled-browser behavior. No public API, oracle law, grammar, comparison, +replay route, or run budget changed. + +An attempted Angular data projection short-circuited the current input and +returned stale rows after compilation failed. The unchanged conformance suite +rejected it in `grouping-operator-switch` and `compile-error-after-rerender`. +The retained projection reads the current Collection before its cached snapshot; +the same two cases and all ten window histories then pass. Direct environment +checks and permanently disabled profiler allocation were also rejected during +inspection because they would change supported development diagnostics. + +`pnpm size:compare --packages db-ivm,db,react-db,preact-db,vue-db,solid-db,angular-db --files` +compares the final sources with main `0531c50ad`. The typical app changes from +**+540 to -15 gzip bytes** and from **+515 to +126 brotli bytes**. The seven-package +sum changes from **+772 to +83 gzip bytes**, and from **+626 to +35 brotli bytes**. +Thus the typical gzip scenario is net negative; all measurements are not. +The remaining guards enforce distinctions that existing receiving tests reject +when removed. Svelte's adapter source is unchanged, and its separate package +measurement remains +110 gzip bytes. These figures exclude tests and evidence. +The original RED/GREEN/mutant records and R2-5/G1 chronology qualification remain +unchanged; a behavior-preserving refactor supplies no new oracle-first claim. + +Final window receiving checks pass 106 tests across React (44), Preact (22), +Vue (10), Solid (10), Angular (10), and Svelte (10). The complete IVM suite +passes 657 tests. Four focused resolver/cache/pooling suites pass 4,446 tests; +the five React development, hook, Suspense, render and SSR suites pass 145. +Vue and Solid full suites pass 152 and 132 tests respectively. IVM, core, +React, Angular, Vue, and Solid TypeScript checks pass. Explicit ESLint, +changed-file lint, formatting, and whitespace checks pass. The root test +command was not run. These checks preserve the existing bounded evidence; +they do not extend its closure claim. + +The follow-up removes 215 net production lines from the published version; +the complete branch removes 82 net production lines relative to main. +Shipped-byte measurements above, rather than these line counts, determine +production weight. + +| Verification log | SHA256 | +| --- | --- | +| `reduction-size-23.log` | `379c08a502c21abcb4a6b7cd91db6ed395e19bf06670cae819f8f8b281a7c75c` | +| `reduction-ivm-verified.log` | `1dc19af4313e94afb18d0b3763af72603bd526398f0890cfab44908c3729d07c` | +| `reduction-core-verified.log` | `c1d4b3927d9d7dac741dae9283dca2c6ccb05b41c0165c304f6f0672c902502c` | +| `reduction-react-verified.log` | `edea08e3ee5afe412cb55a17b50ab6cc9f28256ea03bb6805fe4a1e970e5469c` | +| `reduction-react-windows.log` | `7cd3c265d3cf208b295d0877e8cd75af94aad16ad93d9263f537641dbb008a99` | +| `reduction-preact-windows.log` | `52b9195d842501faf8eb8da7f08950f66b27321b3ac7d28cec10b00887294eb3` | +| `reduction-vue-windows.log` | `743df5ec99bf5fbd5a6f86245a6207d794b67bab2e93516ed7f662dc436947a0` | +| `reduction-solid-windows.log` | `05a1319bbc71c67d8cee0b93a3159fb543181ae68a082885ef5b5bcb9e5ae5e9` | +| `reduction-angular-windows.log` | `9afb4e92151a9b63f66dc1fc652677850cf6159cc21ef05dcc99a4071dbf1f19` | +| `reduction-svelte-windows.log` | `abd368f953a4d7a73fb9b4456f2c4beff5dcea8cc6c693c87dc53e477cd59227` | +| `reduction-eslint-verified.log` | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | +| `reduction-changed-lint-verified.log` | `89d7da780deb04850db632f478bc4155d23659849c0e5bec6d114c74dc9d5168` | + +The final full Angular suite passes 92 tests with one existing TODO. +Its log SHA256 is `ad8d6187d3d88ad25d11ad9cf1c2321a55114b10902d99b52bdd72c3f7dd5fb5`. + +A post-commit size measurement confirms the same figures. +`reduction-size-final.log` SHA256: `88277584aca7eae332dced945bf7f94d2001990eb11c6f984c4384242058dde0`. diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-reach.json b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-reach.json new file mode 100644 index 0000000000..df7f7c0cd0 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-reach.json @@ -0,0 +1,1038 @@ +{ + "render reach false/false/fixed": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + }, + "render reach false/false/fresh": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + }, + "render reach false/true/fixed": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + }, + "render reach false/true/fresh": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + }, + "render reach true/false/fixed": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + }, + "pending reach react/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "render reach true/false/fresh": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + }, + "render reach true/true/fixed": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + }, + "render reach true/true/fresh": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + }, + "pending reach react/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach react/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach react/fresh": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "window reach react/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/changing/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/changing/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/stable/direct/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach react/stable/direct/fresh": { + "source write": 11, + "stable key": 13 + }, + "window reach react/opaque/direct/fixed": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react/opaque/direct/fresh": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/changing/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/changing/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react/stable/nested/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach react/stable/nested/fresh": { + "source write": 11, + "stable key": 13 + }, + "window reach react/opaque/nested/fixed": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react/opaque/nested/fresh": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "pending reach react (dependency list)/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach react (dependency list)/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach react (dependency list)/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach react (dependency list)/fresh": { + "publication callback entered": 9, + "offset requested in callback": 8, + "limit requested in callback": 3, + "callback update settled": 9 + }, + "window reach react (dependency list)/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/changing/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/changing/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/stable/direct/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach react (dependency list)/stable/direct/fresh": { + "source write": 9, + "stable key": 13 + }, + "window reach react (dependency list)/opaque/direct/fixed": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react (dependency list)/opaque/direct/fresh": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react (dependency list)/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/changing/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/changing/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach react (dependency list)/stable/nested/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach react (dependency list)/stable/nested/fresh": { + "source write": 12, + "stable key": 13 + }, + "window reach react (dependency list)/opaque/nested/fixed": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach react (dependency list)/opaque/nested/fresh": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "preact render reach false/false/fixed": { + "committed move": 7, + "aborted move": 7 + }, + "preact render reach false/false/fresh": { + "committed move": 7, + "aborted move": 7 + }, + "preact render reach false/true/fixed": { + "committed move": 7, + "aborted move": 7 + }, + "preact render reach false/true/fresh": { + "committed move": 7, + "aborted move": 7 + }, + "preact render reach true/false/fixed": { + "committed move": 7, + "aborted move": 7 + }, + "preact render reach true/false/fresh": { + "committed move": 7, + "aborted move": 7 + }, + "preact render reach true/true/fixed": { + "committed move": 7, + "aborted move": 7 + }, + "preact render reach true/true/fresh": { + "committed move": 7, + "aborted move": 7 + }, + "pending reach preact/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach preact/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach preact/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach preact/fresh": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 6, + "callback update settled": 9 + }, + "window reach preact/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/changing/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/changing/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/stable/direct/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach preact/stable/direct/fresh": { + "source write": 11, + "stable key": 13 + }, + "window reach preact/opaque/direct/fixed": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach preact/opaque/direct/fresh": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach preact/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/changing/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/changing/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach preact/stable/nested/fixed": { + "source write": 13, + "stable key": 13 + }, + "window reach preact/stable/nested/fresh": { + "source write": 11, + "stable key": 13 + }, + "window reach preact/opaque/nested/fixed": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "window reach preact/opaque/nested/fresh": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + }, + "pending reach vue/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach vue/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach vue/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach vue/fresh": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 8, + "callback update settled": 9 + }, + "window reach vue/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach vue/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach vue/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach vue/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "pending reach solid/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach solid/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach solid/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach solid/fresh": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 4, + "callback update settled": 9 + }, + "window reach solid/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach solid/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach solid/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach solid/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "pending reach angular/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach angular/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach angular/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach angular/fresh": { + "publication callback entered": 9, + "offset requested in callback": 5, + "limit requested in callback": 7, + "callback update settled": 9 + }, + "window reach angular/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach angular/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach angular/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach angular/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "pending reach svelte/fixed": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "pending reach svelte/fresh": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + }, + "publication callback reach svelte/fixed": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + }, + "publication callback reach svelte/fresh": { + "publication callback entered": 9, + "offset requested in callback": 7, + "limit requested in callback": 4, + "callback update settled": 9 + }, + "window reach svelte/derived/direct/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach svelte/derived/direct/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach svelte/derived/nested/fixed": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "window reach svelte/derived/nested/fresh": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + }, + "ivm.topk-numeric-window/array/offset fixed": { + "large offset move returned": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/grouped/offset fixed": { + "large offset move returned": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/array/offset random": { + "large offset move returned": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/grouped/offset random": { + "large offset move returned": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/array/width fixed": { + "large width move returned": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/grouped/width fixed": { + "large width move returned": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/array/width random": { + "large width move returned": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/grouped/width random": { + "large width move returned": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/array/partial fixed": { + "partial move returned to installed rows": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/grouped/partial fixed": { + "partial move returned to installed rows": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/array/partial random": { + "partial move returned to installed rows": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/grouped/partial random": { + "partial move returned to installed rows": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/array/demand fixed": { + "empty installed range changed demand": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/grouped/demand fixed": { + "empty installed range changed demand": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/array/demand random": { + "empty installed range changed demand": 100, + "returned to installed rows": 100 + }, + "ivm.topk-numeric-window/grouped/demand random": { + "empty installed range changed demand": 100, + "returned to installed rows": 100 + } +} diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-round-2-manifest.json b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-round-2-manifest.json new file mode 100644 index 0000000000..da120df9f6 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-round-2-manifest.json @@ -0,0 +1,76 @@ +{ + "base_head": "c351fc04d7f70dc59bf92ecbed310f899b9511cd", + "branch": "codex/evaluate-2291-window", + "review_entry_manifest_sha256": "69f3395bb34456601fbfbfbd5537a9185b6060952f829f6a65c0d60c24f60985", + "sources_sha256": { + "packages/angular-db/src/index.ts": "b856d0a98c47071691583a370edffc25764d529fa33fecc28c383ed54deb3fe8", + "packages/db-ivm/src/operators/topKArray.ts": "ff21d0f944043db4d28c9be7c429594d668763f634443801a8dc65188ee8d9f1", + "packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts": "1ef076cdca0976323847511681d6d4c75a25bd2c345377a8df4b1d52a9625577", + "packages/db/src/live-query-options.ts": "0a049edd35b0b2f8c803aacc8457df9ff619c2f356f6a234660674487d03a368", + "packages/db/tests/conformance/contract.ts": "b6f345576497c94aeb2720b11ff188a55f27b74905ae2c69ec42e9421f232b41", + "packages/db/tests/conformance/lagging-source.ts": "19feb3b6f87e89fd60cf22605f81c755d94e43db39a11a19e1d49f42accc1097", + "packages/db/tests/conformance/suite.ts": "be7929e6d0d0838f4214c7af473c77b91d32741b347d91d20041a5f8c790ebd2", + "packages/db/tests/conformance/window-history-oracle.ts": "531dbc269de8663de5ba5c7eb4b70f94b8056e2d438ca0960122588991b25168", + "packages/db/tests/failure-fidelity.test.ts": "5072d609cde0a399f41e24e90929377497ec4a7f8276dfbab520ce3a4bd0bf64", + "packages/db/tests/oracle-config.ts": "3de8b1bbfdf9d99ed9c85f52f24411e49da5b9cdc99bee7e82912da4cdeac92a", + "packages/db/tests/oracle-failure-fidelity.ts": "f95203e1c3f7d9f256f10862dd7b9627e85879321fd82b214a3f40ad01ed6901", + "packages/preact-db/src/useLiveQuery.ts": "fe381f9c87cfdd5b57e9cc9ff77a6d1dbfa1fc84191c69fa7247fa9953223f0d", + "packages/preact-db/tests/window-render-oracle.test.tsx": "994f5702a0ff4649369f44bc7ea8341519b19c0260c887b458b97b3f951c06a1", + "packages/react-db/src/useLiveQuery.ts": "46684d6f8365237d49656fe0cbfa87439178bf9c3edfa2468f56dbf26d16cb66", + "packages/react-db/tests/window-render-oracle.test.tsx": "819359c432c167bf2f23462c6fa8e63de443569aba66a6838def2183eca539cb", + "packages/solid-db/src/useLiveQuery.ts": "8e414683931992f72d3208d1f5952d2f33625eac7b0b88aaf5755a210d19ded2", + "packages/svelte-db/src/useLiveQuery.svelte.ts": "23e316c6515bcbdd159121c9497911075ed3d54670a6d7b19a76089eef2212bc", + "packages/vue-db/src/useLiveQuery.ts": "5c4d910952e54b58002459b21d86c9ba6c25fcc67e7cce677e0c6b66b7aecc3c" + }, + "production_changes_during_round": [], + "focused_shared_test_counts": { + "react": 44, + "preact": 22, + "vue": 10, + "solid": 10, + "angular": 10, + "svelte": 10 + }, + "focused_receiving_test_counts": { + "react": 24, + "preact": 24 + }, + "failure_fidelity_test_count": 5, + "fresh_numeric_review_test_count": 16, + "total_distinct_focused_tests": 175, + "logs_sha256": { + "review-2291/fresh-react-primary-fidelity-finalized-calibration.log": "2d2e3117792955f7ef3a1e47abb4251128619dbad1fb090714c23287759a3565", + "review-2291/fresh-receiving-preact-finalized-green.log": "ab655316938bc245419f6a347ddb049cdabe94a195b7b54b5399f67d0b46a240", + "review-2291/fresh-receiving-preact-finalized-types.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "review-2291/fresh-receiving-react-finalized-green.log": "becc7248e81a9152303b0673685124fda40f98f6be0fd7216481368f8c59f266", + "review-2291/fresh-receiving-react-finalized-types.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "review-2291/round2-fidelity-direct-replay-green.log": "cc6aad51a9f7487b2febcaee83b215ae4fb8ec13741b0c73be690187e1579664", + "review-2291/round2-fidelity-eslint.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "review-2291/round2-final-angular.log": "f8250e67be0809523db781eda03fc0ae75f0aee9dd94da2e8880fbd878a706bc", + "review-2291/round2-final-failure-fidelity.log": "5d29d562c10813aab5d46e5d8e84b76ab5893d789e454bb1ef2e262125f33dc1", + "review-2291/round2-final-preact.log": "5e46f2f358f2625fab76a69fb3b935e9020793167d6940887f18f8dc914e6ab8", + "review-2291/round2-final-react.log": "3ded791e9033f95dd34c3d47dd8e1e67b7a223551a4dfefabeb96213f9b56252", + "review-2291/round2-final-solid.log": "938ab637dbdec588aa31c8cedc09405966490c2d7782f554243de5c75681b6bd", + "review-2291/round2-final-svelte.log": "7ed5edc95ac3fb04358756ebd2ba8ae14eeb9f6585c64056f4d2e3cc0f1c7023", + "review-2291/round2-final-vue.log": "1e575c23495bdc72c6291f92f9f3807a6c31953ca815726710b7ce1607fd8147", + "review-2291/round2-shrink-calibration-driver.ts.txt": "4d648d742c86d033e0c560a54cde72a9ee2126abf15256c23d1c04ded21b0461", + "review-2291/round2-shrink-fidelity-calibration-green.log": "3c50f086c2fb38f000c2c90e2fae003b0dce65222f875d2abe3af4f793142ea9", + "review-2291/round2-shrink-fidelity-calibration-red.log": "fffd2b2fa5bef77ceea8712a423bb0eb8a0773b08e808574b3fb6a5f25d1ceab", + "review-2291/subscription-churn-old-check-red.log": "6b2678e6f6ee6bfaed4acd08f93d70f6b22616ebd3d74019dedb4b14d9bdb305", + "review-2291/round2-frozen-lint.log": "89d7da780deb04850db632f478bc4155d23659849c0e5bec6d114c74dc9d5168", + "review-2291/round2-frozen-db-types.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "reach_sha256": { + "2026-10-30-adapter-page-windows-round-2-reach.json": "eb61bdd2b2d30e3d6fd89d45165d77cd36876c00c3cdf5c0dae24f9c8f4489a9" + }, + "validation_exit_codes": { + "pnpm lint:changed": 0, + "core test TypeScript": 0, + "React TypeScript": 0, + "Preact TypeScript": 0, + "git diff --check": 0 + }, + "review_record_sha256": { + "2026-10-30-adapter-page-windows-round-2.md": "d19a8799a1dacc9e57cf8f6d793e33fc0f8f8212688208f2641a6acc1faf0cb9" + } +} diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-round-2-reach.json b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-round-2-reach.json new file mode 100644 index 0000000000..bcb093068d --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-round-2-reach.json @@ -0,0 +1,1894 @@ +[ + { + "log": "review-2291/round2-final-react.log", + "path": "pending reach react/fixed ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "pending reach react/fresh ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "publication callback reach react/fixed ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "publication callback reach react/fresh ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 5, + "limit requested in callback": 8, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/derived/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/derived/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/changing/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/changing/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/stable/direct/fixed ", + "counts": { + "source write": 13, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/stable/direct/fresh ", + "counts": { + "source write": 10, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/opaque/direct/fixed ", + "counts": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/opaque/direct/fresh ", + "counts": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/derived/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/derived/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/changing/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/changing/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/stable/nested/fixed ", + "counts": { + "source write": 13, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/stable/nested/fresh ", + "counts": { + "source write": 12, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/opaque/nested/fixed ", + "counts": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react/opaque/nested/fresh ", + "counts": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "pending reach react (dependency list)/fixed ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "pending reach react (dependency list)/fresh ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "publication callback reach react (dependency list)/fixed ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "publication callback reach react (dependency list)/fresh ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 6, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/derived/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/derived/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/changing/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/changing/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/stable/direct/fixed ", + "counts": { + "source write": 13, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/stable/direct/fresh ", + "counts": { + "source write": 12, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/opaque/direct/fixed ", + "counts": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/opaque/direct/fresh ", + "counts": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/derived/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/derived/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/changing/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/changing/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/stable/nested/fixed ", + "counts": { + "source write": 13, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/stable/nested/fresh ", + "counts": { + "source write": 13, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/opaque/nested/fixed ", + "counts": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-react.log", + "path": "window reach react (dependency list)/opaque/nested/fresh ", + "counts": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "pending reach preact/fixed ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "pending reach preact/fresh ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "publication callback reach preact/fixed ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "publication callback reach preact/fresh ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 6, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/derived/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/derived/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/changing/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/changing/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/stable/direct/fixed ", + "counts": { + "source write": 13, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/stable/direct/fresh ", + "counts": { + "source write": 11, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/opaque/direct/fixed ", + "counts": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/opaque/direct/fresh ", + "counts": { + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/derived/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/derived/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/changing/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/changing/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/stable/nested/fixed ", + "counts": { + "source write": 13, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/stable/nested/fresh ", + "counts": { + "source write": 11, + "stable key": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/opaque/nested/fixed ", + "counts": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-preact.log", + "path": "window reach preact/opaque/nested/fresh ", + "counts": { + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "source write": 13, + "source growth": 13, + "opaque key change": 13 + } + }, + { + "log": "review-2291/round2-final-vue.log", + "path": "pending reach vue/fixed ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-vue.log", + "path": "pending reach vue/fresh ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-vue.log", + "path": "publication callback reach vue/fixed ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-vue.log", + "path": "publication callback reach vue/fresh ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 6, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-vue.log", + "path": "window reach vue/derived/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-vue.log", + "path": "window reach vue/derived/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-vue.log", + "path": "window reach vue/derived/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-vue.log", + "path": "window reach vue/derived/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-solid.log", + "path": "pending reach solid/fixed ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-solid.log", + "path": "pending reach solid/fresh ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-solid.log", + "path": "publication callback reach solid/fixed ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-solid.log", + "path": "publication callback reach solid/fresh ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 5, + "limit requested in callback": 7, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-solid.log", + "path": "window reach solid/derived/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-solid.log", + "path": "window reach solid/derived/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-solid.log", + "path": "window reach solid/derived/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-solid.log", + "path": "window reach solid/derived/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-angular.log", + "path": "pending reach angular/fixed ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-angular.log", + "path": "pending reach angular/fresh ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-angular.log", + "path": "publication callback reach angular/fixed ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-angular.log", + "path": "publication callback reach angular/fresh ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-angular.log", + "path": "window reach angular/derived/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-angular.log", + "path": "window reach angular/derived/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-angular.log", + "path": "window reach angular/derived/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-angular.log", + "path": "window reach angular/derived/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-svelte.log", + "path": "pending reach svelte/fixed ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-svelte.log", + "path": "pending reach svelte/fresh ", + "counts": { + "initial loading move": 7, + "initial oldest settled while latest pending": 7, + "initial ready": 7, + "overlap pending": 7, + "oldest settled while latest pending": 7, + "settled latest": 7, + "unmounted pending": 7 + } + }, + { + "log": "review-2291/round2-final-svelte.log", + "path": "publication callback reach svelte/fixed ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 6, + "limit requested in callback": 5, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-svelte.log", + "path": "publication callback reach svelte/fresh ", + "counts": { + "publication callback entered": 9, + "offset requested in callback": 4, + "limit requested in callback": 7, + "callback update settled": 9 + } + }, + { + "log": "review-2291/round2-final-svelte.log", + "path": "window reach svelte/derived/direct/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-svelte.log", + "path": "window reach svelte/derived/direct/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-svelte.log", + "path": "window reach svelte/derived/nested/fixed ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/round2-final-svelte.log", + "path": "window reach svelte/derived/nested/fresh ", + "counts": { + "offset move": 13, + "limit move": 13, + "large offset move": 13, + "large limit move": 13, + "filter change": 13, + "order change": 13, + "nested window change": 13, + "source change": 13, + "projection change": 13, + "Collection option change": 13, + "source write": 13, + "source growth": 13 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/false/offset/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/false/offset/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/false/offset/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/false/offset/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/false/limit/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/false/limit/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/false/limit/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/false/limit/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/false/both/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/false/both/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/false/both/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/false/both/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/true/offset/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/true/offset/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/true/offset/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/true/offset/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/true/limit/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/true/limit/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/true/limit/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/true/limit/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/true/both/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/true/both/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach false/true/both/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach false/true/both/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/false/offset/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/false/offset/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/false/offset/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/false/offset/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/false/limit/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/false/limit/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/false/limit/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/false/limit/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/false/both/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/false/both/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/false/both/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/false/both/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/true/offset/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/true/offset/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/true/offset/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/true/offset/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/true/limit/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/true/limit/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/true/limit/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/true/limit/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/true/both/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/true/both/fixed ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "render reach true/true/both/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-react-finalized-green.log", + "path": "window render reach true/true/both/fresh ", + "counts": { + "suspended move": 7, + "cancelled move": 7, + "committed move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/false/offset/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/false/offset/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/false/limit/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/false/limit/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/false/both/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/false/both/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/true/offset/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/true/offset/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/true/limit/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/true/limit/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/true/both/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach false/true/both/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/false/offset/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/false/offset/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/false/limit/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/false/limit/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/false/both/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/false/both/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/true/offset/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/true/offset/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/true/limit/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/true/limit/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/true/both/fixed ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + }, + { + "log": "review-2291/fresh-receiving-preact-finalized-green.log", + "path": "preact render reach true/true/both/fresh ", + "counts": { + "committed move": 7, + "aborted move": 7 + } + } +] diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-round-2.md b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-round-2.md new file mode 100644 index 0000000000..842371be46 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows-round-2.md @@ -0,0 +1,114 @@ +# Independent page-window review, round 2 + +The maintainer requested several fresh subagents to use `oracle-review` on the +oracle changes. Four independent reviewers examined contract/model authority, +shared histories and replay, real framework receiving cuts, and numeric TopK +windows. They initially worked read-only and did not receive a desired verdict. +Each read the skill, rubric, glossary, relevant coverage owners and source +authority. Reviewers who examined live-query behavior also read its architecture. + +Base HEAD: `c351fc04d7f70dc59bf92ecbed310f899b9511cd`. The reviewed uncommitted +snapshot is identified by the existing source manifest, whose SHA256 at review +entry was `69f3395bb34456601fbfbfbd5537a9185b6060952f829f6a65c0d60c24f60985`. +All 113 source and evidence hashes matched before review repairs. The prior +record and manifest describe that snapshot; later repairs do not change its +historical results. + +## Findings at review entry + +| ID | Finding and authority | Smallest distinguishing witness and evidence | +| --- | --- | --- | +| R2-1 | P2, retained upstream subscriptions were under-observed. `contract.ts` promises subscription retention; owner-Collection snapshots check only continuous owner presence. | Offset 0 to 1 at width 1 retains Collection C, but replaces its source subscription by adding before removing. Source subscription count is 1, 2, 1 while deduplicated owners stay `[C]`. Source-inspected enforcement gap, independently found by two reviewers. No production defect claimed. | +| R2-2 | P2, shared shrinking can replace the first violated law/checkpoint (ORC-010). | A disposable checker-calibration driver used the real grammar and `runWindowHistory`. Seed 2291 first failed ordered rows for ranks `[16,0,3,2,8]`; nine shrinks reduced to empty rows and a Collection-retention failure at path `0:0:0:0:0:0:0:0:0:0`. The original row mismatch was absent from the final error. Executed wrong-observation calibration; not a production mutant. | +| R2-3 | P2, speculative resize paths were absent. The commit-isolation law covers limit and offset, but both React and Preact receiving histories kept limit fixed. | Commit offset 0/limit 1, attempt a suspended or aborted offset 0/limit 2 render, then inspect the captured committed Collection. A design that defers offset but changes limit during render can pass the old histories. Source-inspected grammar gap; no production defect claimed. | +| R2-4 | P2, a React action error can erase an earlier captured mismatch (ORC-010). | The cancelled-cut comparison saves wrong rows, then a committed `act` rejects before the final AggregateError is constructed. The later action replaces the saved mismatch. Source-inspected reporting gap. | +| R2-5 | P2, process: initial-loading, Collection-option and Preact aborted-render coverage entered after production changed. | The first preproduction gate names shared and React sources, not the later Preact receiver. The prior evidence record accurately discloses the late coverage. Later GREEN/mutant results do not satisfy the original pre-edit RED gate. This historical ordering cannot be repaired retrospectively. | +| R2-6 | P2, held-window keyed state was not compared. The shared binding contract requires keyed state to follow the same public snapshot as data. | While an acquisition is held, an adapter could expose the requested page in `state` but leave the last settled page in `data`. The old pending comparison read only data and Collection identity, so it could accept this wrong observation. Source-inspected gap found during repair follow-up; no production defect claimed. | + +The contract/model reviewer found no authority or independent-model defect +inside the declared scope. The receiving reviewer confirmed that all six shared +drivers call real public hooks/injectors and that React and Preact prove their +speculative/aborted premises. Fresh focused receiving checks passed 8 React and +8 Preact tests before repairs. + +The numeric reviewer found no actionable issue. Its fresh focused run passed all +16 numeric operator/profile/campaign cells, each with 100 histories at both +declared states. Current production/oracle hashes matched the numeric evidence +record. Timeout/runtime REDs remain distinct from assertion rejection. + +The exact fresh numeric command selected `-t 'Numeric window oracle'` in +`packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts`, with +coverage and typechecking disabled. It passed 16 tests and skipped 6, exit 0; +all 16 operator/profile/campaign cells reached both declared states 100 times. +This was a fresh reviewer execution, not a rerun of the earlier 172-test log. + +## Repair follow-up + +The oracle-authoring skill governed the follow-up. All eight production sources +match their review-entry hashes. The [final source manifest](2026-10-30-adapter-page-windows-round-2-manifest.json) +identifies 18 package sources and the supporting evidence; the original +manifest remains unchanged. + +| Finding | Repair and calibration | +| --- | --- | +| R2-1 | Record each public subscription-count transition as well as continuous owner presence. Positive transitions during a retained page move fail; retired-Collection GC decrements remain legal. The old checker accepted the injected 1→2→1 replacement, causing the calibration to fail with `expected [Function] to throw an error`. The repaired shared checker rejects that replacement and accepts the GC control. | +| R2-2 | Mark each law/checkpoint and keep the first failure, original input trace and secondary diagnostics throughout shrinking. The original calibration changed from ordered rows to retention after nine shrinks. The repaired calibration retains `ordered window rows` at `input 0`, reducing to ranks `[1]` after fourteen shrinks. Non-reproducing candidates are rejected as reductions; they are not passing product evidence. | +| R2-3 | Run separate offset-only, limit-only and combined campaigns through both real receivers, eager/on-demand sources, and derived/explicit keys. Keys contain both bounds. Every category proves its own speculative/aborted and committed cuts. A wrong-observation control through the shared checker rejects premature width changes. | +| R2-4 | Keep later React action/premise failures after captured comparison failures. A temporary wrong cancelled-row observation followed by `late host update fault` remains RED at `ordered window rows / cancelled`, seed 2291, path `0:0:0`, with the host fault separate. Source bytes were restored exactly. | +| R2-5 | Remains a historical process gap. No retrospective result is described as satisfying the original pre-edit gate. | +| R2-6 | Compare pending keyed-map values with the same model rows at every held and settled cut. A shared calibration keeps valid data/Collection identity while supplying an empty map; the map check rejects it and accepts the model-derived map. | + +An independent follow-up reviewer also found **R2-7**, a defect in the new +failure-reporting helper during its construction. For the actual helper, +`fc.integer({ min: 0, max: 1 })`, seed 0, one run executed inputs `[1, 0]`. +Input 1 failed the marked row law; input 0 failed setup. Fast-check retained the +earlier matching error, whose diagnostic snapshot omitted the final setup +failure. The helper now retains live secondary evidence and finalizes rendered +diagnostics after shrinking, while preserving fast-check's seed/path report. +The five support tests include this final-candidate case, initial setup failure, +cleanup diagnostics, primary-versus-secondary matching, and rendered evidence. +The follow-up reviewer executed all five and accepted the correction. + +## Final validation and independent follow-up + +| Focused suite | Passing tests | +| --- | ---: | +| Shared React / Preact / Vue / Solid / Angular / Svelte | 44 / 22 / 10 / 10 / 10 / 10 | +| React / Preact receiving | 24 / 24 | +| Failure-fidelity support | 5 | +| Fresh numeric reviewer run | 16 | +| Total distinct tests in this review | 175 | + +Shared runs selected `-t 'window histor'` in each package's real conformance +test, with coverage/typechecking disabled and console interception disabled. +React/Solid use `tests/conformance.test.tsx`; Preact/Vue/Angular use +`tests/conformance.test.ts`; Svelte uses `tests/conformance.svelte.test.ts`. +Receiving runs selected their complete `tests/window-render-oracle.test.tsx`. +Support tests ran in `@tanstack/db` as `tests/failure-fidelity.test.ts`. + +The [per-path reach record](2026-10-30-adapter-page-windows-round-2-reach.json) +contains 164 measured records. Ordinary shared states reach 13 histories per +lane/campaign. Pending states reach 7. Publication histories run 9 cases per +campaign and report offset/limit premises separately. Every receiving +category/source/key/campaign reaches each required cut 7 times: six generated +histories plus its shared-model example. No run budget was lowered. + +Direct React replay of `framework.window-history`, seed `2121258135`, path `0`, +selection `derived/direct window histories`, passed its two fresh registrations; +fixed/control tests stood aside. The calibration-driver archive and RED/GREEN +logs are hashed in the manifest. No production mutant was needed in this +follow-up: the hostile observations exercise the same model/comparisons. + +Core test TypeScript and both receiving-package TypeScript checks passed. +Focused lint and changed-file lint passed. React retains three `require-await` +warnings in its native `act` callbacks; these are not lint errors. Whitespace +validation passed. + +The independent follow-up reviewer read the frozen subscription, receiving, +failure-fidelity and pending-map repairs and verified their final hashes. It +reported no remaining finding in that scope. This is an oracle review result, +not a whole-PR readiness verdict. R2-5 remains disclosed. + +Limits from the original page-window and numeric records remain in force: +bounded query shapes and host cuts, controlled acquisition settlement, and no +claim about arbitrary provider delivery, all framework schedules or CPU bounds. diff --git a/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows.md b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows.md new file mode 100644 index 0000000000..b396937ff3 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-adapter-page-windows.md @@ -0,0 +1,219 @@ +# Adapter page windows + +Authority: the maintainer's 2026-10-30 decision for +[issue 2291](https://github.com/TanStack/db/issues/2291): changing only +`.limit()` and `.offset()` moves the same window. This repairs existing behavior +and adds no hook option. The earlier evaluation's proposed window API was +superseded by that decision. + +Base HEAD: `c351fc04d7f70dc59bf92ecbed310f899b9511cd`. Evidence concerns the +working-tree sources identified in the [source manifest](2026-10-30-adapter-page-windows-manifest.json), not an unmodified +checkout of that commit. + +## Law and boundary + +An invalidated adapter-owned ordered list input with the same structured plan, +source objects and Collection options retains its live-query Collection and +upstream ownership when its finite outer offset or limit changes. Changed +predicates, order, projection, sources, nested bounds or Collection options +compile normally. An unchanged explicit key keeps its authored input. + +A window move requires a committed host update and runs after any current +Collection publication. A speculative render cannot move the committed window. +Pending acquisitions retain the last settled page; superseded acquisition +settlement cannot publish an obsolete page. Core's window and publication laws +remain authoritative. + +The shared owner is `packages/db/tests/conformance/window-history-oracle.ts`, +registered through all six real framework drivers. Its independent model sorts, +filters and slices complete numeric rows. Explicit scalar comparisons identify +authored plan changes; no production identity or transition helper computes the +expected answer. Rows, order, keyed-map values, Collection identity and the +retained Collection's upstream ownership are observed after each framework cut. + +The ownership observation constrains the current retained Collection. An older, +replaced Collection may finish garbage collection during the same step. This +does not measure arbitrary CPU work or every internal compilation strategy. + +React and Preact add receiving witnesses that reuse the shared model/checker. +React suspends and abandons a transition before committing another page. +Preact throws after the hook under a real error boundary. A public subscription +keeps its captured Collection active across the fallback. + +## Experiment order + +1. The original oracle extension ran against unchanged production. The saved + preproduction gates record an empty production diff and oracle hashes. + Every adapter rejected Collection replacement. The held-acquisition lane + reached oldest settlement while the latest request remained pending before + production changed. React reached suspended, cancelled and committed cuts. +2. Three independent reviewers used `oracle-review`. Their findings added direct + and nested lanes, source swaps under one explicit ID, projection changes, + source liveness, keyed state, ownership recording, stable/opaque key controls, + and cleanup that preserves the original mismatch. +3. Production retained the existing Collection and called its existing + `setWindow`. Later audits added initial-loading moves, Collection-option + variation and Preact's aborted-render witness. These are late coverage: + their GREEN and mutant results do not retroactively extend the first gate. +4. A new shared publication-callback history exposed `SetWindowReentrancyError` + in Solid, Preact, Angular and Svelte before its repair. React and Vue passed + that same history. Queuing committed moves after publication repaired it. +5. [Large numeric windows](2026-10-30-topk-numeric-windows.md) exposed the existing TopK numeric-range enumeration + and partial-bound inheritance defects. Both the primary TopK owner and real + framework receiving paths were extended before the TopK repair. + +Setup errors are excluded from defect evidence. These include an early provider +fixture with unsupported ordering, nested React `act` scopes, and an initial +Preact subscription-cleanup mistake. A direct render-time mutation experiment +also hung and was terminated; the later single-fault injection supplies the +intended assertion failure instead. + +## Grammar and observations + +The shared grammar varies empty and nonempty sources, ties, direct and nested +queries, offset/limit including zero and out-of-range pages, predicates, order, +projection, nested bounds, source objects and source writes. Bare callback lanes +vary `gcTime` together with a page change. Stable keys and opaque keyed inputs +have separate controls. Large-window histories grow the source after a move, +so bounds clamped permanently to installed row count are distinguishable. + +The pending grammar covers moves before initial readiness, an old acquisition +settling while the latest remains held, overlapping ready-page moves, final +settlement and unmount before completion. Its provider controls acquisition +settlement over already installed rows. It does not simulate delayed delivery. + +The callback grammar changes offset, limit or both during a public Collection +change callback, proves that callback entry occurred on the publication stack, +then compares the retained Collection after the native update settles. + +These axes reconstruct the reported page-change trace and adjacent wrong +designs. Removing any plan/source/option distinction admits unsafe reuse; +removing source writes loses liveness; removing pending or pre-commit cuts loses +temporal isolation. Bounds are nonnegative safe integers; negative and +fractional windows are excluded. Bounded runs do not prove all legal histories. + +## Calibration and replay + +Hostile edits were temporary and exact source bytes were restored in `finally`. + +| Wrong design | Observed rejection | +| --- | --- | +| Rebuild for a page change | Collection-retention assertion | +| Compare source IDs without source objects | Wrong selected rows on a same-ID swap | +| Ignore Collection-option differences | Replacement assertion on an invalidated page input | +| Reuse only after initial readiness | Retention assertions during initial loading | +| Apply one move during React render | Wrong committed rows at the suspended cut | +| Apply the Preact move during render | Wrong captured rows after the aborted render | +| Apply synchronously during publication | Rejected callback update with `SetWindowReentrancyError` | +| Infer changed demand only from installed rows | Missing demand-message assertion | +| Recover an omitted limit from rounded end minus offset | Wrong rows after a partial move | +| Clamp stored bounds permanently to installed row count | Wrong rows after source growth | + +Direct replay selected only fresh campaigns. The primary retained-window +failure replayed with seed `2121258135`, path `0`, property +`framework.window-history`, then passed after restoration. Pending and both +render properties also ran direct seed/path replays. The publication-callback +RED replay used seed `1915472220`, path `0`. + +Example command, run at repository root: + +This command records the original property name. On the current branch, use +`framework.react.window-history`; the [replay review](2026-10-30-adapter-page-window-replay.md) +records the later driver-scoped names. + +```sh +TANSTACK_DB_ORACLE_PROPERTY=framework.window-history \ +TANSTACK_DB_ORACLE_SEED=2121258135 TANSTACK_DB_ORACLE_PATH=0 \ +pnpm --filter @tanstack/react-db exec vitest run tests/conformance.test.tsx \ + -t 'derived/direct window histories' --coverage.enabled=false \ + --typecheck.enabled=false --disableConsoleIntercept --silent=false +``` + +## Limits + +DbClient/hydration and Suspense identify shared results by complete query +identity. This repair does not retarget those shared Collections. Supplied +Collections, opaque-plan optimization and `findOne` keep their existing paths. +An ordered query with an explicit `.limit(Infinity)` can retain its Collection +when it moves to a finite safe-integer target. Other unbounded transitions keep +their existing paths. The retention evidence below covers finite windows; it +does not establish the additional Infinity-to-finite domain. These ownership +and API boundaries do not establish reuse for every compiled result. + +The model covers finite numeric rows and selected projections, not every query +operator or arbitrary concurrent schedule. Core ordered-window owners retain +delivery, cancellation, failure, repair and progressive-includes obligations. +The controlled acquisition fixture supplies no real-provider conformance claim. +No whole-PR readiness or universal bug-class proof is asserted. + +## Oracle audit + +| Requirement | Outcome | +| --- | --- | +| ORC-001 | Maintainer authority, existing identity law and core publication laws stated above; scope explicit. | +| ORC-002 | Scalar authored-plan comparison and full sort/filter/slice use no production classifier. TopK uses an independent rank predicate and exact requested bounds. | +| ORC-003 | Contract, model, grammar, real drivers and checkpoints have adjacent prose in executable owners. | +| ORC-004 | Reconstruction, ablation, bounded values and excluded invalid windows described above; primary TopK profiles isolate separate numeric failure modes. | +| ORC-005 | All six real hooks/injectors compare data, state, identity and ownership at native cuts. Suspended/aborted render and pending-acquisition premises are asserted. | +| ORC-006 | Original defects and the named hostile edits reach the intended comparisons; TopK nontermination is classified separately. | +| ORC-007 | Fixed/fresh campaigns share grammar, recorder, checker and budget. Seed/path replays select only their requested fresh targets. | +| ORC-008 | Settled and requested windows differ during acquisition; exact authored offset and limit differ after partial moves. The model retains those distinctions. | +| ORC-009 | Glossary terms reused; full recomputation abstracts the incremental graph explicitly. | +| ORC-010 | ScenarioLifetime preserves original and cleanup errors. Drivers aggregate later action errors with earlier comparison mismatches; numeric shrinking preserves observation and move boundary. | +| ORC-011 | No additional independent-formulation obligation identified by the three reviewers. Numeric rank membership specifically avoids shared end-point addition. | +| ORC-012 | This record, numeric evidence and source/reach manifests preserve the review and its limits. The late-coverage timing remains explicit. | +| ORC-013 | Page versus plan/source/options changes, zero/empty/beyond-source windows, source growth, partial options and pending cuts distinguish plausible wrong boundaries. | +| ORC-014 | Real framework drivers receive the shared model. Providers are controlled; no external-provider conformance is claimed. | +| ORC-015 | Each driver/form/campaign asserts declared reach. Exact final counts are in the accompanying reach record. | +| ORC-016 | Fixed regressions replay the same model, driver and checker; no standalone expected-answer examples added to the oracle owners. | + +Three independent reviewers covered law/ownership, grammar/calibration, and +framework receiving cuts. Their supported enforcement findings are addressed in +the histories, observations and repairs described here. The initial unreviewed +proposal for a new API has no current implementation or authority. + +## Final evidence block + +- **Law:** finite structured outer page changes retain an adapter-owned window; + authority and ownership boundary are stated above. Exact authored bounds also + survive partial TopK moves, including large safe-integer values. +- **Reach:** [exact counts by path/form/campaign/state](2026-10-30-adapter-page-windows-reach.json). + Shared ordinary campaigns run 13 histories each; pending states reach 7 each; + publication callbacks run 9 each; pre-commit states reach 7 each. Numeric + operator states reach 100 each in both operators and both campaigns. +- **RED:** `vitest run tests/conformance... -t 'window histor'` rejected original + Collection replacement on unchanged base production in every adapter. + Publication-callback RED preceded its queue repair. All six large-window + receivers reported `RangeError: Invalid array length` and wrong empty rows + before TopK changed, including both large moves and source growth. Primary + numeric offset/width moves timed out after explicit move-entry markers; + partial width produced the intended row assertion. Exact commands and error + categories are preserved in the numeric record and supporting log hashes. +- **GREEN:** final selected suites passed 2,321 tests: React 711, Preact 126, + Vue 109, Solid 70, Angular 64, Svelte 270, core 799 and TopK 172. These include + the shared fixed/fresh campaigns, hook neighbors, hydration/Suspense neighbors, + ordered work and deferred acquisition. TypeScript, Svelte checks, lint, + package builds and whitespace validation passed. +- **Mutant:** the ten wrong-design edits listed above failed the intended + row, identity, demand or callback-update checks. Exact seed/path RED/GREEN + replays also passed. Source bytes were restored after every edit. +- **Limits:** the explicit ownership, provider, value-domain and observation + boundaries above remain. Late coverage does not retroactively establish a + wider original pre-edit gate. + +Production-size comparison against the base adds 766 gzip bytes across the +seven measured core/adapter bundles with dependencies external. The typical +tree-shaken React app adds 539 gzip bytes. Svelte uses its own package/compiler +pipeline; the comparable compiled package bundle adds 110 gzip bytes. The size +script cannot build Svelte through Vite, so it was measured separately with the +same Svelte compiler and esbuild settings on both revisions. + +## Subsequent independent review + +A maintainer-requested [fresh four-reviewer audit](2026-10-30-adapter-page-windows-round-2.md) +found missing subscription-count, speculative-width, pending keyed-state and +failure-fidelity distinctions. The follow-up record preserves those findings, +their repairs and the exact later source snapshot. The manifest and results +above remain the historical pre-follow-up evidence; they are not a claim that +the earlier instruments already enforced those distinctions. Production did not +change during the follow-up. The late-coverage process gap remains explicit. diff --git a/docs/contributing/oracle-reviews/2026-10-30-react-ssr-effects-reach.json b/docs/contributing/oracle-reviews/2026-10-30-react-ssr-effects-reach.json new file mode 100644 index 0000000000..fb96b02f1b --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-react-ssr-effects-reach.json @@ -0,0 +1,290 @@ +{ + "head": "bfd1da1a7e752d66fd667adfd3cdf54f60732b22", + "source": "prep-final-react-integration.log", + "generatedPathCampaigns": [ + { + "runtime": "react18", + "path": "ordinary-provider", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35, + "cold": 4, + "prepared-ready": 8, + "prepared-loading": 10, + "rejected": 13 + } + }, + { + "runtime": "react18", + "path": "ordinary-provider", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35, + "cold": 5, + "prepared-ready": 10, + "prepared-loading": 6, + "rejected": 13 + } + }, + { + "runtime": "react18", + "path": "ordinary-explicit", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35, + "cold": 4, + "prepared-ready": 8, + "prepared-loading": 10, + "rejected": 13 + } + }, + { + "runtime": "react18", + "path": "ordinary-explicit", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35, + "cold": 2, + "prepared-ready": 8, + "prepared-loading": 9, + "rejected": 11 + } + }, + { + "runtime": "react18", + "path": "suspense-provider", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "react18", + "path": "suspense-provider", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "react18", + "path": "suspense-explicit", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "react18", + "path": "suspense-explicit", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "react18", + "path": "standalone", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "react18", + "path": "standalone", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "react18", + "path": "supplied", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "react18", + "path": "supplied", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "current", + "path": "ordinary-provider", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35, + "cold": 4, + "prepared-ready": 8, + "prepared-loading": 10, + "rejected": 13 + } + }, + { + "runtime": "current", + "path": "ordinary-provider", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35, + "cold": 6, + "prepared-ready": 6, + "prepared-loading": 7, + "rejected": 12 + } + }, + { + "runtime": "current", + "path": "ordinary-explicit", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35, + "cold": 4, + "prepared-ready": 8, + "prepared-loading": 10, + "rejected": 13 + } + }, + { + "runtime": "current", + "path": "ordinary-explicit", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35, + "cold": 4, + "prepared-ready": 10, + "prepared-loading": 8, + "rejected": 10 + } + }, + { + "runtime": "current", + "path": "suspense-provider", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "current", + "path": "suspense-provider", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "current", + "path": "suspense-explicit", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "current", + "path": "suspense-explicit", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "current", + "path": "standalone", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "current", + "path": "standalone", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "current", + "path": "supplied", + "campaign": "fixed", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + }, + { + "runtime": "current", + "path": "supplied", + "campaign": "random", + "generatedHistories": 24, + "fixedExamples": 11, + "counts": { + "rendered": 35 + } + } + ], + "disabledReplays": { + "react18": { + "kind": "fixed shared-check replay", + "hookCallsPositive": true, + "comparisonsPassed": 1 + }, + "current": { + "kind": "fixed shared-check replay", + "hookCallsPositive": true, + "comparisonsPassed": 1 + } + } +} diff --git a/docs/contributing/oracle-reviews/2026-10-30-react-ssr-effects.md b/docs/contributing/oracle-reviews/2026-10-30-react-ssr-effects.md new file mode 100644 index 0000000000..b02b1dcdc5 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-react-ssr-effects.md @@ -0,0 +1,238 @@ +# React server effect review, 2026-10-30 + +This records prep-pr finding H1 on parent `c351fc04d7f70dc59bf92ecbed310f899b9511cd` +plus the uncommitted page-window change. It is separate from the historical +page-window records. Later main integration and verification append their own +evidence; they do not change the order reported here. + +## Law and repair + +React DB supports React >=18 and prepared server rendering. The SSR guide and +#2229 establish ordinary cold/loading and explicitly prepared server views. +React18 diagnoses `useLayoutEffect` during a server render because that hook +cannot run there. An ordinary legal server render must not acquire that warning +merely because client page moves now have a commit callback. + +The accepted client window law still requires a committed update. Its layout +commit schedules the move; a provider acquisition can remain pending afterward. +The repair chooses `useEffect` on the server and `useLayoutEffect` in a browser, +once when the module loads. Both hooks keep stable ordering. It changes no +public hook API, query identity, preparation judgment or window expectation. + +The original review was source inspection. The previous green SSR comparisons +used React19 and observed rows/status rather than the React18 diagnostic. That +missing receiving runtime and observation explain the test gap. + +## Oracle and real receiving paths + +`packages/react-db/tests/ssr-effects-oracle.test.ts` reuses the shared +`ssr-snapshot-admission-oracle.ts` grammar and independent preparation model. +The server projection removes later server writes and browser actions. The +ordinary grammar retains 26 preparation/source-state/row-count shapes, with +zero through three rows. Completed-query/ready Suspense and standalone source +inputs retain row-count variation. Query preload while idle remains excluded. + +Every runtime has separate campaigns and reach for these receiving paths: + +- ordinary hook under `DbProvider`; +- ordinary hook with its explicit client option; +- prepared non-streaming Suspense under `DbProvider`; +- prepared non-streaming Suspense with its explicit client option; +- standalone builder over a concrete source Collection; +- supplied Collection without a client. + +A disabled hook has one fixed replay through the same driver and checker. It +is not repeated as a generated property. A checker calibration accepts an +unrelated diagnostic and rejects React18's layout-effect diagnostic. + +Ordinary client-backed paths compare rejection or selected rows and status +with the shared preparation model. Suspense exposes no hook status field, so +its prepared receiving path compares selected rows only. Standalone, supplied +and disabled paths judge return and the diagnostic, without adding a transport +or hydration promise. The recorder projects the authored `id`/`name` columns, +as the existing SSR receiving owner does, and rejects malformed selected fields. +Virtual metadata remains outside this shared row judgment. Every diagnostic +stays recorded, while the law rejects only the client-layout diagnostic. + +`ssr-effects-driver.tsx` invokes the real React server renderer in an isolated +Node subprocess. esbuild bundles current production sources and redirects all +React imports, including the renderer's internal imports, to one matched +runtime. Every observation asserts both actual versions: React/ReactDOM18.3.1 +or the installed React/ReactDOM19.2.4. The package aliases are exact development +dependencies; pnpm's ReactDOM18 peer link is not used by the isolated bundle. +No override or installation workaround was required. + +Each path has fixed-seed 2291 and fresh campaigns with 24 generated histories +plus the same 11 shared replay inputs. Each campaign declares reach and requires +its fixed inputs to reach the declared states. Ordinary paths distinguish cold, +prepared ready, source-prepared loading and rejected views. Each other path +requires an actual renderer invocation. Failure fidelity preserves the first +law, path and input through shrinking; cleanup failures stay secondary. + +## Experiment order and results + +Before any SSR production repair, the expanded receiver and the existing real +client window receiver ran on unchanged production: + +```sh +pnpm --filter @tanstack/react-db exec vitest run \ + tests/ssr-effects-oracle.test.ts tests/window-render-oracle.test.tsx \ + --maxWorkers=2 --fileParallelism=false --coverage.enabled=false +``` + +**RED:** 13 assertion failures, all at `SSR client-layout diagnostic`: the six +React18 receiving paths in both campaigns, plus its disabled replay. Each +receiving path ran independently, so the first failure hid no later path. The +cold ordinary replay was `{preparation:"none", sourceState:"idle", rows:[]}`. +The actual diagnostic began: + +> Warning: useLayoutEffect does nothing on the server, because its effect +> cannot be encoded into the server renderer's output format. + +The other 38 tests passed: every React19 receiving campaign and disabled replay, +the diagnostic calibration, and all 24 existing client window receiving tests. +Those client histories include committed, suspended and cancelled render cuts, +with eager/on-demand sources and derived/explicit identity. Thus the receiving +gate covered both the new server diagnostic and the neighboring client law +before the repair. + +**GREEN:** after the isomorphic effect repair, the same selected tests passed: +51 tests in two files. The source comparison and types remained unchanged in +meaning. Later recorder typing and reach-accounting refinements are described +below; the parent runs the complete combined tree after main integration. + +**Mutant:** restoring the original unconditional layout hook on the final +receiver rejected all six fixed React18 campaigns at the intended diagnostic +comparison: six failed, 21 skipped. This was an assertion rejection, not a +setup failure or timeout. The correct source was restored afterward. + +```sh +pnpm --filter @tanstack/react-db exec vitest run \ + tests/ssr-effects-oracle.test.ts --maxWorkers=2 --fileParallelism=false \ + --coverage.enabled=false --disableConsoleIntercept --silent=false \ + -t 'react18 .*fixed' +``` + +**Direct replay:** while that hostile mutant was active, this replay selected +only the requested ordinary-provider history: one diagnostic failure, 26 tests +stood aside. It preserved the original tagged law/path and exact warning. + +```sh +TANSTACK_DB_ORACLE_PROPERTY=react.ssr-effects-react18-ordinary-provider \ +TANSTACK_DB_ORACLE_SEED=2291 TANSTACK_DB_ORACLE_PATH=0 \ +pnpm --filter @tanstack/react-db exec vitest run \ + tests/ssr-effects-oracle.test.ts --maxWorkers=2 --fileParallelism=false \ + --coverage.enabled=false --disableConsoleIntercept --silent=false +``` + +The final React package `tsc --noEmit` and explicit ESLint on the two receiver +files and hook passed. Changed-file lint also passed. Production mutation had +ended before main integration began. + +## Review reconciliation and experiment limits + +Three fresh oracle reviewers challenged authority, grammar/fidelity and the +real receiving boundary. Their repairs were incorporated as follows: + +- Filter the specific layout diagnostic rather than promising silence for every + unrelated message; retain raw messages and calibrate both cases. +- Add explicit-client, prepared Suspense, standalone, supplied and disabled + receiving paths. Separate campaigns ensure every required path runs in RED. +- Preserve first-failure identity and secondary cleanup diagnostics. Use equal + fixed/fresh budgets, direct per-path replay and fixed-input reach. +- Observe Suspense's actual rows rather than inventing a hook status field. +- Keep disabled coverage as a fixed replay, and count `prepared-loading` only + when source preparation actually supplied that state. + +Intermediate experiments found driver defects before the final evidence: +Suspense has no public status field, and source cleanup needed to end dependent +queries. A later type repair briefly recorded whole public rows, whose virtual +metadata disagreed with the shared selected-column model. That run failed 16 +row comparisons and was not GREEN. Restoring the same selected-column recording +with explicit unknown-value checks restored all 51 passes. These were recorder +repairs; no product expectation was changed to match production. Final type and +reach-accounting refinements require the parent's combined-tree verification. + +This evidence covers Node server rendering without DOM globals, through +`renderToString`, at completed render/diagnostic observation. It does not prove +streaming Suspense discovery, hydration equality, arbitrary concurrent schedules, +server execution with browser DOM polyfills, every React release, or absence of +all server-side work. Existing admission, hydration and window owners retain +those laws. No new missing-input hypothesis arose for this diagnostic: the +renderer receives the hook call directly. + +## Oracle rubric record + +| Requirement | Evidence or limit | +| --- | --- | +| ORC-001 | Supported React peer and established prepared SSR; bounded diagnostic and host scope above. | +| ORC-002 | Shared preparation judgment and diagnostic contract do not reproduce resolver/effect branches. | +| ORC-003 | Executable prose states law, projection, grammar, driver, observation and checker. | +| ORC-004 | Shared 26-shape grammar reconstructs cold RED; each axis/path contributes a distinct preparation, data or host boundary; impossible preload/idle combinations excluded. | +| ORC-005 | Actual matched server renderers invoke the public hooks; selected fields and raw diagnostics observed at return. | +| ORC-006 | Unconditional-effect mutant and scoped diagnostic calibration rejected the intended wrong result. | +| ORC-007 | Same 24+11 fixed/fresh campaigns; direct seed2291/path0 replay demonstrated. No command-model replay path applies. | +| ORC-008 | No new stateful reference state; shared stateless preparation judgment unchanged. | +| ORC-009 | Server projection explicitly removes later actions; path names identify real public entry points. | +| ORC-010 | Shared first-failure fidelity plus marked comparisons and secondary cleanup diagnostics. | +| ORC-011 | No plausible shared semantic-classifier fault needing a second model identified; actual renderer versions directly supply the disputed diagnostic. | +| ORC-012 | This chronological record, exact commands and source hashes tie evidence to reviewed sources; final combined reach is appended separately. | +| ORC-013 | Cold/prepared, ready-empty/nonempty, rejected/admitted neighbors; React19 control and unrelated-diagnostic calibration distinguish the boundary. | +| ORC-014 | Both actual installed renderers supply the warning/no-warning premise; real client window owner supplies commit/abandonment neighbors. | +| ORC-015 | Separate runtime/path reach and fixed-input requirement; numeric combined-tree counts appended by the parent. | +| ORC-016 | Fixed replay and calibration use the shared checker; no standalone expected-answer tests added to the oracle. | + +## Frozen sources before main integration + +| File | SHA256 | +| --- | --- | +| `packages/react-db/src/useLiveQuery.ts` | `ffa5a4fa3e68e9dfbfea2ff3b988db852ecc14f688aeeabbad5ac1a79a09b6e9` | +| `packages/react-db/tests/ssr-effects-driver.tsx` | `9cf27fdbe6f1fbd72629d9ba11880ab650421feb58d82ad174891043b53845c5` | +| `packages/react-db/tests/ssr-effects-oracle.test.ts` | `4052260aa9b192c3773e3147a6d31b568152f563ea869f195f9b602ad66555ce` | +| `packages/react-db/package.json` | `2136a1486e15558a8cd8ec57615db17f6aa5e4032b1afe803404bdd83d99ec0a` | +| `pnpm-lock.yaml` | `aad3ef34e9f98129d6929b4b9c46880b8c9758971da14a5686f479824d1549f4` | +| `packages/db/tests/oracle-config.ts` | `c2c3269f5ce27662cf0eb8e5a2b972c6df307d33482b85b7abda0581e8432d63` | + +## Combined-tree verification after main integration + +Main integrated normally at `bfd1da1a7e752d66fd667adfd3cdf54f60732b22` after all +mutants were restored. The frozen source hashes above remained unchanged. +The parent then ran the real receiving suite with console interception disabled: + +```sh +pnpm --filter @tanstack/react-db exec vitest run \ + tests/effect-callback-oracle.test.tsx tests/useLiveQueryEffect.test.tsx \ + tests/window-render-oracle.test.tsx tests/ssr-effects-oracle.test.ts \ + tests/ssr-runtime-error-handoff-oracle.test.tsx \ + --maxWorkers=2 --fileParallelism=false --coverage.enabled=false \ + --typecheck.enabled=false --disableConsoleIntercept --silent=false +``` + +**GREEN:** all 154 receiving tests passed, including the 27 SSR effect tests, +24 client window tests and adjacent SSR runtime-error handoff witnesses. Core +integration checks passed 391 tests, and the separate shared React window +selection passed 44: 589 tests total. Core and React TypeScript checks passed +separately from the runtime command. + +The [durable reach record](2026-10-30-react-ssr-effects-reach.json) contains all +24 runtime/path/campaign cells. Every cell measured 35 renderer invocations: +24 generated histories and 11 fixed inputs. This gives 840 compared server +renders, plus the two disabled fixed replays. Every declared minimum and each +fixed-input reach requirement passed. Ordinary source-prepared loading counts +refer only to actual source preparation; other admitted empty/loading histories +still run the shared comparison and contribute to `rendered`. + +| Runtime | Ordinary path | Campaign | Cold | Prepared ready | Prepared loading | Rejected | +| --- | --- | --- | ---: | ---: | ---: | ---: | +| React18 | Provider | Fixed | 4 | 8 | 10 | 13 | +| React18 | Provider | Fresh | 5 | 10 | 6 | 13 | +| React18 | Explicit client | Fixed | 4 | 8 | 10 | 13 | +| React18 | Explicit client | Fresh | 2 | 8 | 9 | 11 | +| React19 | Provider | Fixed | 4 | 8 | 10 | 13 | +| React19 | Provider | Fresh | 6 | 6 | 7 | 12 | +| React19 | Explicit client | Fixed | 4 | 8 | 10 | 13 | +| React19 | Explicit client | Fresh | 4 | 10 | 8 | 10 | + +The bounded H1 enforcement gap is repaired under the law, histories, real paths +and observations described here. This does not remove the stated host/version +limits or change the original page-window work's historical process finding. diff --git a/docs/contributing/oracle-reviews/2026-10-30-topk-numeric-windows.md b/docs/contributing/oracle-reviews/2026-10-30-topk-numeric-windows.md new file mode 100644 index 0000000000..afefbd3464 --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-30-topk-numeric-windows.md @@ -0,0 +1,179 @@ +# Top-K numeric window evidence + +This record extends the support-window owner during the #2291 adapter-window +repair. The oracle changed before `TopKArray` changed. Reviewed base: +`c351fc04d7f70dc59bf92ecbed310f899b9511cd`. + +## Law and owner + +The fractional-index top-K offset/limit API selects positive support by ordered +rank. Nonnegative safe-integer authored bounds must return a finite installed +result. A partial move keeps the exact authored bound that it omits. An offset +and a width describe the requested window independently of installed support. + +The top-K operator API supplies the selection and partial-options authority. +The live-query architecture's “Ordered requests, continuation, and recovery” +contract requires the graph to revisit changed ordered demand. At this operator +boundary, a changed nonempty requested window emits an output message even when +no installed rank enters or leaves. Additional messages are permitted, including +when both requested windows are empty. This is not a callback-count promise. + +Primary owner: +`packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts`. +The existing signed-support law and its 100-history budgets remain in place. +The added `ivm.topk-numeric-window` property uses the same row-relation checker. + +## Independent model and legal histories + +The model stores authored offset and limit separately. It selects each present +key by `rank >= offset && rank - offset < limit`. It neither adds the bounds nor +enumerates absent ranks. A later offset-only return can therefore distinguish +width two from a rounded width one, even while both earlier installed windows +were empty. + +The generated input varies installed positive typed keys (3–5), distance from +`MAX_SAFE_INTEGER` (0–2), finite width (2–4), and an empty-range gap (1–3). +Each of four profiles runs a shared eight-move history: + +- `offset`: a near-maximum offset with a small width. +- `width`: a near-maximum width from offset zero. +- `partial`: near-maximum offset with width two, followed by offset-only zero. +- `demand`: a finite window beyond installed support. + +Every profile then crosses changed empty installed ranges, width growth, +zero-width windows, an offset-only zero-width change, and a return to installed +rows. The fixed example is an input to this shared history builder, model, +production driver and comparison. Both normal campaigns use the same generator, +recorder, checker and 100-history budget; the fixed seed is 2291. + +Reconstruction includes the reported maximum-offset/width-three hang, huge +width, rounded retained width, and changed-but-uninstalled demand. Removing +near-maximum bounds loses the arithmetic defect; removing partial options loses +the exact-width defect; removing the empty-range gap loses the lost-demand +mutant. Negative, fractional and unsafe authored bounds are excluded. The +production paths are the ordinary and single-group fractional-index top-K +operators. Observations occur after each move returns and its graph run ends: +complete signed indexed output, reported size, and required demand messages. + +## RED before the production change + +Unchanged `TopKArray` blob: +`5de9b6f300ffc1da5e425a63ab11251295ca4b83`. + +The subprocess command was: + +```sh +pnpm --filter @tanstack/db-ivm exec vitest run \ + tests/operators/topk-support-window-oracle.test.ts \ + -t "Numeric window oracle: 'array'.*preserves offset bounds.*fixed campaign" \ + --coverage.enabled=false --typecheck.enabled=false --silent=false +``` + +The watchdog launched this command in a new process group, streamed its output, +and killed that group after a fixed-history move-entry marker failed to return. +The marker was written directly to the subprocess file descriptor so Vitest's +buffered console forwarding could not hide entry. Startup had a separate +20-second bound. The final guarded runs used a 10-millisecond post-entry bound +and a 256-MB V8 heap cap. This bound is a diagnostic stop, not a product CPU-time +threshold. Source inspection separately establishes the offset-three loop: +incrementing `9007199254740992` no longer advances toward its greater end. + +| Path/profile | Actual pre-repair result | +| --- | --- | +| array, offset | Timeout after entering `{offset: 9007199254740991, limit: 3}`; process group killed, exit -9. | +| grouped, offset | Same move-entry timeout, exit -9. | +| array, width | Timeout after entering `{offset: 0, limit: 9007199254740991}`; exit -9. | +| array, partial | Row assertion RED at move 1: one row after offset-only zero, model requires two. Fixed campaign reported seed 2291, path `0:0:0`. | +| grouped, partial | GREEN: its wrapper separately retains the authored width. Both declared reaches were 100. | +| array and grouped, demand | GREEN. The grouped final run reports both declared reaches as 100. | + +These outcomes are distinct. Timeouts did not reach the post-return comparison +and are not assertion kills. Initial offset/width probes exhausted the capped +worker heap; those are runtime failures after setup, outside the intended +comparison. A preliminary command selected no tests and supplies no evidence. +The final timeout logs contain the move-entry marker and no heap failure. + +Raw development logs are retained under `review-2291/topk-numeric-*`. The checked +commands, state markers, failure trace and exact results above preserve their +verdict-critical contents in this versioned record. + +## GREEN, reach and calibration + +The production repair stores the exact authored width, clamps output-difference +intervals to installed support, and derives demand changes from authored bounds. +Production GREEN blob: `85b58a7de32bc881179b2c85955f9f3773d27319`. +Oracle blob: `627eee1e6c8a34eb6b3e5274be42cf405006d4ab`. + +The focused top-K run passed 172 tests in five files. Its numeric lane ran both +operators, all four profiles and both campaigns. In each of those 16 cells, +`createOracleReach` reported 100 histories for the profile-specific completed +move and 100 for the return to installed rows. The fixed example reached each +declared state. The RED timeout has no completed-move reach; its direct entry +marker establishes the attempted boundary instead. + +The original source supplies timeout/runtime and row-assertion RED controls. +Three additional hostile mutants failed intended assertions: + +| Mutant | Rejection | +| --- | --- | +| Derive `changes` only from clipped installed intervals | Both operators and both campaigns fail the demand assertion at move 3: changed nonempty requested windows both select no installed row, but emit no demand message. | +| Recover omitted width as computed end minus offset | The ordinary operator's partial profile fails the row assertion at move 1: one row instead of two. | +| Store clamped bounds as the authored window | The shared React receiving history fails its row comparison when source growth installs rows after a huge-offset or huge-width move. | + +These are assertion kills, not timeout or setup failures. The source was restored +after each mutant. Development logs are `review-2291/mutant-clipped-demand.log`, +`mutant-rounded-width.log`, and `mutant-stored-clamp.log`. + +The direct replay was then checked sequentially against the rounded-width +mutant and repaired production: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=ivm.topk-numeric-window \ +TANSTACK_DB_ORACLE_SEED=2291 \ +TANSTACK_DB_ORACLE_PATH=0:0:0 \ +pnpm --filter @tanstack/db-ivm exec vitest run \ + tests/operators/topk-support-window-oracle.test.ts \ + -t "Numeric window oracle: 'array'.*preserves partial bounds.*random campaign" \ + --coverage.enabled=false --typecheck.enabled=false --silent=false +``` + +The mutant reproduced the row mismatch at move 1, seed 2291 and path `0:0:0`, +with zero additional shrinks. Exactly one replay failed and 21 tests were +skipped; no fixed campaign ran. Byte restoration was checked against production +blob `85b58a7de32bc881179b2c85955f9f3773d27319` before the same replay passed. +Its repaired reach was one for both states. Replay skips normal reach budgets +by design. Logs: `review-2291/topk-numeric-partial-replay-red.log` and +`topk-numeric-partial-replay-green.log`. + +## Rubric audit and limits + +- ORC-001–005: the API and architecture authority, bounded grammar, independent + rank predicate, real operator paths and observation cut are stated above. +- ORC-006: original production is rejected as a timeout/runtime failure and by + the partial-width row assertion. The three mutants fail their intended + demand/row comparisons, as recorded above. +- ORC-007: fixed/random campaigns share mechanics and budget. Registry key + `ivm.topk-numeric-window` accepts seed/path; the checked replay fails against + the rounded-width mutant and passes after exact source restoration. +- ORC-008–009: authored offset and width are independently observable after a + partial move. Installed support and requested window remain separate concepts. +- ORC-010: shrinking preserves the first row/size/demand observation and move + checkpoint. The graph owns no asynchronous resource in this driver; the RED + watchdog kills the entire subprocess group. Heap failures remain separate. +- ORC-011: the named shared-overflow hypothesis is addressed by the rank + predicate, which avoids production's offset-plus-width arithmetic. No further + independent formulation was identified as needed for this bounded law. +- ORC-012–013: this record ties the law, source revision, neighboring witnesses, + actual RED categories and GREEN reach to their observation boundary. +- ORC-014: this owner claims an operator boundary. The shared real-framework + receiving grammar owns source growth and the cross-adapter handoff. +- ORC-015–016: each profile declares reach. Fixed examples and generated histories + use the same model/driver/checker; no expected-answer example was added. + +Limits: one payload per typed key, positive installed support, one group, finite +safe-integer authored numeric bounds, and settled graph cuts. This evidence does +not establish real provider completeness, asynchronous demand settlement, +multi-group reconfiguration, arbitrary negative/fractional window semantics, or +a general CPU bound. Those remain with their existing owners. Extra graph +messages are deliberately permitted; the demand law requires progress, not +minimal scheduling. diff --git a/packages/angular-db/src/index.ts b/packages/angular-db/src/index.ts index 84e2848209..1f0ca67a03 100644 --- a/packages/angular-db/src/index.ts +++ b/packages/angular-db/src/index.ts @@ -5,6 +5,7 @@ import { effect, inject, signal, + untracked, } from '@angular/core' import { BaseQueryBuilder, @@ -21,11 +22,10 @@ import type { CollectionStatus, Context, GetResult, - HealthNode, InferResultType, InitialQueryBuilder, LiveQueryHookConfig, - LiveQueryObserver, + LiveQuerySnapshot, LiveQueryStatusFlags, NonSingleResult, PooledLiveQueries, @@ -152,23 +152,27 @@ export function injectLiveQuery(opts: any) { assertInInjectionContext(injectLiveQuery) const destroyRef = inject(DestroyRef) - const collection = computed(() => { + let pendingWindow: (() => void) | undefined + let previousCollection: ReturnType = null + const resolveNext = (value: unknown, pool?: PooledLiveQueries) => + (previousCollection = resolveLiveQueryValue(value, { + gcTime: DEFAULT_GC_TIME_MS, + pool, + previousCollection, + windowOwner: resolveNext, + onWindowChange: (move) => { + pendingWindow = move + }, + })) + const resolveInput = () => { // Check if it's an existing collection if (isCollection(opts)) { + previousCollection = null return opts } if (typeof opts === `function`) { - // Check if query function returns null/undefined (disabled query) - const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder - const result = opts(queryBuilder) - - if (result === undefined || result === null) { - // Disabled query - return null - return null - } - - return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS }) + return resolveNext(opts) } // Check if it's reactive query options @@ -186,60 +190,62 @@ export function injectLiveQuery(opts: any) { params: opts.params(), q: new BaseQueryBuilder() as InitialQueryBuilder, }) - return resolveLiveQueryValue(result, { - gcTime: DEFAULT_GC_TIME_MS, - pool, - }) + return resolveNext(result, pool) } // Handle LiveQueryCollectionConfig objects. Default startSync/gcTime to // match the query-fn and reactive-options paths, but let an explicit value // in the config win — otherwise a bare `{ query }` never syncs. if (opts && typeof opts === `object` && typeof opts.query === `function`) { - return resolveLiveQueryValue(opts, { gcTime: DEFAULT_GC_TIME_MS }) + return resolveNext(opts) } throw new Error(`Invalid options provided to injectLiveQuery`) + } + const resolved = computed<{ + collection: ReturnType + move: (() => void) | undefined + }>(() => { + pendingWindow = undefined + const collection = resolveInput() + return { collection, move: pendingWindow } + }) + const collection = computed(() => resolved().collection) + effect(() => { + const { move } = resolved() + untracked(() => move?.()) }) - const state = signal(new Map()) - const internalData = signal>([]) - const statusValue = signal(`idle`) + // The shared observer already captures one coherent view per publication. + // Signals project that snapshot instead of copying its rows and health again. + const snapshot = signal | null>(null) + const state = computed( + () => (snapshot()?.state ?? new Map()) as Map, + ) const status = computed(() => { - const value = statusValue() + const value = snapshot()?.status ?? `idle` return value === `idle` && !collection() ? `disabled` : value }) - const health = signal(undefined) + const health = computed(() => snapshot()?.health) // Every status flag comes from the shared rule; a disabled query is ready. const flags = computed(() => getLiveQueryStatusFlags(status() as CollectionStatus, health()), ) - // Returns single item for singleResult collections, array otherwise + // The snapshot already applies findOne's single-result projection. Before + // the observer attaches, preserve Angular's empty array/undefined defaults. const data = computed(() => { + // Read the current input even when a snapshot exists: a compile rejection + // must propagate instead of exposing the previous query's rows. const currentCollection = collection() - if (!currentCollection) { - return internalData() - } - return isSingleResultCollection(currentCollection) - ? internalData()[0] - : internalData() + return ( + snapshot()?.data ?? + (currentCollection && isSingleResultCollection(currentCollection) + ? undefined + : []) + ) }) - const syncDataFromCollection = ( - currentCollection: Collection, - observer: LiveQueryObserver, - ) => { - const newState = new Map(currentCollection.entries()) - const newData = Array.from(newState.values()) - - state.set(newState) - internalData.set(newData) - const snapshot = observer.getSnapshot() - statusValue.set(snapshot.status) - health.set(snapshot.health) - } - let unsub: (() => void) | null = null const cleanup = () => { unsub?.() @@ -251,35 +257,25 @@ export function injectLiveQuery(opts: any) { // Handle null collection (disabled query) if (!currentCollection) { - statusValue.set(`disabled` as const) - health.set(undefined) - state.set(new Map()) - internalData.set([]) + snapshot.set(null) cleanup() return } cleanup() - // The shared observer owns sync start, subscription, the ready-race, and - // status transitions; Angular re-reads the whole collection on each notify - // (wholesale) into its signals. - // Angular re-reads the collection on notify; wholesale mode preserves its - // pre-observer loading policy (no initial-state snapshot request). + // Wholesale mode preserves Angular's loading policy: attaching requests no + // initial-state snapshot from the source. const observer = createLiveQueryObserver(currentCollection, { mode: `wholesale`, }) - const unsubscribe = observer.subscribe(() => { - syncDataFromCollection(currentCollection, observer) - }) + const syncSnapshot = () => snapshot.set(observer.getSnapshot()) + observer.subscribe(syncSnapshot) // Wholesale attach suppresses listener calls raised by synchronous sync // startup. Read once after subscribe returns to capture that final state. - syncDataFromCollection(currentCollection, observer) - unsub = () => { - unsubscribe() - observer.dispose() - } + syncSnapshot() + unsub = () => observer.dispose() onCleanup(cleanup) }) diff --git a/packages/db-ivm/src/operators/topKArray.ts b/packages/db-ivm/src/operators/topKArray.ts index 5de9b6f300..c72c8e561f 100644 --- a/packages/db-ivm/src/operators/topKArray.ts +++ b/packages/db-ivm/src/operators/topKArray.ts @@ -1,26 +1,10 @@ import { generateKeyBetween } from 'fractional-indexing' -import { binarySearch, compareKeys, diffHalfOpen } from '../utils.js' -import type { HRange } from '../utils.js' +import { binarySearch, compareKeys } from '../utils.js' // Abstraction for fractionally indexed values export type FractionalIndex = string export type IndexedValue = [V, FractionalIndex] -export function indexedValue( - value: V, - index: FractionalIndex, -): IndexedValue { - return [value, index] -} - -export function getValue(indexedVal: IndexedValue): V { - return indexedVal[0] -} - -export function getIndex(indexedVal: IndexedValue): FractionalIndex { - return indexedVal[1] -} - /** * Creates a comparator for [key, value] tuples that first compares values, * then uses the row key as a stable tie-breaker. @@ -79,9 +63,13 @@ export interface TopK { */ export class TopKArray implements TopK { #sortedValues: Array> = [] - #comparator: (a: V, b: V) => number + #comparator: (a: IndexedValue, b: V) => number #topKStart: number - #topKEnd: number + #topKLimit: number + + get #topKEnd(): number { + return this.#topKStart + this.#topKLimit + } constructor( offset: number, @@ -89,22 +77,22 @@ export class TopKArray implements TopK { comparator: (a: V, b: V) => number, ) { this.#topKStart = offset - this.#topKEnd = offset + limit - this.#comparator = comparator + this.#topKLimit = limit + this.#comparator = ([row], target) => comparator(row, target) } get size(): number { - const offset = this.#topKStart - const limit = this.#topKEnd - this.#topKStart - const available = this.#sortedValues.length - offset - return Math.max(0, Math.min(limit, available)) + return Math.max( + 0, + Math.min(this.#topKLimit, this.#sortedValues.length - this.#topKStart), + ) } countLeadingInWindow(predicate: (value: V) => boolean): number { let count = 0 const end = this.#topKStart + this.size for (let index = this.#topKStart; index < end; index++) { - if (!predicate(getValue(this.#sortedValues[index]!))) break + if (!predicate(this.#sortedValues[index]![0])) break count++ } return count @@ -121,95 +109,51 @@ export class TopKArray implements TopK { limit?: number }): TopKMoveChanges { const oldOffset = this.#topKStart - const oldLimit = this.#topKEnd - this.#topKStart - - // `this.#topKEnd` can be `Infinity` if it has no limit - // but `diffHalfOpen` expects a finite range - // so we restrict it to the size of the topK if topKEnd is infinite - const oldRange: HRange = [ - this.#topKStart, - this.#topKEnd === Infinity ? this.#topKStart + this.size : this.#topKEnd, - ] - + const oldLimit = this.#topKLimit + const oldEnd = this.#topKEnd this.#topKStart = offset ?? oldOffset - this.#topKEnd = this.#topKStart + (limit ?? oldLimit) // can be `Infinity` if limit is `Infinity` - - // Also handle `Infinity` in the newRange - const newRange: HRange = [ - this.#topKStart, - this.#topKEnd === Infinity - ? Math.max(this.#topKStart + this.size, oldRange[1]) // since the new limit is Infinity we need to take everything (so we need to take the biggest (finite) topKEnd) - : this.#topKEnd, - ] - const { onlyInA, onlyInB } = diffHalfOpen(oldRange, newRange) - - const moveIns: Array> = [] - onlyInB.forEach((index) => { - const value = this.#sortedValues[index] - if (value) { - moveIns.push(value) - } - }) - - const moveOuts: Array> = [] - onlyInA.forEach((index) => { - const value = this.#sortedValues[index] - if (value) { - moveOuts.push(value) - } - }) + this.#topKLimit = limit ?? oldLimit + + // A window minus another window has at most two intervals. Slice the rows + // directly: slice clamps to installed ranks without enumerating absent ones. + // Keep authored bounds exact for future inserts and partial moves. + const outside = ( + start: number, + end: number, + otherStart: number, + otherEnd: number, + ) => + this.#sortedValues + .slice(start, Math.min(end, otherStart)) + .concat(this.#sortedValues.slice(Math.max(start, otherEnd), end)) + const moveIns = outside(this.#topKStart, this.#topKEnd, oldOffset, oldEnd) + const moveOuts = outside(oldOffset, oldEnd, this.#topKStart, this.#topKEnd) // It could be that there are changes (i.e. moveIns or moveOuts) // but that the collection is lazy so we don't have the data yet that needs to move in/out // so `moveIns` and `moveOuts` will be empty but `changes` will be true // this will tell the caller that it needs to run the graph to load more data - return { moveIns, moveOuts, changes: onlyInA.length + onlyInB.length > 0 } + const changes = + (oldLimit > 0 || this.#topKLimit > 0) && + (oldOffset !== this.#topKStart || oldLimit !== this.#topKLimit) + return { moveIns, moveOuts, changes } } insert(value: V): TopKChanges { - const result: TopKChanges = { moveIn: null, moveOut: null } - // Lookup insert position const index = this.#findIndex(value) // Generate fractional index based on the fractional indices of the elements before and after it - const indexBefore = - index === 0 ? null : getIndex(this.#sortedValues[index - 1]!) - const indexAfter = - index === this.#sortedValues.length - ? null - : getIndex(this.#sortedValues[index]!) - const fractionalIndex = generateKeyBetween(indexBefore, indexAfter) + const fractionalIndex = generateKeyBetween( + this.#sortedValues[index - 1]?.[1] ?? null, + this.#sortedValues[index]?.[1] ?? null, + ) // Insert the value at the correct position - const val = indexedValue(value, fractionalIndex) + const val: IndexedValue = [value, fractionalIndex] // Splice is O(n) where n = all elements in the collection (i.e. n >= k) ! this.#sortedValues.splice(index, 0, val) - if (this.#topKStart === this.#topKEnd) return result - - // Check if the topK changed - if (index < this.#topKEnd) { - // The inserted element is either before the top K or within the top K - // If it is before the top K then it moves the element that was right before the topK into the topK - // If it is within the top K then the inserted element moves into the top K - // In both cases the last element of the old top K now moves out of the top K - const moveInIndex = Math.max(index, this.#topKStart) - if (moveInIndex < this.#sortedValues.length) { - // We actually have a topK - // because in some cases there may not be enough elements in the array to reach the start of the topK - // e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK - result.moveIn = this.#sortedValues[moveInIndex]! - - // We need to remove the element that falls out of the top K - // The element that falls out of the top K has shifted one to the right - // because of the element we inserted, so we find it at index topKEnd - if (this.#topKEnd < this.#sortedValues.length) { - result.moveOut = this.#sortedValues[this.#topKEnd]! - } - } - } - - return result + return this.#windowChanges(index, true) } /** @@ -219,56 +163,38 @@ export class TopKArray implements TopK { * that is on the position where the provided `value` would be. */ delete(value: V): TopKChanges { - const result: TopKChanges = { moveIn: null, moveOut: null } - // Lookup delete position const index = this.#findIndex(value) // Remove the value at that position const [removedElem] = this.#sortedValues.splice(index, 1) - if (this.#topKStart === this.#topKEnd) return result - - // Check if the topK changed - if (index < this.#topKEnd) { - // The removed element is either before the top K or within the top K - // If it is before the top K then the first element of the topK moves out of the topK - // If it is within the top K then the removed element moves out of the topK - result.moveOut = removedElem! - if (index < this.#topKStart) { - // The removed element is before the topK - // so actually, the first element of the topK moves out of the topK - // and not the element that we removed - // The first element of the topK is now at index topKStart - 1 - // since we removed an element before the topK - const moveOutIndex = this.#topKStart - 1 - if (moveOutIndex < this.#sortedValues.length) { - result.moveOut = this.#sortedValues[moveOutIndex]! - } else { - // No value is moving out of the topK - // because there are no elements in the topK - result.moveOut = null - } - } - - // Since we removed an element that was before or in the topK - // the first element after the topK moved one position to the left - // and thus falls into the topK now - const moveInIndex = this.#topKEnd - 1 - if (moveInIndex < this.#sortedValues.length) { - result.moveIn = this.#sortedValues[moveInIndex]! - } - } - - return result + return this.#windowChanges(index, false, removedElem) } - // TODO: see if there is a way to refactor the code for insert and delete in the topK above - // because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right - // so i have the feeling there is a common pattern here and we can implement both cases using that pattern + // Insertion shifts retained rows right; deletion shifts them left. Only a + // change before the exclusive end can cross either window boundary. + #windowChanges( + index: number, + inserting: boolean, + removed?: IndexedValue, + ): TopKChanges { + const start = this.#topKStart + const end = this.#topKEnd + if (start === end || index >= end) return { moveIn: null, moveOut: null } + const moveIn = + this.#sortedValues[inserting ? Math.max(index, start) : end - 1] ?? null + // A deletion before the window ejects its first row, now at start - 1; + // a deletion inside it ejects the removed row. Insertion ejects the last + // row, now at end. + const moveOut = inserting + ? this.#sortedValues[end] + : index < start + ? this.#sortedValues[start - 1] + : removed + return { moveIn, moveOut: moveOut ?? null } + } #findIndex(value: V): number { - return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) => - this.#comparator(getValue(a), getValue(b)), - ) + return binarySearch(this.#sortedValues, value, this.#comparator) } } diff --git a/packages/db-ivm/src/utils.ts b/packages/db-ivm/src/utils.ts index 6fce8c7ce8..f67e272fe6 100644 --- a/packages/db-ivm/src/utils.ts +++ b/packages/db-ivm/src/utils.ts @@ -16,10 +16,10 @@ export function chunkedArrayPush(array: Array, other: Array) { } } -export function binarySearch( +export function binarySearch( array: Array, - value: T, - comparator: (a: T, b: T) => number, + value: TValue, + comparator: (a: T, b: TValue) => number, ): number { let low = 0 let high = array.length @@ -37,39 +37,6 @@ export function binarySearch( return low } -export type HRange = [number, number] // half-open [start, end[ i.e. end is exclusive - -/** - * Computes the difference between two half-open ranges. - * @param a - The first half-open range - * @param b - The second half-open range - * @returns The difference between the two ranges - */ -export function diffHalfOpen(a: HRange, b: HRange) { - const [a1, a2] = a - const [b1, b2] = b - - // A \ B can be up to two segments (left and right of the overlap) - const onlyInA: Array = [ - ...range(a1, Math.min(a2, b1)), // left side of A outside B - ...range(Math.max(a1, b2), a2), // right side of A outside B - ] - - // B \ A similarly - const onlyInB: Array = [ - ...range(b1, Math.min(b2, a1)), - ...range(Math.max(b1, a2), b2), - ] - - return { onlyInA, onlyInB } -} - -function range(start: number, end: number): Array { - const out: Array = [] - for (let i = start; i < end; i++) out.push(i) - return out -} - /** * Compares two keys (string | number) in a consistent, deterministic way. * Handles mixed types by ordering strings before numbers. diff --git a/packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts b/packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts index 042a9aa684..433980bcba 100644 --- a/packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts +++ b/packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts @@ -1,3 +1,4 @@ +import { writeSync } from 'node:fs' import { describe, expect, it } from 'vitest' import fc from 'fast-check' import { D2 } from '../../src/d2.js' @@ -11,9 +12,13 @@ import { oracleRuns, } from '../../../db/tests/oracle-config.js' import { declareOracle } from '../../../db/tests/oracle-declaration.js' +import { createOracleReach } from '../../../db/tests/oracle-reach.js' import { TopKRelation } from './topk-relation-oracle.js' -declareOracle({ check: [TopKRelation] }) +declareOracle({ + check: [TopKRelation, checkNumericWindow], + model: [selectedWindowKeys, requestedWindowChanged], +}) /** * A top-K window selects support from a signed, typed-key relation. @@ -27,12 +32,14 @@ declareOracle({ check: [TopKRelation] }) * calibrate that checker without exercising production. * * The grammar covers five finite scalar keys, one fixed payload per key, - * support 0..3, and offset/limit 0..6. Every input turn and window move has + * support 0..3, and offset/limit 0..6. The numeric-window lane also reaches + * safe-integer offsets and widths near Number.MAX_SAFE_INTEGER. Every input turn and window move has * a graph-run checkpoint for output rows and reported window size. * The pinned 0/1/2 support crossing and boundary history distinguish presence * from raw multiplicity and inclusive from displaced window cuts. Row replacements, * multiple groups, negative net support, and downstream lazy acquisition are - * outside this owner. + * outside this owner. The numeric lane does observe the empty output message + * that lets a downstream graph respond to changed requested demand. */ const keys = [1, `1`, 2, `2`, `a`] as const @@ -40,6 +47,27 @@ const orderedKeys = [`1`, `2`, `a`, 1, 2] as const type Key = (typeof keys)[number] type Row = { id: number; value: string } type Step = { slot: number; support: number; offset: number; limit: number } +type Window = { offset: number; limit: number } + +// Selection is a predicate on each installed row's rank. It never enumerates +// absent ranks or adds offset and width, so large bounds cannot corrupt the +// reference through floating-point addition. Authored offset and width remain +// independent: a later partial move must retain the other exact bound. +function selectedWindowKeys(present: ReadonlyArray, window: Window) { + return present.filter( + (_, rank) => rank >= window.offset && rank - window.offset < window.limit, + ) +} + +// A requested window is a set of ranks, independently of installed rows. +// Distinct nonempty windows differ even when both currently select no row. +// Two zero-width windows both request the empty set. +function requestedWindowChanged(before: Window, after: Window): boolean { + return ( + (before.limit > 0 || after.limit > 0) && + (before.offset !== after.offset || before.limit !== after.limit) + ) +} // The fixed and random campaigns share the same property, recorder, checks, // and run budget. A path replay of this property selects only its random @@ -144,9 +172,10 @@ describe.each([ checkpoint: `input` | `move`, step: number, ): boolean => { - const selected = orderedKeys - .filter((key) => (counts.get(key) ?? 0) > 0) - .slice(window.offset, window.offset + window.limit) + const selected = selectedWindowKeys( + orderedKeys.filter((key) => (counts.get(key) ?? 0) > 0), + window, + ) const assertAt = ( observation: `rows` | `size`, assertion: () => void, @@ -218,6 +247,266 @@ describe.each([ ) }) +/** + * Finite authored bounds select installed support and each move must return. + * The operator's offset/limit API supplies that law; the live-query architecture's + * ordered-demand contract requires graph work when a requested window changes, + * including when the source has not supplied any selected row. An empty output + * message is this operator boundary's demand signal, not source completeness. + * Extra graph messages remain permitted, including for unchanged requested sets. + * + * This lane keeps three to five positive typed keys installed. It varies the + * installed size, near-maximum offset, finite width, and empty-range gap. + * The four profiles distinguish overflowing arithmetic, work proportional to + * absent ranks, exact width retention after a partial move, and lost demand + * signals when installed ranges agree. Negative, fractional and unsafe authored + * bounds are excluded. It does not impose a CPU threshold or claim real provider + * acquisition; the external RED watchdog classifies nontermination separately. + */ +type NumericWindowInput = { + installed: number + nearMaximum: number + width: number + gap: number +} +const numericWindowProperty = `ivm.topk-numeric-window` +const numericWindowInput = fc.record({ + installed: fc.integer({ min: 3, max: keys.length }), + nearMaximum: fc.integer({ min: 0, max: 2 }), + width: fc.integer({ min: 2, max: 4 }), + gap: fc.integer({ min: 1, max: 3 }), +}) +const numericWindowExample: NumericWindowInput = { + installed: 3, + nearMaximum: 0, + width: 3, + gap: 2, +} +type NumericProfile = `offset` | `width` | `partial` | `demand` + +// Every fixed regression is an input to this same history builder and model. +// Removing nearMaximum loses overflow neighbors; removing partial options loses +// width retention; removing the empty-range gap loses changed-but-uninstalled +// demand. Width zero distinguishes empty requested sets from nonempty ones. +function numericWindowHistory( + profile: NumericProfile, + input: NumericWindowInput, +): Array> { + const emptyOffset = input.installed + input.gap + const first: Window = + profile === `offset` + ? { + offset: Number.MAX_SAFE_INTEGER - input.nearMaximum, + limit: input.width, + } + : profile === `width` + ? { offset: 0, limit: Number.MAX_SAFE_INTEGER - input.nearMaximum } + : profile === `partial` + ? { offset: Number.MAX_SAFE_INTEGER - input.nearMaximum, limit: 2 } + : { offset: emptyOffset, limit: input.width } + return [ + first, + { offset: 0 }, + { offset: emptyOffset, limit: input.width }, + { offset: emptyOffset + 1 }, + { limit: input.width + 1 }, + { limit: 0 }, + { offset: emptyOffset + 2 }, + { offset: 0, limit: input.width }, + ] +} + +function checkNumericWindow( + relation: TopKRelation, + present: ReadonlyArray, + window: Window, + size: number, + moveSignals: number, + before?: Window, +) { + const selected = selectedWindowKeys(present, window) + const checks = [ + [ + `rows`, + () => + relation.expectRows( + selected.map((key) => [key, 10 - keys.indexOf(key), `tie`]), + ), + ], + [`size`, () => expect(size).toBe(selected.length)], + [ + `demand`, + () => { + if (before && requestedWindowChanged(before, window)) { + expect( + moveSignals, + `changed requested window must drive graph demand`, + ).toBeGreaterThan(0) + } + }, + ], + ] as const + for (const [observation, check] of checks) { + try { + check() + } catch (error) { + throw new NumericWindowMismatch(observation, error) + } + } +} + +class NumericWindowMismatch extends Error { + constructor( + readonly observation: `rows` | `size` | `demand`, + cause: unknown, + ) { + super( + `${observation}: ${cause instanceof Error ? cause.message : String(cause)}`, + { cause }, + ) + } +} + +declareOracle({ grammar: [numericWindowInput] }) + +describe.each([ + { name: `array`, operator: topKWithFractionalIndex }, + { name: `grouped`, operator: grouped }, +])(`Numeric window oracle: $name`, ({ operator }) => { + for (const profile of [`offset`, `width`, `partial`, `demand`] as const) { + for (const campaign of [ + { + name: `fixed`, + test: oracleReplayTest(it), + parameters: { numRuns: runs, seed: 2291 }, + }, + { + name: `random`, + test: oracleReplayTest(it, numericWindowProperty), + parameters: oraclePropertyOptions(100, numericWindowProperty), + }, + ]) { + campaign.test( + `preserves ${profile} bounds and demand (${campaign.name} campaign)`, + () => { + const profileState = + profile === `offset` + ? `large offset move returned` + : profile === `width` + ? `large width move returned` + : profile === `partial` + ? `partial move returned to installed rows` + : `empty installed range changed demand` + const reach = createOracleReach( + `${numericWindowProperty}.${profile}`, + [profileState, `returned to installed rows`], + { fixedHistories: 1 }, + ) + let begun = 0 + let firstFailure: + { step: number; error: NumericWindowMismatch } | undefined + try { + fc.assert( + fc.property(numericWindowInput, (generated) => { + reach.beginHistory({ fixed: begun++ === 0 }) + const graph = new D2() + const input = graph.newInput<[Key, Row]>() + const relation = new TopKRelation() + const present = orderedKeys.slice(0, generated.installed) + let window: Window = { offset: 0, limit: 1 } + let size!: () => number + let move!: (options: Partial) => void + let signals = 0 + input.pipe( + operator(() => 0, { + ...window, + setSizeCallback: (getSize) => { + size = getSize + }, + setWindowFn: (setWindow) => { + move = setWindow + }, + }), + output((message) => { + signals++ + relation.add(message.getInner()) + }), + ) + graph.finalize() + input.sendData( + new MultiSet( + present.map((key) => [ + [key, { id: 10 - keys.indexOf(key), value: `tie` }], + 1, + ]), + ), + ) + graph.run() + const checkAt = (step: number, before?: Window): boolean => { + try { + checkNumericWindow( + relation, + present, + window, + size(), + signals, + before, + ) + return true + } catch (error) { + if (!(error instanceof NumericWindowMismatch)) throw error + firstFailure ??= { step, error } + // Reject reductions that switch observation or move boundary. + if ( + firstFailure.step !== step || + firstFailure.error.observation !== error.observation + ) + return false + error.message += `\nOriginal ${firstFailure.error.observation} mismatch at move ${firstFailure.step}: ${firstFailure.error.message}` + throw error + } + } + if (!checkAt(-1)) return + for (const [index, options] of numericWindowHistory( + profile, + generated, + ).entries()) { + const before = window + window = { ...window, ...options } + signals = 0 + // Log only the fixed history. An external watchdog can then + // distinguish a move that never returns from setup failure. + if (begun === 1) + writeSync( + 1, + `numeric-window ${profile} move ${index} entered ${JSON.stringify(options)}\n`, + ) + move(options) + graph.run() + if (!checkAt(index, before)) return + if ( + index === + (profile === `partial` ? 1 : profile === `demand` ? 3 : 0) + ) + reach.reach(profileState) + if (index === 7) reach.reach(`returned to installed rows`) + } + }), + { ...campaign.parameters, examples: [[numericWindowExample]] }, + ) + reach.assertReached() + } finally { + writeSync( + 1, + `${numericWindowProperty}.${profile} ${campaign.name} reach ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + } + }, + ) + } + } +}) + it.each([-2, 2])( `rejects excess signed weight %s across a moved window`, (weight) => { diff --git a/packages/db/src/live-query-options.ts b/packages/db/src/live-query-options.ts index e5c30e2fad..5836e6297b 100644 --- a/packages/db/src/live-query-options.ts +++ b/packages/db/src/live-query-options.ts @@ -9,12 +9,13 @@ import { createLiveQueryCollection } from './query/live-query-collection.js' import { LIVE_QUERY_INTERNAL } from './query/live/internal.js' import { getBuilderFromConfig } from './query/live/collection-registry.js' import { + getQueryIdentity, getStableQueryBuilderHash, getStableValueHash, } from './query/ir-stable-identity.js' import { getStringCollationIdentity } from './query/runtime-reference-identity.js' import { codedMessage, devBuild } from './error-message.js' -import { InvalidPoolOptionError } from './errors.js' +import { InvalidPoolOptionError, UnhashableQueryIRError } from './errors.js' import type { Collection, CollectionImpl } from './collection/index.js' import type { CollectionOptionsIdentity } from './collection-options.js' import type { CollectionOptions, DbClient } from './client.js' @@ -197,7 +198,6 @@ export function prepareLiveQueryValue( value && typeof value === `object` && !isCollection(value) && - !(value instanceof BaseQueryBuilder) && `query` in value ) { const pool = configPool(value) @@ -237,12 +237,7 @@ export function getPreparedLiveQuerySources( onUnboundDescriptor?: (alias: string) => never, ): Array { if (isCollection(value)) return [value] - const query = - value instanceof BaseQueryBuilder - ? value - : value && typeof value === `object` && `query` in value - ? value.query - : value + const query = queryConfig(value)?.query ?? value if (query instanceof BaseQueryBuilder) { return collectSourceRefs(query._getQuery()).map((source) => source.type === `descriptorRef` @@ -309,14 +304,7 @@ export function getLiveQueryHash( queryKey?: LiveQueryKey, ): string { if (queryKey?.length) { - const query = - preparedValue instanceof BaseQueryBuilder - ? preparedValue - : preparedValue && - typeof preparedValue === `object` && - `query` in preparedValue - ? preparedValue.query - : undefined + const query = queryConfig(preparedValue)?.query const ir = isCollection(preparedValue) ? getBuilderFromConfig(preparedValue.config)?.getWindow() : query instanceof BaseQueryBuilder @@ -335,22 +323,99 @@ export function getLiveQueryHash( return getStableValueHash(identity, `queryKey`) } -// A config that names only its query and lifetime describes the same live -// query as its builder. Any other option shapes its Collection, so compiles. -function poolConfig( - config: LiveQueryCollectionConfig, - gcTime: number | undefined, - pool: PooledLiveQueries, -): Collection | undefined { +// Only adapter-owned Collections enter this table. Cache- and Suspense-owned +// Collections retain their result identity and are never movable by a peer. +const resolvedQueryValues = new WeakMap< + object, + { collection: object; value: unknown } +>() + +function queryConfig( + value: unknown, +): (Record & { query?: unknown }) | undefined { + return value instanceof BaseQueryBuilder + ? { query: value } + : value && typeof value === `object` + ? (value as Record) + : undefined +} + +function moveExistingWindow( + value: unknown, + previous: Collection | null, + owner: object | undefined, +) { + if (!previous || ![`ready`, `loading`].includes(previous.status)) return + const retained = owner && resolvedQueryValues.get(owner) + if (!retained || retained.collection !== previous) return + const before = queryConfig(retained.value) + const after = queryConfig(value) if ( - !(config.query instanceof BaseQueryBuilder) || - !Object.keys(config).every((key) => key === `query` || key === `gcTime`) - ) { - return undefined + !(before?.query instanceof BaseQueryBuilder) || + !(after?.query instanceof BaseQueryBuilder) + ) + return + const oldQuery = before.query._getQuery() + const query = after.query._getQuery() + const limit = query.limit + const offset = query.offset ?? 0 + // Adding/removing the outer limit, findOne, and opaque plans keep their + // ordinary compilation path. A move needs a finite ordered target window. + if ( + after.singleResult === true || + query.singleResult || + !query.orderBy?.length || + oldQuery.limit === undefined || + limit === undefined || + !Number.isSafeInteger(limit) || + limit < 0 || + !Number.isSafeInteger(offset) || + offset < 0 + ) + return + const keys = Object.keys(before) + if ( + keys.length !== Object.keys(after).length || + keys.some((key) => key !== `query` && before[key] !== after[key]) + ) + return + // IR source IDs establish semantic identity, but a keyed query may legally + // swap the object under an ID. Reuse must preserve the exact source objects. + const sources = getPreparedLiveQuerySources(before) + const nextSources = getPreparedLiveQuerySources(after) + if ( + sources.length !== nextSources.length || + sources.some((source, index) => source !== nextSources[index]) + ) + return + try { + if ( + getQueryIdentity({ ...oldQuery, limit: undefined, offset: undefined }) !== + getQueryIdentity({ ...query, limit: undefined, offset: undefined }) + ) + return + } catch (error) { + if (error instanceof UnhashableQueryIRError) return + throw error + } + const builder = getBuilderFromConfig(previous.config) + if (!builder) return + return () => { + // A framework can commit synchronously from a Collection publication + // callback. Move after that publication, while this remains the binding's + // last resolved result and a consumer still asks for it. + queueMicrotask(() => { + if ( + resolvedQueryValues.get(owner)?.collection !== previous || + previous.subscriberCount === 0 + ) + return + const result = builder.setWindow({ offset, limit }) + // The Collection reports request failures through its health tree. A + // superseded move or unmount can reject its waiter without a hook caller. + if (result !== true) void result.catch(() => {}) + }) } - return pool.createPooledLiveQuery(config.query, { - gcTime: config.gcTime ?? gcTime, - }) } /** @@ -365,6 +430,9 @@ export function resolveLiveQueryValue( gcTime, pool, startSync = true, + previousCollection, + windowOwner, + onWindowChange, }: { gcTime?: number // `false` keeps a hook that cannot pool, such as a Suspense or DbClient @@ -372,22 +440,32 @@ export function resolveLiveQueryValue( // A pool here has passed `configPool`. pool?: PooledLiveQueries | false startSync?: boolean + /** @internal Previous adapter-owned result; absent for shared results. */ + previousCollection?: Collection | null + /** @internal Identity of one framework binding; never shared by peers. */ + windowOwner?: object + /** @internal React/Preact defer mutation until their render commits. */ + onWindowChange?: (move: () => void) => void } = {}, ): Collection | null { + if (previousCollection !== undefined) { + value = prepareLiveQueryValue(value, undefined) + const move = moveExistingWindow(value, previousCollection, windowOwner) + if (move) { + if (onWindowChange) onWindowChange(move) + else move() + return previousCollection! + } + } if (value === undefined || value === null) return null if (isCollection(value)) { value.startSyncImmediate() return value } - if (value instanceof BaseQueryBuilder) { - return ( - (pool ? pool.createPooledLiveQuery(value, { gcTime }) : undefined) ?? - createLiveQueryCollection({ query: value, startSync, gcTime }) - ) - } - if (typeof value === `object`) { - const requested = configPool(value) - const { pool: _pool, ...given } = value as LiveQueryHookConfig + const input = queryConfig(value) + if (input) { + const requested = configPool(input) + const { pool: _pool, ...given } = input as LiveQueryHookConfig const pooling = pool === false ? undefined : (requested ?? pool) // A pooled view needs the built query. Build a query function once and // compile the same builder if the partition cannot serve it. @@ -397,14 +475,25 @@ export function resolveLiveQueryValue( : undefined const config = built instanceof BaseQueryBuilder ? { ...given, query: built } : given - return ( - (pooling ? poolConfig(config, gcTime, pooling) : undefined) ?? + // A query and lifetime alone can pool; other options shape a Collection. + const pooled = + pooling && + config.query instanceof BaseQueryBuilder && + Object.keys(config).every((key) => key === `query` || key === `gcTime`) + ? pooling.createPooledLiveQuery(config.query, { + gcTime: config.gcTime ?? gcTime, + }) + : undefined + const collection = + pooled ?? createLiveQueryCollection({ ...config, startSync: config.startSync ?? startSync, gcTime: config.gcTime ?? gcTime, }) - ) + if (previousCollection !== undefined && windowOwner) + resolvedQueryValues.set(windowOwner, { collection, value }) + return collection } throw new Error( devBuild() && process.env.NODE_ENV !== `production` diff --git a/packages/db/tests/conformance/contract.ts b/packages/db/tests/conformance/contract.ts index 6469f4f085..e77b2dce05 100644 --- a/packages/db/tests/conformance/contract.ts +++ b/packages/db/tests/conformance/contract.ts @@ -22,8 +22,24 @@ * Query identity (1.0 law, maintainer decisions 2026-10-22 and 2026-10-23). * The law depends on how a binding decides to compile again. * - * A binding that compares query identity re-runs its query input and compiles - * again only when the input's identity changed. React and Preact compare it + * A binding that compares query identity invalidates its authored input only + * when the input's identity changed. Invalidation does not always compile: + * an adapter-owned ordered list query whose stable structured plan changes only + * its finite outer limit/offset moves the existing window and retains its + * live-query Collection and upstream subscriptions (maintainer decision, + * 2026-10-30, #2291). For an invalidated input, changed source objects, + * 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. + * + * A window move requires a committed host update and runs after any current + * Collection publication. A speculative React or Preact render cannot move the + * committed window or start acquisition. Pending moves follow core's atomic + * window publication and latest-window rules. + * + * React and Preact compare it * on every render. Such a binding compiles an input that has no `queryKey` * only when the query IR has a stable identity; otherwise it throws * `UnhashableQueryIRError`. The identity names each source by its Collection @@ -39,7 +55,7 @@ * * A binding that compiles again whenever its reactive reads change (Vue, * Solid, Angular, and Svelte without a `DbClient`) compares no identity. It - * compiles any query input, including one without a stable IR identity, and + * accepts any query input, including one without a stable IR identity, and * its rows follow the current input: after a source swap under the same * Collection ID, every value it publishes holds only the new source's rows * (or none while it loads) and never the old ones, @@ -122,6 +138,8 @@ export interface LaggingSourceHandle< loadStarted: () => boolean /** Settle every pending subset request. */ settle: () => void + /** Settle the oldest request, leaving later requests pending. */ + settleNext: () => void } /** diff --git a/packages/db/tests/conformance/lagging-source.ts b/packages/db/tests/conformance/lagging-source.ts index 025164f2ea..4ccb74e80c 100644 --- a/packages/db/tests/conformance/lagging-source.ts +++ b/packages/db/tests/conformance/lagging-source.ts @@ -50,6 +50,7 @@ export function makeLaggingSource( emit: (rows) => writeRows(`insert`, rows), markReady: () => controls!.markReady(), loadStarted: () => pendingLoads.length > 0, + settleNext: () => pendingLoads.shift()?.(), settle: () => pendingLoads.splice(0).forEach((resolve) => resolve()), } } diff --git a/packages/db/tests/conformance/suite.ts b/packages/db/tests/conformance/suite.ts index f8705b3e04..ef5bd1c349 100644 --- a/packages/db/tests/conformance/suite.ts +++ b/packages/db/tests/conformance/suite.ts @@ -56,6 +56,7 @@ import { describe, expect, it } from 'vitest' import { include, pooledLiveQueries } from '@tanstack/db' import { createOracleReach } from '../oracle-reach' +import { registerWindowHistoryOracle } from './window-history-oracle' import { ScenarioLifetime } from './scenario-lifetime' import { ScenarioSources } from './scenario-sources' import { scenarioRegistry } from './registration' @@ -160,6 +161,7 @@ const ISSUES: Array = [ ] export function runSuite(rawDriver: LiveQueryDriver) { + registerWindowHistoryOracle(rawDriver) const { ops } = rawDriver const registry = scenarioRegistry(rawDriver.knownGaps) diff --git a/packages/db/tests/conformance/window-history-oracle.ts b/packages/db/tests/conformance/window-history-oracle.ts new file mode 100644 index 0000000000..df5352d1d2 --- /dev/null +++ b/packages/db/tests/conformance/window-history-oracle.ts @@ -0,0 +1,1173 @@ +/** + * An outer limit/offset change moves the existing window. It does not replace + * the live-query Collection or recompile its upstream plan (maintainer decision, + * 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. + * 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 + * their existing keyed recompilation behavior. Nested limits are plan inputs. + * + * At each driver's settled framework cut, rows equal a full sort/filter/slice. + * Same-plan page moves retain the public Collection; plan changes replace it. + * This model has no graph, subscription, identity hash, or incremental cache. + * React's abandoned-render cut is checked by its receiving driver separately. + */ +import fc from 'fast-check' +import { describe, expect, it } from 'vitest' +import { declareOracle } from '../oracle-declaration' +import { createOracleReach } from '../oracle-reach' +import { + atOracleCheckpoint, + createOracleFailureFidelity, +} from '../oracle-failure-fidelity' +import { + oraclePropertyOptions, + oracleReplayTest, + oracleRuns, +} from '../oracle-config' +import { expectOrderedRows, expectUnorderedRows } from './result-laws' +import { ScenarioLifetime } from './scenario-lifetime' +import type { + ControllableHandle, + LiveQueryDriver, + QueryBuild, +} from './contract' + +export interface WindowRow { + id: string + rank: number +} +export interface WindowInput { + offset: number + limit: number + minimum: number + descending: boolean + nestedLimit: number + source?: number + projectRank?: boolean + gcTime?: number +} +export interface WindowHistory { + rows: Array + inputs: Array + nested?: boolean +} + +// Authored clauses, source objects and Collection options identify the plan. +// Only offset and limit select its outer window; gcTime changes its lifecycle. +export function sameWindowPlan(a: WindowInput, b: WindowInput): boolean { + return ( + a.minimum === b.minimum && + a.descending === b.descending && + a.nestedLimit === b.nestedLimit && + a.source === b.source && + a.projectRank === b.projectRank && + (a.gcTime ?? 10) === (b.gcTime ?? 10) + ) +} +export function windowRows( + rows: ReadonlyArray, + input: WindowInput, + nested = true, +) { + const sourceRows = [...rows] + .sort((a, b) => a.rank - b.rank || a.id.localeCompare(b.id)) + .slice(0, nested ? input.nestedLimit : undefined) + const ordered = sourceRows + .filter((row) => row.rank > input.minimum) + .sort( + (a, b) => + (input.descending ? b.rank - a.rank : a.rank - b.rank) || + a.id.localeCompare(b.id), + ) + return ordered + .slice(input.offset, input.offset + input.limit) + .map((row) => (input.projectRank === false ? { id: row.id } : { ...row })) +} + +export function checkWindowObservation( + actual: { rows: unknown; collection: unknown }, + expected: { + rows: ReadonlyArray<{ id: string }> + previousCollection?: unknown + retain: boolean + }, + checkpoint = `window observation`, +): void { + atOracleCheckpoint(`ordered window rows`, checkpoint, () => + expectOrderedRows(actual.rows, expected.rows), + ) + atOracleCheckpoint(`enabled window Collection`, checkpoint, () => + expect(actual.collection, `enabled window Collection`).not.toBeNull(), + ) + if (expected.previousCollection !== undefined) { + if (expected.retain) + atOracleCheckpoint(`outer window retains Collection`, checkpoint, () => + expect(actual.collection, `outer window retains Collection`).toBe( + expected.previousCollection, + ), + ) + else + atOracleCheckpoint(`changed plan replaces Collection`, checkpoint, () => + expect(actual.collection, `changed plan replaces Collection`).not.toBe( + expected.previousCollection, + ), + ) + } +} + +// The keyed public view owes the same rows as data, including while demand is +// held. Keeping only data stale would otherwise hide an early map publication. +export function checkWindowState( + actual: ReadonlyMap | undefined, + expected: ReadonlyArray<{ id: string }>, + checkpoint: string, +): void { + atOracleCheckpoint(`keyed window rows`, checkpoint, () => { + expect(actual, `enabled keyed window`).toBeDefined() + expectUnorderedRows([...actual!.values()], expected) + }) +} + +interface WindowSubscriptionObservation { + owners: ReadonlyArray + previousSubscriberCount: number + subscriberCount: number +} + +// Moving the page keeps the existing upstream subscriptions (contract.ts). +// Owner continuity alone loses overlapping replacement subscriptions because +// Collection.subscribers groups them by owner. Public count transitions keep +// that distinction. Retired Collections may finish GC during a retained move, +// so their decrements remain legal; a page move starts no new subscription. +export function checkWindowSubscriptions( + observations: ReadonlyArray, + previousCollection: unknown, + checkpoint = `upstream window ownership`, +): void { + for (const observation of observations) { + atOracleCheckpoint(`retained upstream ownership`, checkpoint, () => + expect(observation.owners, `retained upstream ownership`).toContain( + previousCollection, + ), + ) + atOracleCheckpoint(`retained upstream subscription count`, checkpoint, () => + expect( + observation.subscriberCount, + `retained upstream subscription count`, + ).toBeLessThanOrEqual(observation.previousSubscriberCount), + ) + } +} + +const inputArbitrary = fc.record({ + offset: fc.integer({ min: 0, max: 12 }), + limit: fc.integer({ min: 0, max: 6 }), + minimum: fc.integer({ min: -1, max: 8 }), + descending: fc.boolean(), + source: fc.integer({ min: 0, max: 1 }), + projectRank: fc.boolean(), + nestedLimit: fc.integer({ min: 0, max: 12 }), + gcTime: fc.integer({ min: 1, max: 100 }), +}) +// Compulsory page moves and adjacent plan/option changes make every campaign +// reach the distinctions even at the ordinary budget. The tail permits repeats, +// reversals, zero windows, empty sources, and jumps beyond the final row. +export const windowHistoryArbitrary = fc + .tuple( + fc.array(fc.integer({ min: 0, max: 20 }), { maxLength: 12 }), + inputArbitrary, + fc.boolean(), + fc.array(inputArbitrary, { maxLength: 6 }), + ) + .map(([ranks, first, nested, tail]): WindowHistory => ({ + nested, + rows: ranks.map((rank, id) => ({ id: String(id).padStart(2, `0`), rank })), + inputs: [ + first, + { ...first, offset: first.offset + 1 }, + { ...first, offset: first.offset + 1, limit: first.limit + 1 }, + { ...first, offset: Number.MAX_SAFE_INTEGER, limit: 3 }, + { ...first, offset: 0, limit: Number.MAX_SAFE_INTEGER }, + { ...first, minimum: first.minimum + 1 }, + { ...first, descending: !first.descending }, + { ...first, nestedLimit: first.nestedLimit + 1 }, + { ...first, source: 1 - first.source, offset: first.offset + 1 }, + { ...first, projectRank: !first.projectRank }, + { ...first, offset: first.offset + 1, gcTime: first.gcTime + 1 }, + ...tail.map((input) => ({ ...input, gcTime: first.gcTime + 1 })), + ], + })) + +export const windowHistoryExamples: Array<[WindowHistory]> = [ + [ + { + rows: Array.from({ length: 6 }, (_, id) => ({ + id: String(id), + rank: id, + })), + inputs: [ + { offset: 0, limit: 2, minimum: -1, descending: false, nestedLimit: 6 }, + { offset: 2, limit: 2, minimum: -1, descending: false, nestedLimit: 6 }, + { offset: 2, limit: 0, minimum: -1, descending: false, nestedLimit: 6 }, + { + offset: Number.MAX_SAFE_INTEGER, + limit: 3, + minimum: -1, + descending: false, + nestedLimit: 6, + }, + { + offset: 0, + limit: Number.MAX_SAFE_INTEGER, + minimum: -1, + descending: false, + nestedLimit: 6, + }, + { offset: 0, limit: 3, minimum: 0, descending: false, nestedLimit: 6 }, + { offset: 0, limit: 3, minimum: 0, descending: true, nestedLimit: 6 }, + { offset: 0, limit: 3, minimum: 0, descending: true, nestedLimit: 2 }, + { + offset: 1, + limit: 3, + minimum: 0, + descending: true, + nestedLimit: 2, + source: 1, + }, + { + offset: 1, + limit: 3, + minimum: 0, + descending: true, + nestedLimit: 2, + source: 1, + projectRank: false, + }, + { + offset: 2, + limit: 3, + minimum: 0, + descending: true, + nestedLimit: 2, + source: 1, + projectRank: false, + gcTime: 100, + }, + ], + }, + ], +] +const states = [ + `offset move`, + `limit move`, + `large offset move`, + `large limit move`, + `filter change`, + `order change`, + `nested window change`, + `source change`, + `projection change`, + `Collection option change`, + `source write`, + `source growth`, + `stable key`, + `opaque key change`, +] as const + +export async function runWindowHistory( + driver: LiveQueryDriver, + history: WindowHistory, + keyed: boolean, + reach: ReturnType>, + keyPolicy: `changing` | `stable` | `opaque` = `changing`, +) { + const lifetime = new ScenarioLifetime() + let handle: ControllableHandle | undefined + lifetime.defer(() => handle?.unmount()) + await lifetime.run(async () => { + const source = driver.makeSource(history.rows) + lifetime.defer(() => source.collection.cleanup({ dependents: true })) + const alternateRows = history.rows.map((row) => ({ + id: `b${row.id}`, + rank: row.rank + 3, + })) + const alternate = driver.makeSource( + alternateRows, + keyed ? source.collection.id : undefined, + ) + lifetime.defer(() => alternate.collection.cleanup({ dependents: true })) + const modelRows = [history.rows.map((row) => ({ ...row })), alternateRows] + // Only the current live query's ownership is constrained. A previously + // replaced Collection may finish its GC during this step. + const subscriptionSnapshots: Array = [] + const recordSubscribers = (event: { + previousSubscriberCount: number + subscriberCount: number + }) => + subscriptionSnapshots.push({ + owners: [ + ...source.collection.subscribers.map((owner) => owner.collection), + ...alternate.collection.subscribers.map((owner) => owner.collection), + ], + previousSubscriberCount: event.previousSubscriberCount, + subscriberCount: event.subscriberCount, + }) + const stop = source.collection.on(`subscribers:change`, recordSubscribers) + const stopAlternate = alternate.collection.on( + `subscribers:change`, + recordSubscribers, + ) + lifetime.defer(stop) + lifetime.defer(stopAlternate) + const build = (q: Parameters[0], input: WindowInput) => { + const from = input.source === 1 ? alternate.collection : source.collection + const innerSource = + history.nested === false + ? from + : q + .from({ inner: from }) + .orderBy(({ inner }: { inner: WindowRow }) => inner.rank) + .orderBy(({ inner }: { inner: WindowRow }) => inner.id) + .limit(input.nestedLimit) + .select(({ inner }: { inner: WindowRow }) => ({ + id: inner.id, + rank: inner.rank, + })) + const query = q.from({ row: innerSource }) + const filtered = + keyPolicy === `opaque` + ? query.fn.where( + ({ row }: { row: WindowRow }) => row.rank > input.minimum, + ) + : query.where(({ row }: { row: WindowRow }) => + driver.ops.gt(row.rank, input.minimum), + ) + const built = filtered + .orderBy( + ({ row }: { row: WindowRow }) => row.rank, + input.descending ? `desc` : `asc`, + ) + .orderBy(({ row }: { row: WindowRow }) => row.id) + .limit(input.limit) + .offset(input.offset) + .select(({ row }: { row: WindowRow }) => + input.projectRank === false + ? { id: row.id } + : { id: row.id, rank: row.rank }, + ) + // The bare query callback accepts Collection options. The keyed receiving + // form wraps this callback in its own config, so it keeps the query shape. + return keyed ? built : { query: built, gcTime: input.gcTime ?? 10 } + } + const first = history.inputs[0]! + const mounted = keyed + ? driver.mountKeyedControllable!( + build, + (input) => + keyPolicy === `stable` + ? [`window-history`] + : [`window-history`, { ...input, gcTime: undefined }], + first, + ) + : driver.mountControllable(build, first) + handle = mounted + await mounted.flush() + let previous = first + let previousCollection: unknown + const failures: Array = [] + try { + for (const [index, requested] of history.inputs.entries()) { + const authored = keyPolicy === `stable` ? first : requested + const input = keyed ? { ...authored, gcTime: undefined } : authored + const beforeSubscriptions = subscriptionSnapshots.length + if (index > 0) await mounted.setParam(requested) + // A direct query does not author nestedLimit, so its value cannot require + // replacement. The same independent plan rule judges both input shapes. + const samePlan = sameWindowPlan( + history.nested === false ? { ...previous, nestedLimit: 0 } : previous, + history.nested === false ? { ...input, nestedLimit: 0 } : input, + ) + const retain = + keyPolicy === `stable` || + (keyPolicy === `opaque` + ? JSON.stringify(previous) === JSON.stringify(input) + : samePlan) + if (index > 0) { + if (keyPolicy === `stable`) reach.reach(`stable key`) + if (keyPolicy === `opaque`) reach.reach(`opaque key change`) + if (retain) { + if (previous.offset !== input.offset) reach.reach(`offset move`) + if (previous.limit !== input.limit) reach.reach(`limit move`) + if (input.offset === Number.MAX_SAFE_INTEGER) + reach.reach(`large offset move`) + if (input.limit === Number.MAX_SAFE_INTEGER) + reach.reach(`large limit move`) + } + if (!keyed && (previous.gcTime ?? 10) !== (input.gcTime ?? 10)) + reach.reach(`Collection option change`) + if (previous.minimum !== input.minimum) reach.reach(`filter change`) + if (previous.descending !== input.descending) + reach.reach(`order change`) + if ( + history.nested !== false && + previous.nestedLimit !== input.nestedLimit + ) + reach.reach(`nested window change`) + if (previous.source !== input.source) reach.reach(`source change`) + if (previous.projectRank !== input.projectRank) + reach.reach(`projection change`) + } + const rows = modelRows[input.source === 1 ? 1 : 0]! + const compare = () => { + const expected = windowRows(rows, input, history.nested !== false) + checkWindowObservation( + { rows: mounted.current().data, collection: mounted.collection() }, + { + rows: expected, + previousCollection, + retain, + }, + `input ${index}`, + ) + if (!keyed) + atOracleCheckpoint( + `authored Collection option`, + `input ${index}`, + () => + expect( + mounted.collection()!.config.gcTime, + `authored Collection option`, + ).toBe(input.gcTime ?? 10), + ) + atOracleCheckpoint(`keyed window rows`, `input ${index}`, () => + expectUnorderedRows( + [...mounted.current().state!.values()], + expected, + ), + ) + if (index > 0 && retain && keyPolicy !== `opaque`) + checkWindowSubscriptions( + subscriptionSnapshots.slice(beforeSubscriptions), + previousCollection, + `input ${index}`, + ) + } + // Keep neighboring checkpoints even when retention fails. This reports + // baseline row/plan evidence without changing the law or hiding a failure. + try { + compare() + } catch (error) { + failures.push(error) + } + previous = input + previousCollection = mounted.collection() + // Bounds describe future rows too. Growing the direct source after a + // huge move rejects a resolver that stores bounds clamped to row count. + if ( + input.offset === Number.MAX_SAFE_INTEGER || + input.limit === Number.MAX_SAFE_INTEGER + ) { + const row = { + id: `grown-${index}`, + rank: + Math.max(input.minimum, ...rows.map((entry) => entry.rank), 0) + + 1, + } + rows.push(row) + await mounted.apply(() => + (input.source === 1 ? alternate : source).insert({ ...row }), + ) + reach.reach(`source growth`) + try { + const expected = windowRows(rows, input, history.nested !== false) + atOracleCheckpoint( + `ordered window rows`, + `input ${index}/source growth`, + () => expectOrderedRows(mounted.current().data, expected), + ) + atOracleCheckpoint( + `keyed window rows`, + `input ${index}/source growth`, + () => + expectUnorderedRows( + [...mounted.current().state!.values()], + expected, + ), + ) + } catch (error) { + failures.push(error) + } + } + if (rows.length > 0) { + rows[0] = { ...rows[0]!, rank: rows[0]!.rank + 1 } + await mounted.apply(() => + (input.source === 1 ? alternate : source).update({ ...rows[0]! }), + ) + reach.reach(`source write`) + try { + const expected = windowRows(rows, input, history.nested !== false) + atOracleCheckpoint( + `ordered window rows`, + `input ${index}/source write`, + () => expectOrderedRows(mounted.current().data, expected), + ) + atOracleCheckpoint( + `keyed window rows`, + `input ${index}/source write`, + () => + expectUnorderedRows( + [...mounted.current().state!.values()], + expected, + ), + ) + } catch (error) { + failures.push(error) + } + } + } + } catch (error) { + failures.push(error) + } + if (failures.length) + throw new AggregateError( + failures, + `Window history mismatches: ${failures.map((error) => String(error)).join('; ')}`, + { + cause: failures[0], + }, + ) + }) +} + +// Replay identity includes the production driver. Equal seeds and paths in a +// different framework cannot reproduce a scheduler-specific counterexample. +function windowHistoryProperty( + driver: LiveQueryDriver, + family: string, +): string { + const id = driver.name.replace(/\W+/g, `-`).replace(/-$/, ``) + return `framework.${id}.${family}` +} + +// Each registration runs the same grammar and comparison with a fixed and fresh +// seed. Reach belongs to one driver and input form; one framework cannot supply +// another's missing witness. Replay goes directly to the requested campaign. +export function registerWindowHistoryOracle(driver: LiveQueryDriver) { + describe(`window history :: ${driver.name}`, () => { + oracleReplayTest(it)( + `rejects a pending keyed map that differs from the settled rows`, + () => { + const history = windowHistoryExamples[0]![0] + const input = history.inputs[0]! + const rows = windowRows(history.rows, input, false) + const collection = {} + // data and Collection are valid. The empty map alone is the wrong answer. + checkWindowObservation( + { rows, collection }, + { rows, previousCollection: collection, retain: true }, + `held acquisition`, + ) + expect(() => + checkWindowState(new Map(), rows, `held acquisition`), + ).toThrow(`keyed window rows at held acquisition`) + checkWindowState( + new Map(rows.map((row) => [row.id, row])), + rows, + `held acquisition`, + ) + }, + ) + // These controls use the campaign's shared comparison. Owner-only recording + // accepts 1 -> 2 -> 1 when a replacement subscribes before its predecessor + // retires. A retired Collection's 2 -> 1 decrement must still be accepted. + oracleReplayTest(it)( + `retained subscriptions distinguish overlapping replacement from GC`, + () => { + const collection = {} + const overlappingReplacement = [ + { + owners: [collection], + previousSubscriberCount: 1, + subscriberCount: 2, + }, + { + owners: [collection], + previousSubscriberCount: 2, + subscriberCount: 1, + }, + ] + expect(() => + checkWindowSubscriptions(overlappingReplacement, collection), + ).toThrow(`retained upstream subscription count`) + checkWindowSubscriptions( + [ + { + owners: [collection], + previousSubscriberCount: 2, + subscriberCount: 1, + }, + ], + collection, + ) + }, + ) + registerPendingWindowOracle(driver) + registerPublicationWindowOracle(driver) + for (const nested of [false, true]) { + for (const keyed of driver.mountKeyedControllable + ? [false, true] + : [false]) { + for (const keyPolicy of keyed + ? ([`changing`, `stable`, `opaque`] as const) + : ([`changing`] as const)) { + for (const campaign of [`fixed`, `fresh`] as const) { + const form = `${keyed ? keyPolicy : `derived`}/${nested ? `nested` : `direct`}` + const property = windowHistoryProperty(driver, `window-history`) + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`${form} window histories (${campaign})`, async () => { + // Registration's replay selector is the same one the ordinary suite uses. + const reach = createOracleReach( + `${driver.name}/${form}/${campaign}`, + states.filter((state) => + keyed && state === `Collection option change` + ? false + : keyPolicy === `stable` + ? state === `stable key` || state === `source write` + : keyPolicy === `opaque` + ? ![ + `stable key`, + `offset move`, + `limit move`, + `large offset move`, + `large limit move`, + ].includes(state) && + (nested || state !== `nested window change`) + : ![`stable key`, `opaque key change`].includes( + state, + ) && + (nested || state !== `nested window change`), + ), + { + fixedHistories: windowHistoryExamples.length, + }, + ) + let historyIndex = 0 + const fidelity = createOracleFailureFidelity() + const propertyUnderTest = fc.asyncProperty( + windowHistoryArbitrary, + async (history) => { + reach.beginHistory({ + fixed: historyIndex++ < windowHistoryExamples.length, + }) + const exercised = { ...history, nested } + await fidelity.run(exercised, () => + runWindowHistory( + driver, + exercised, + keyed, + reach, + keyPolicy, + ), + ) + }, + ) + try { + await fidelity.assert(() => + fc.assert( + propertyUnderTest, + campaign === `fixed` + ? { + numRuns: + oracleRuns(12) + windowHistoryExamples.length, + seed: 2291, + examples: windowHistoryExamples, + } + : oraclePropertyOptions(12, property, { + examples: windowHistoryExamples, + }), + ), + ) + } finally { + console.info( + `window reach ${driver.name}/${form}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + reach.assertReached() + }, 30_000) + } + } + } + } + }) +} + +declareOracle({ + model: [sameWindowPlan, windowRows], + grammar: [windowHistoryArbitrary], + check: [ + checkWindowObservation, + checkWindowState, + checkWindowSubscriptions, + runWindowHistory, + ], +}) + +/** + * Pending window moves keep the last settled page. A later commit supersedes + * the requested window; an older acquisition cannot publish its own page. This + * receiving lane uses each real adapter over an on-demand source that already + * holds the rows but has not settled its demand. Available rows alone prove no + * coverage (ARCHITECTURE.md, atomic window publication). Settlement is explicit; + * no timer stands in for provider completion. The core ordered-window owner + * separately checks source delivery, failures and repair chains. + */ +const pendingWindowArbitrary = fc.record({ + width: fc.integer({ min: 1, max: 3 }), + jump: fc.integer({ min: 1, max: 4 }), +}) +const pendingExamples = [[{ width: 2, jump: 2 }]] +const pendingStates = [ + `initial loading move`, + `initial oldest settled while latest pending`, + `initial ready`, + `overlap pending`, + `oldest settled while latest pending`, + `settled latest`, + `unmounted pending`, +] as const + +async function checkPendingWindowHistory( + driver: LiveQueryDriver, + history: { width: number; jump: number }, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + let unmount: (() => void) | undefined + lifetime.defer(() => unmount?.()) + await lifetime.run(async () => { + const source = driver.makeLaggingSource() + lifetime.defer(() => source.collection.cleanup({ dependents: true })) + const rows = Array.from({ length: 30 }, (_, id) => ({ + id: String(id).padStart(2, `0`), + rank: id, + })) + source.emit(rows) + 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 }) => ({ + id: row.id, + rank: row.rank, + })) + const handle = driver.mountControllable(build, 0) + unmount = () => handle.unmount() + const expected = (offset: number) => + windowRows( + rows, + { + offset, + limit: history.width, + minimum: -1, + descending: false, + nestedLimit: rows.length, + }, + false, + ) + await handle.flush() + const collection = handle.collection() + const failures: Array = [] + const readiness = (ready: boolean, checkpoint: string) => + atOracleCheckpoint(`initial-query readiness`, checkpoint, () => + expect(handle.current().isReady).toBe(ready), + ) + const acquisitionPending = (pending: boolean, checkpoint: string) => + atOracleCheckpoint(`acquisition pending premise`, checkpoint, () => + expect(source.loadStarted()).toBe(pending), + ) + // Before initial-query readiness there is no settled public snapshot. The + // same shared comparison therefore expects no rows; later cuts name the + // last settled offset and recompute that page independently. + const compare = (offset: number | undefined, checkpoint: string) => { + try { + const expectedRows = offset === undefined ? [] : expected(offset) + checkWindowObservation( + { rows: handle.current().data, collection: handle.collection() }, + { + rows: expectedRows, + previousCollection: collection, + retain: true, + }, + checkpoint, + ) + checkWindowState(handle.current().state, expectedRows, checkpoint) + } catch (error) { + failures.push(error) + } + } + // A page move is legal while the first acquisition is still held. It keeps + // the initial Collection, and settling the obsolete page alone neither + // opens readiness nor publishes that page. Every generated history enters + // this state, separately for each adapter's scheduler. + let interruption: { error: unknown } | undefined + try { + readiness(false, `initial pending`) + acquisitionPending(true, `initial pending`) + compare(undefined, `initial pending`) + await handle.setParam(history.jump) + readiness(false, `initial loading move`) + reach.reach(`initial loading move`) + compare(undefined, `initial loading move`) + source.settleNext() + await handle.flush() + acquisitionPending(true, `initial oldest settled while latest pending`) + readiness(false, `initial oldest settled while latest pending`) + reach.reach(`initial oldest settled while latest pending`) + compare(undefined, `initial oldest settled while latest pending`) + for (let turn = 0; turn < 20 && source.loadStarted(); turn++) { + source.settle() + await handle.flush() + } + acquisitionPending(false, `initial ready`) + readiness(true, `initial ready`) + reach.reach(`initial ready`) + compare(history.jump, `initial ready`) + + const firstMove = history.jump + history.width + 1 + const latestMove = firstMove + history.jump + 1 + await handle.setParam(firstMove) + acquisitionPending(true, `first move pending`) + compare(history.jump, `first move pending`) + await handle.setParam(latestMove) + acquisitionPending(true, `overlap pending`) + reach.reach(`overlap pending`) + compare(history.jump, `overlap pending`) + // One old request settles while its replacement remains held. Its result + // cannot establish the later requested window on its own. + source.settleNext() + await handle.flush() + acquisitionPending(true, `oldest settled while latest pending`) + reach.reach(`oldest settled while latest pending`) + compare(history.jump, `oldest settled while latest pending`) + for (let turn = 0; turn < 20 && source.loadStarted(); turn++) { + source.settle() + await handle.flush() + } + acquisitionPending(false, `settled latest`) + reach.reach(`settled latest`) + compare(latestMove, `settled latest`) + await handle.setParam(20) + acquisitionPending(true, `unmounted pending`) + reach.reach(`unmounted pending`) + unmount() + unmount = undefined + source.settle() + await handle.flush() + } catch (error) { + interruption = { error } + } + // A later action/premise failure cannot erase an earlier comparison. Keep + // it as a secondary diagnostic; ScenarioLifetime adds cleanup separately. + const errors = [...failures, ...(interruption ? [interruption.error] : [])] + if (errors.length) + throw new AggregateError( + errors, + `Pending window mismatches${interruption ? `; history interrupted` : ``}: ${errors.map(String).join(`; `)}`, + { cause: errors[0] }, + ) + }) +} + +declareOracle({ + grammar: [pendingWindowArbitrary], + check: [checkPendingWindowHistory], +}) + +function registerPendingWindowOracle(driver: LiveQueryDriver) { + for (const campaign of [`fixed`, `fresh`] as const) { + const property = windowHistoryProperty(driver, `pending-window-history`) + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`pending window histories (${campaign})`, async () => { + const reach = createOracleReach( + `${driver.name}/${property}/${campaign}`, + pendingStates, + { fixedHistories: pendingExamples.length }, + ) + let index = 0 + const fidelity = createOracleFailureFidelity<{ + width: number + jump: number + }>() + const propertyUnderTest = fc.asyncProperty( + pendingWindowArbitrary, + async (history) => { + reach.beginHistory({ fixed: index++ < pendingExamples.length }) + await fidelity.run(history, () => + checkPendingWindowHistory(driver, history, reach), + ) + }, + ) + try { + await fidelity.assert(() => + fc.assert( + propertyUnderTest, + campaign === `fixed` + ? { + numRuns: oracleRuns(6) + pendingExamples.length, + seed: 2291, + examples: pendingExamples, + } + : oraclePropertyOptions(6, property, { + examples: pendingExamples, + }), + ), + ) + } finally { + console.info( + `pending reach ${driver.name}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + reach.assertReached() + }, 30_000) + } +} + +/** + * A source publication may cause application code to change the input that a + * framework binding reads. The declarative limit/offset contract still applies: + * at the driver's settled host cut, the same Collection shows the new window. + * The application never calls imperative setWindow, whose reentrancy guard is + * outside this input language (contract.ts; maintainer decision 2026-10-30). + * + * This receiving lane uses all six real framework drivers. A direct Collection + * change callback changes the binding's parameter while source publication is + * on the stack. The callback entry is measured; no delayed trigger simulates it. + * A one-shot guard permits the moved window to publish without another input + * change. The model is the existing full sort/filter/slice over authored rows. + * These histories bound the claim to eager sources, direct ordered list queries, + * and one callback-driven move; core owns imperative reentrancy and repair. + */ +const publicationWindowArbitrary = fc.record({ + width: fc.integer({ min: 1, max: 3 }), + jump: fc.integer({ min: 1, max: 4 }), + descending: fc.boolean(), + move: fc.constantFrom(`offset` as const, `limit` as const, `both` as const), +}) +const publicationWindowExamples = [ + [{ width: 1, jump: 1, descending: false, move: `offset` as const }], + [{ width: 2, jump: 2, descending: true, move: `limit` as const }], + [{ width: 3, jump: 3, descending: false, move: `both` as const }], +] +const publicationWindowStates = [ + `publication callback entered`, + `offset requested in callback`, + `limit requested in callback`, + `callback update settled`, +] as const + +async function checkPublicationWindowHistory( + driver: LiveQueryDriver, + history: { + width: number + jump: number + descending: boolean + move: `offset` | `limit` | `both` + }, + reach: ReturnType< + typeof createOracleReach<(typeof publicationWindowStates)[number]> + >, +) { + const lifetime = new ScenarioLifetime() + let handle: ControllableHandle | undefined + let unsubscribe: (() => void) | undefined + lifetime.defer(() => unsubscribe?.()) + lifetime.defer(() => handle?.unmount()) + await lifetime.run(async () => { + const rows = Array.from({ length: 8 }, (_, index) => ({ + id: String(index).padStart(2, `0`), + rank: index, + })) + const source = driver.makeSource(rows) + lifetime.defer(() => source.collection.cleanup({ dependents: true })) + const initial: WindowInput = { + offset: 0, + limit: history.width, + minimum: -10, + descending: history.descending, + nestedLimit: rows.length, + } + const next: WindowInput = { + ...initial, + offset: history.move === `limit` ? 0 : history.jump, + limit: history.move === `offset` ? history.width : history.width + 1, + } + const mounted = driver.mountControllable( + (q: Parameters[0], input) => + q + .from({ row: source.collection }) + .orderBy( + ({ row }: { row: WindowRow }) => row.rank, + input.descending ? `desc` : `asc`, + ) + .orderBy(({ row }: { row: WindowRow }) => row.id) + .limit(input.limit) + .offset(input.offset) + .select(({ row }: { row: WindowRow }) => ({ + id: row.id, + rank: row.rank, + })), + initial, + ) + handle = mounted + await mounted.flush() + const collection = mounted.collection() + checkWindowObservation( + { rows: mounted.current().data, collection }, + { rows: windowRows(rows, initial, false), retain: true }, + `initial window`, + ) + atOracleCheckpoint(`enabled window Collection`, `initial window`, () => + expect(collection, `mounted callback owner`).not.toBeNull(), + ) + let entered = false + let update: + | Promise<{ fulfilled: true } | { fulfilled: false; error: unknown }> + | undefined + const subscription = collection!.subscribeChanges( + () => { + if (entered) return + entered = true + reach.reach(`publication callback entered`) + if (next.offset !== initial.offset) + reach.reach(`offset requested in callback`) + if (next.limit !== initial.limit) + reach.reach(`limit requested in callback`) + // Observe rejection immediately. The source may publish before the + // native scheduler's update promise settles, so an unhandled rejection + // would otherwise replace the intended callback-update comparison. + update = mounted.setParam(next).then( + () => ({ fulfilled: true as const }), + (error: unknown) => ({ fulfilled: false as const, error }), + ) + }, + { includeInitialState: false }, + ) + unsubscribe = () => subscription.unsubscribe() + const inserted = { + id: `new`, + rank: history.descending ? 100 : -1, + } + rows.push(inserted) + // setParam supplies the driver's native update scope. Wrapping this source + // write in apply as well would overlap two async React act scopes; the + // callback itself must remain on the source publication stack. + source.insert({ ...inserted }) + atOracleCheckpoint( + `publication callback premise`, + `callback entered`, + () => { + expect(entered, `source publication invoked the callback`).toBe(true) + expect(update, `callback submitted the parameter update`).toBeDefined() + }, + ) + const outcome = await update! + atOracleCheckpoint( + `callback update fulfills`, + `callback update settled`, + () => { + if (!outcome.fulfilled) + throw new Error( + `Publication callback window update rejected: ${String(outcome.error)}`, + { cause: outcome.error }, + ) + }, + ) + reach.reach(`callback update settled`) + await mounted.flush() + const expected = windowRows(rows, next, false) + checkWindowObservation( + { rows: mounted.current().data, collection: mounted.collection() }, + { rows: expected, previousCollection: collection, retain: true }, + `callback update settled`, + ) + atOracleCheckpoint(`keyed window rows`, `callback update settled`, () => + expectUnorderedRows([...mounted.current().state!.values()], expected), + ) + }) +} + +function registerPublicationWindowOracle(driver: LiveQueryDriver) { + for (const campaign of [`fixed`, `fresh`] as const) { + const property = windowHistoryProperty(driver, `publication-window-history`) + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`publication callback window histories (${campaign})`, async () => { + const reach = createOracleReach( + `${driver.name}/${property}/${campaign}`, + publicationWindowStates, + { fixedHistories: publicationWindowExamples.length }, + ) + let index = 0 + const fidelity = createOracleFailureFidelity<{ + width: number + jump: number + descending: boolean + move: `offset` | `limit` | `both` + }>() + const propertyUnderTest = fc.asyncProperty( + publicationWindowArbitrary, + async (history) => { + reach.beginHistory({ + fixed: index++ < publicationWindowExamples.length, + }) + await fidelity.run(history, () => + checkPublicationWindowHistory(driver, history, reach), + ) + }, + ) + try { + await fidelity.assert(() => + fc.assert( + propertyUnderTest, + campaign === `fixed` + ? { + numRuns: oracleRuns(6) + publicationWindowExamples.length, + seed: 2291, + examples: publicationWindowExamples, + } + : oraclePropertyOptions(6, property, { + examples: publicationWindowExamples, + }), + ), + ) + } finally { + console.info( + `publication callback reach ${driver.name}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + reach.assertReached() + }, 30_000) + } +} + +declareOracle({ + grammar: [publicationWindowArbitrary], + check: [checkPublicationWindowHistory], +}) diff --git a/packages/db/tests/failure-fidelity.test.ts b/packages/db/tests/failure-fidelity.test.ts new file mode 100644 index 0000000000..24446dffc4 --- /dev/null +++ b/packages/db/tests/failure-fidelity.test.ts @@ -0,0 +1,137 @@ +import fc from 'fast-check' +import { expect, it } from 'vitest' +import { + OracleFailureReport, + atOracleCheckpoint, + createOracleFailureFidelity, +} from './oracle-failure-fidelity' + +it(`keeps the first law and checkpoint while reducing a two-fault history`, async () => { + const fidelity = createOracleFailureFidelity() + const result = await fc.check( + fc.asyncProperty(fc.integer({ min: 0, max: 20 }), (input) => + fidelity.run(input, () => { + atOracleCheckpoint( + input > 0 ? `ordered rows` : `Collection retention`, + `settled window`, + () => { + throw new Error(`deliberate wrong answer`) + }, + ) + }), + ), + { seed: 2291, numRuns: 1 }, + ) + expect(result.failed).toBe(true) + expect(result.counterexample).toEqual([1]) + expect(result.errorInstance).toBeInstanceOf(OracleFailureReport) + const report = result.errorInstance as OracleFailureReport + expect(report.original.fingerprint).toBe(report.reduced.fingerprint) + expect(report.original.fingerprint).toContain(`ordered rows`) + expect(report.nonReproducing.map((failure) => failure.fingerprint)).toContain( + JSON.stringify([`comparison`, `Collection retention`, `settled window`]), + ) + expect(report.original.trace).not.toBe(report.reduced.trace) +}) + +it(`preserves an initial setup failure instead of shrinking to a law failure`, async () => { + const fidelity = createOracleFailureFidelity() + const result = await fc.check( + fc.asyncProperty(fc.integer({ min: 0, max: 20 }), (input) => + fidelity.run(input, () => { + if (input > 0) throw new Error(`setup failed`) + atOracleCheckpoint(`ordered rows`, `settled window`, () => { + throw new Error(`wrong rows`) + }) + }), + ), + { seed: 2291, numRuns: 1 }, + ) + const report = result.errorInstance as OracleFailureReport + expect(result.counterexample).toEqual([1]) + expect(report.original.fingerprint).toContain(`setup failed`) + expect(report.original.fingerprint).toBe(report.reduced.fingerprint) +}) + +it(`keeps cleanup diagnostics and rejects a matching secondary mismatch`, async () => { + const fidelity = createOracleFailureFidelity() + const cleanup = new Error(`cleanup failed`) + const rowFailure = () => { + try { + atOracleCheckpoint(`ordered rows`, `settled window`, () => { + throw new Error(`wrong rows`) + }) + } catch (error) { + return error + } + throw new Error(`calibration did not fail`) + } + const result = await fc.check( + fc.asyncProperty(fc.integer({ min: 0, max: 20 }), (input) => + fidelity.run(input, () => { + if (input === 0) + throw new AggregateError([new Error(`action failed`), rowFailure()]) + throw new AggregateError([rowFailure(), cleanup]) + }), + ), + { seed: 2291, numRuns: 1 }, + ) + const report = result.errorInstance as OracleFailureReport + expect(result.counterexample).toEqual([1]) + expect(report.original.fingerprint).toContain(`ordered rows`) + expect(report.nonReproducing[0]?.fingerprint).toContain(`action failed`) + expect((report.original.error as AggregateError).errors).toContain(cleanup) +}) + +it(`reports a non-reproducing final shrink after the retained error was thrown`, async () => { + const fidelity = createOracleFailureFidelity() + const setup = new Error(`final shrink setup failure`) + const result = await fc.check( + fc.asyncProperty(fc.integer({ min: 0, max: 1 }), (input) => + fidelity.run(input, () => { + if (input === 0) throw setup + atOracleCheckpoint(`ordered rows`, `settled window`, () => { + throw new Error(`wrong rows`) + }) + }), + ), + { seed: 2291, numRuns: 1, examples: [[1]] }, + ) + const report = result.errorInstance as OracleFailureReport + expect(result.counterexample).toEqual([1]) + expect(report.original.fingerprint).toContain(`ordered rows`) + expect(report.nonReproducing[0]?.error).toBe(setup) + expect(report.errors).toContain(setup) + expect(report.message).toContain(`final shrink setup failure`) +}) + +it(`finalizes rejected-shrink diagnostics after fast-check renders its error`, async () => { + const fidelity = createOracleFailureFidelity() + let failure: unknown + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(fc.integer({ min: 0, max: 1 }), (input) => + fidelity.run(input, () => { + if (input === 0) throw new Error(`last setup failure`) + atOracleCheckpoint(`ordered rows`, `settled window`, () => { + throw new Error(`wrong rows`) + }) + }), + ), + { seed: 0, numRuns: 1 }, + ), + ) + } catch (error) { + failure = error + } + expect(failure).toBeInstanceOf(AggregateError) + expect(String(failure)).toContain(`seed: 0`) + expect(String(failure)).toContain(`path: "0"`) + expect(String(failure)).toContain(`last setup failure`) + expect(String(failure)).toContain(`Original trace: 1`) + expect(String(failure)).toContain(`Reduced trace: 1`) + expect((failure as AggregateError).errors[1]).toBeInstanceOf( + OracleFailureReport, + ) +}) diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 49016d06ee..a248ea4571 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -5,12 +5,47 @@ export { recordOracleReplay } from './oracle-replay-witness.js' type OracleEnvironment = Record const staticOracleProperties = [ + `framework.react.window-history`, + `framework.react.pending-window-history`, + `framework.react.publication-window-history`, + `framework.react-dependency-list.window-history`, + `framework.react-dependency-list.pending-window-history`, + `framework.react-dependency-list.publication-window-history`, + `framework.preact.window-history`, + `framework.preact.pending-window-history`, + `framework.preact.publication-window-history`, + `framework.vue.window-history`, + `framework.vue.pending-window-history`, + `framework.vue.publication-window-history`, + `framework.solid.window-history`, + `framework.solid.pending-window-history`, + `framework.solid.publication-window-history`, + `framework.angular.window-history`, + `framework.angular.pending-window-history`, + `framework.angular.publication-window-history`, + `framework.svelte.window-history`, + `framework.svelte.pending-window-history`, + `framework.svelte.publication-window-history`, + `react.window-render`, + `preact.window-render`, `lint.part-reach`, `oracle.declared-reach`, `scripts.lint-scope`, `scripts.run-budget`, `scripts.release-version`, `scripts.release-seed-failure`, + `react.ssr-effects-react18-ordinary-provider`, + `react.ssr-effects-react18-ordinary-explicit`, + `react.ssr-effects-react18-suspense-provider`, + `react.ssr-effects-react18-suspense-explicit`, + `react.ssr-effects-react18-standalone`, + `react.ssr-effects-react18-supplied`, + `react.ssr-effects-current-ordinary-provider`, + `react.ssr-effects-current-ordinary-explicit`, + `react.ssr-effects-current-suspense-provider`, + `react.ssr-effects-current-suspense-explicit`, + `react.ssr-effects-current-standalone`, + `react.ssr-effects-current-supplied`, `react.ssr-ordinary`, `react.ssr-infinite`, `react.ssr-stream-cut`, @@ -342,6 +377,7 @@ const staticOracleProperties = [ `includes.bucket-collisions`, `query.distinct-collisions`, `ivm.topk-support-window`, + `ivm.topk-numeric-window`, `opfs.close`, `opfs.failure-cleanup`, `opfs.mixed-execute`, diff --git a/packages/db/tests/oracle-failure-fidelity.ts b/packages/db/tests/oracle-failure-fidelity.ts new file mode 100644 index 0000000000..d67831cf12 --- /dev/null +++ b/packages/db/tests/oracle-failure-fidelity.ts @@ -0,0 +1,138 @@ +/** + * Preserve an oracle's first violated law and checkpoint while fast-check + * reduces its input (oracle rubric ORC-010). These are test-harness facts, not + * production state. A different failure is a diagnostic, not a reproduction. + */ +export class OracleCheckpointFailure extends Error { + constructor( + readonly law: string, + readonly checkpoint: string, + cause: unknown, + ) { + super(`${law} at ${checkpoint}: ${String(cause)}`, { cause }) + this.name = `OracleCheckpointFailure` + } +} + +/** Mark a synchronous comparison; asynchronous driver actions remain separate. */ +export function atOracleCheckpoint( + law: string, + checkpoint: string, + compare: () => T, +): T { + try { + return compare() + } catch (error) { + throw new OracleCheckpointFailure(law, checkpoint, error) + } +} + +interface FailureEvidence { + fingerprint: string + trace: string + error: unknown +} + +// Cleanup and history wrappers put the primary failure first. Never select a +// matching secondary mismatch when an earlier, different failure interrupted it. +function primaryFingerprint(error: unknown): string { + if (error instanceof AggregateError && error.errors.length > 0) + return primaryFingerprint(error.errors[0]) + if (error instanceof OracleCheckpointFailure) + return JSON.stringify([`comparison`, error.law, error.checkpoint]) + // An initial setup/action failure must also stay red. Its exact diagnostic is + // its fingerprint; it cannot silently become a later product-law mismatch. + return JSON.stringify([ + `unmarked`, + typeof error, + error instanceof Error ? error.name : null, + error instanceof Error ? error.message : String(error), + ]) +} + +export class OracleFailureReport extends AggregateError { + constructor( + readonly original: FailureEvidence, + readonly reduced: FailureEvidence, + readonly nonReproducing: ReadonlyArray, + ) { + super([], `Oracle reduction reproduced the original failure`, { + cause: original.error, + }) + // Fast-check retains the last reproducing error. Its final attempted + // reduction can fail differently, so diagnostics must remain live after + // this report was thrown. Cause and the two reproducing traces stay fixed. + Object.defineProperties(this, { + errors: { + get: () => [ + original.error, + ...(reduced.error === original.error ? [] : [reduced.error]), + ...nonReproducing.map((failure) => failure.error), + ], + }, + message: { + get: () => + `Oracle reduction reproduced the original failure ${original.fingerprint}` + + `\nOriginal trace: ${original.trace}` + + `\nReduced trace: ${reduced.trace}` + + (nonReproducing.length + ? `\nNon-reproducing reductions: ${nonReproducing.map((failure) => failure.fingerprint).join(`; `)}` + : ``), + }, + }) + this.name = `OracleFailureReport` + } +} + +/** One instance belongs to one campaign, including its shrink attempts. */ +export function createOracleFailureFidelity( + describeHistory: (history: THistory) => string = (history) => + JSON.stringify(history), +) { + let original: FailureEvidence | undefined + let reduced: FailureEvidence | undefined + const nonReproducing: Array = [] + return { + async run( + history: THistory, + body: () => void | Promise, + ): Promise { + // Capture before production, model stepping, or cleanup can alter inputs. + const trace = describeHistory(history) + try { + await body() + } catch (error) { + const failure = { fingerprint: primaryFingerprint(error), trace, error } + original ??= failure + if (failure.fingerprint !== original.fingerprint) { + nonReproducing.push(failure) + // Accept only as a shrink candidate. The campaign is already red; + // this candidate supplies no successful product comparison evidence. + return + } + reduced = failure + throw new OracleFailureReport(original, failure, nonReproducing) + } + }, + async assert(campaign: () => Promise): Promise { + try { + await campaign() + } catch (error) { + if (original === undefined || reduced === undefined) throw error + // fast-check renders its retained error before the last rejected + // candidate. Finalize afterwards so its seed/path diagnostic and every + // non-reproducing reduction survive in the rendered failure as well. + const report = new OracleFailureReport( + original, + reduced, + nonReproducing, + ) + throw new AggregateError( + [error, report], + `${String(error)}\n${report.message}`, + { cause: error }, + ) + } + }, + } +} diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index 16317346d5..dab23c41ac 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -13,6 +13,62 @@ import { registeredOracleProperties } from './oracle-config.js' const ownerGroups: ReadonlyArray< readonly [string, string, string] | readonly [string, string, string, string] > = [ + [ + `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`, + ], + [ + `solid-db/tests/conformance.test.tsx`, + `framework.solid`, + `window-history pending-window-history publication-window-history`, + `db/tests/conformance/window-history-oracle.ts`, + ], + [ + `angular-db/tests/conformance.test.ts`, + `framework.angular`, + `window-history pending-window-history publication-window-history`, + `db/tests/conformance/window-history-oracle.ts`, + ], + [ + `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/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`, + `react`, + [ + `ssr-effects-react18-ordinary-provider ssr-effects-react18-ordinary-explicit`, + `ssr-effects-react18-suspense-provider ssr-effects-react18-suspense-explicit`, + `ssr-effects-react18-standalone ssr-effects-react18-supplied`, + `ssr-effects-current-ordinary-provider ssr-effects-current-ordinary-explicit`, + `ssr-effects-current-suspense-provider ssr-effects-current-suspense-explicit`, + `ssr-effects-current-standalone ssr-effects-current-supplied`, + ].join(` `), + ], [ `../scripts/eslint-plugin-oracle/part-reach-oracle.property.test.js`, `lint`, @@ -626,7 +682,7 @@ const ownerGroups: ReadonlyArray< [ `db-ivm/tests/operators/topk-support-window-oracle.test.ts`, `ivm`, - `topk-support-window`, + `topk-support-window topk-numeric-window`, ], [ `db-sqlite-persistence-core/tests/sqlite-boolean-arity-oracle.test.ts`, @@ -744,6 +800,9 @@ export type OracleReplayManifestEntry = { // Pagination cells carry their property name in the test title so the guard // can select one cell without running the rest of the matrix. const replayTestNames = new Map([ + [`react.window-render`, `window changes at React commit.*fresh`], + [`preact.window-render`, `window changes at Preact commit.*fresh`], + [`ivm.topk-numeric-window`, `Numeric window oracle:.*random campaign`], [`ivm.distinct-collisions`, `preserves distinct collisions.*random`], [ `ivm.distinct-representatives`, @@ -883,6 +942,22 @@ const replayTestNames = new Map([ const seedReplayProperties = new Set([`evaluators.like`]) function directReplayTestName(property: string): string | undefined { + if (property.startsWith(`framework.`)) { + switch (property.split(`.`).at(-1)) { + case `window-history`: + return `(?:direct|nested) window histories \\(fresh\\)` + case `pending-window-history`: + return `pending window histories \\(fresh\\)` + case `publication-window-history`: + return `publication callback window histories \\(fresh\\)` + } + } + if (property.startsWith(`react.ssr-effects-`)) { + const [runtime, ...path] = property + .slice(`react.ssr-effects-`.length) + .split(`-`) + return `${runtime} ${path.join(`-`)} renders without client layout diagnostics \\(random\\)` + } if (property === `replay.ordered-window-faults`) return `keeps the ordered window under load and release faults \\(replay campaign\\)` if (property === `replay.ordered-failed-replay`) diff --git a/packages/db/tests/oracle-replay.test.ts b/packages/db/tests/oracle-replay.test.ts index 5ffec311db..de31faefbc 100644 --- a/packages/db/tests/oracle-replay.test.ts +++ b/packages/db/tests/oracle-replay.test.ts @@ -40,19 +40,20 @@ function runReplay( fault?: string, property = `oracle-replay.calibration`, cleanupFailure = false, + workingDirectory = packageDirectory, ): { status: number | null; output: string } { const result = spawnSync( process.execPath, [ `--import`, `tsx`, - `tests/oracle-replay.ts`, + join(packageDirectory, `tests/oracle-replay.ts`), ...args, `--maxWorkers=1`, `--coverage.enabled=false`, ], { - cwd: packageDirectory, + cwd: workingDirectory, env: { ...process.env, TANSTACK_DB_ORACLE_RUNS_MULTIPLIER: `1`, @@ -77,6 +78,19 @@ describe(`guarded oracle replay`, () => { // replays instead of starving Vitest's RPC channel across the whole suite. afterEach(() => setImmediate()) + it(`rejects a window replay through a different framework driver`, () => { + const result = runReplay( + [`tests/conformance.test.tsx`], + undefined, + `framework.solid.window-history`, + false, + join(repositoryDirectory, `packages/react-db`), + ) + expect(result.status, result.output).toBe(1) + expect(result.output).toContain(`oracle replay target never executed`) + expect(result.output).toContain(`"executions":0`) + }, 40_000) + it(`runs a named replay directly without the owner's fixed campaign`, () => { const result = runReplay([fixture]) expect(result.status, result.output).toBe(0) @@ -370,6 +384,37 @@ describe(`oracle replay reporting`, () => { }) describe(`named oracle replay manifest`, () => { + // ORC-007 requires the same production path during replay. A shared model + // does not make different framework schedulers interchangeable. Enumerate + // the finite driver × history grammar, including React's dependency-list + // path, and compare its independently named owner with the manifest. + it.each( + [ + [`react`, `react-db/tests/conformance.test.tsx`], + [`react-dependency-list`, `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`], + ].flatMap(([driver, owner]) => + [ + `window-history`, + `pending-window-history`, + `publication-window-history`, + ].map((family) => [`framework.${driver}.${family}`, owner]), + ), + )(`keeps the originating driver for %s`, (property, owner) => { + const entry = oracleReplayManifest.find( + (candidate) => candidate.property === property, + ) + expect(entry, property).toMatchObject({ + file: `packages/${owner}`, + status: `assertion`, + source: `packages/db/tests/conformance/window-history-oracle.ts`, + }) + }) + it(`accounts for every registry name`, () => { expect( new Set(oracleReplayManifest.map(({ property }) => property)), @@ -430,6 +475,26 @@ describe(`named oracle replay manifest`, () => { expect(source).toContain( family?.[1] ?? `includes.matrix.flat-materialization`, ) + } else if (entry.property.startsWith(`framework.`)) { + const [, driver, family] = entry.property.split(`.`) + const driverSource = readFileSync( + join(repositoryDirectory, entry.file), + `utf8`, + ) + const driverName = + driver === `react-dependency-list` + ? `react (dependency list)` + : driver + expect(driverSource).toContain(`name: \`${driverName}\``) + expect(source).toContain(`windowHistoryProperty(driver, \`${family}\`)`) + } else if (entry.property.startsWith(`react.ssr-effects-`)) { + const [runtime, ...parts] = entry.property + .slice(`react.ssr-effects-`.length) + .split(`-`) + expect([`react18`, `current`]).toContain(runtime) + expect(source).toContain(`react.ssr-effects-\${runtime.name}-\${path}`) + expect(source).toContain(`\`${runtime}\``) + expect(source).toContain(`\`${parts.join(`-`)}\``) } else if (/^(utils|comparison)\.\d+$/.test(entry.property)) { // These oracles number their properties in file order. const [family] = entry.property.split(`.`) diff --git a/packages/preact-db/src/useLiveQuery.ts b/packages/preact-db/src/useLiveQuery.ts index 4691300f02..0949e0a6e7 100644 --- a/packages/preact-db/src/useLiveQuery.ts +++ b/packages/preact-db/src/useLiveQuery.ts @@ -4,6 +4,7 @@ import { createLiveQuerySourceIdClaims, getLiveQueryHash, getLiveQueryKey, + getStableValueHash, identifyLiveQuery, isCollection, prepareLiveQueryValue, @@ -238,7 +239,7 @@ export function useLiveQuery( const identity = isCollection(input) ? input : queryKey - ? getLiveQueryHash(preparedValue, queryKey) + ? getStableValueHash([`queryKey`, queryKey], `queryKey`) : identifyLiveQuery( preparedValue, { @@ -251,17 +252,30 @@ export function useLiveQuery( const bindingRef = useRef(null) let binding = bindingRef.current + let commitWindow: (() => void) | undefined if (binding?.identity !== identity) { - binding = bindingRef.current = { - identity, - // Wholesale mode publishes nothing during subscribe; the hook re-reads - // the observer's snapshot on each notification. - observer: createLiveQueryObserver( - resolveLiveQueryValue(preparedValue, { gcTime: DEFAULT_GC_TIME_MS }), - { mode: `wholesale` }, - ), + const collection = resolveLiveQueryValue(preparedValue, { + gcTime: DEFAULT_GC_TIME_MS, + previousCollection: binding?.collection ?? null, + windowOwner: bindingRef, + onWindowChange: (move) => { + commitWindow = move + }, + }) + if (!binding || collection !== binding.collection) { + binding = bindingRef.current = { + identity, + collection, + observer: createLiveQueryObserver(collection, { mode: `wholesale` }), + } } } + useLayoutEffect(() => { + if (commitWindow) { + binding.identity = identity + commitWindow() + } + }) const { observer } = binding const snapshot = observer.getSnapshot() @@ -283,5 +297,6 @@ export function useLiveQuery( /** The live query one hook instance reads until its identity changes. */ interface LiveQueryBinding { identity: unknown + collection: ReturnType observer: LiveQueryObserver } diff --git a/packages/preact-db/tests/window-render-oracle.test.tsx b/packages/preact-db/tests/window-render-oracle.test.tsx new file mode 100644 index 0000000000..51f954c8c6 --- /dev/null +++ b/packages/preact-db/tests/window-render-oracle.test.tsx @@ -0,0 +1,285 @@ +/** + * A Preact window moves only after its render commits (the shared framework + * contract). An error boundary can reject a render after the hook returned. + * That render cannot move the previously committed Collection or acquire rows. + * This receives the shared window model/checker through real Preact rendering, + * with eager and finite synchronous on-demand sources. It does not establish + * Suspense, concurrent scheduling, or asynchronous provider settlement. + * Separate offset-only, limit-only and combined campaigns receive each kind + * of window move; a fixed width would miss speculative resizing. + */ +import { Component, h, render } from 'preact' +import { useLayoutEffect } from 'preact/hooks' +import { act } from 'preact/test-utils' +import { describe, expect, it } from 'vitest' +import fc from 'fast-check' +import { + BTreeIndex, + createCollection, + localOnlyCollectionOptions, +} from '@tanstack/db' +import { useLiveQuery } from '../src/useLiveQuery' +import { makeInfiniteOnDemandSource } from '../../db/tests/conformance/infinite-on-demand' +import { ScenarioLifetime } from '../../db/tests/conformance/scenario-lifetime' +import { + checkWindowObservation, + windowRows, +} from '../../db/tests/conformance/window-history-oracle' +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 { settle } from './render-probe' +import type { InitialQueryBuilder } from '@tanstack/db' +import type { ComponentChildren } from 'preact' +import type { WindowRow } from '../../db/tests/conformance/window-history-oracle' + +const renderHistoryArbitrary = fc.record({ + aborted: fc.integer({ min: 1, max: 4 }), + committed: fc.integer({ min: 5, max: 8 }), + limit: fc.integer({ min: 1, max: 3 }), + abortedLimit: fc.integer({ min: 4, max: 6 }), + committedLimit: fc.integer({ min: 7, max: 9 }), +}) +const examples = [ + [{ aborted: 2, committed: 5, limit: 2, abortedLimit: 4, committedLimit: 7 }], +] as const +const states = [`committed move`, `aborted move`] as const +type RenderHistory = { + aborted: number + committed: number + limit: number + abortedLimit: number + committedLimit: number +} +type PageWindow = { offset: number; limit: number } +type Move = `offset` | `limit` | `both` + +class RejectionBoundary extends Component<{ + onError: (error: unknown) => void + children?: ComponentChildren +}> { + state = { failed: false } + componentDidCatch(error: unknown) { + this.props.onError(error) + this.setState({ failed: true }) + } + render() { + return this.state.failed ? null : this.props.children + } +} + +async function checkRenderHistory( + history: RenderHistory, + remote: boolean, + keyed: boolean, + move: Move, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + const container = document.createElement(`div`) + lifetime.defer(() => act(() => render(null, container))) + let releaseRetainedConsumer: (() => void) | undefined + lifetime.defer(() => releaseRetainedConsumer?.()) + await lifetime.run(async () => { + const rows: Array = Array.from({ length: 24 }, (_, id) => ({ + id: String(id).padStart(2, `0`), + rank: id, + })) + const provider = remote + ? makeInfiniteOnDemandSource({ createCollection, BTreeIndex }, rows) + : undefined + const source = + provider?.collection ?? + createCollection( + localOnlyCollectionOptions({ + getKey: (row: WindowRow) => row.id, + initialData: rows, + autoIndex: `eager`, + defaultIndexType: BTreeIndex, + }), + ) + lifetime.defer(() => source.cleanup({ dependents: true })) + let committed: + | { + rows: unknown + collection: { + toArray: unknown + subscribeChanges: (listener: () => void) => { + unsubscribe: () => void + } + } + } + | undefined + let aborted = 0 + const rejection = new Error(`Abort the render after useLiveQuery`) + let boundaryError: unknown + const initial = { offset: 0, limit: history.limit } + const abortedWindow = { + offset: move === `limit` ? 0 : history.aborted, + limit: move === `offset` ? history.limit : history.abortedLimit, + } + const accepted = { + offset: move === `limit` ? 0 : history.committed, + limit: move === `offset` ? history.limit : history.committedLimit, + } + function Page({ offset, limit, fail }: PageWindow & { fail: boolean }) { + const query = (q: InitialQueryBuilder) => + q + .from({ row: source }) + .orderBy(({ row }) => row.rank, `desc`) + .limit(limit) + .offset(offset) + .select(({ row }) => ({ id: row.id, rank: row.rank })) + const result = useLiveQuery({ + query, + ...(keyed ? { queryKey: [`window-render`, offset, limit] } : {}), + }) + useLayoutEffect(() => { + committed = { rows: result.data, collection: result.collection } + }) + if (fail) { + aborted++ + throw rejection + } + return null + } + const commit = (window: PageWindow, fail = false) => + act(() => + render( + h( + RejectionBoundary, + { onError: (error) => (boundaryError = error) }, + h(Page, { ...window, fail }), + ), + container, + ), + ) + const expected = (window: PageWindow) => + windowRows( + rows, + { + ...window, + minimum: -1, + nestedLimit: rows.length, + descending: true, + }, + false, + ) + commit(initial) + await settle() + const collection = committed!.collection + // The public Collection can have another real consumer after the hook's + // error boundary unmounts it. Keeping that consumer makes a queued illegal + // move observable; teardown cannot mask it by removing the last subscriber. + const retainedConsumer = collection.subscribeChanges(() => {}) + releaseRetainedConsumer = () => retainedConsumer.unsubscribe() + const compare = (window: PageWindow, cut: string, actual = committed!) => + checkWindowObservation( + actual, + { + rows: expected(window), + previousCollection: collection, + retain: true, + }, + cut, + ) + compare(initial, `initial`) + commit(accepted) + await settle() + reach.reach(`committed move`) + compare(accepted, `committed`) + const loads = provider?.calls.length ?? 0 + commit(abortedWindow, true) + await settle() + atOracleCheckpoint(`render attempted`, `aborted`, () => { + expect(aborted, `Preact attempted the rejected render`).toBeGreaterThan(0) + expect( + boundaryError, + `error boundary received the render rejection`, + ).toBe(rejection) + }) + reach.reach(`aborted move`) + compare(accepted, `aborted`, { rows: collection.toArray, collection }) + atOracleCheckpoint(`speculative acquisition`, `aborted`, () => + expect( + provider?.calls.length ?? 0, + `aborted render acquires nothing`, + ).toBe(loads), + ) + // A width-only render-time move would resize the retained Collection even + // if offset mutation waited for commit. The shared checker rejects it. + if (move !== `offset`) + expect(() => + compare(accepted, `aborted width calibration`, { + rows: expected({ ...accepted, limit: abortedWindow.limit }), + collection, + }), + ).toThrow() + }) +} + +describe(`window changes at Preact commit`, () => { + 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 = `preact.window-render` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`${remote ? `on-demand` : `eager`}/${keyed ? `keyed` : `derived`}/${move}/${campaign}`, async () => { + const reach = createOracleReach( + `${property}/${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, 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( + `preact render reach ${remote}/${keyed}/${move}/${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + reach.assertReached() + }) + } +}) + +declareOracle({ + grammar: [renderHistoryArbitrary], + check: [checkRenderHistory], +}) diff --git a/packages/react-db/package.json b/packages/react-db/package.json index 6e785603d1..fcc9d73f32 100644 --- a/packages/react-db/package.json +++ b/packages/react-db/package.json @@ -59,6 +59,8 @@ "@types/react-dom": "^19.2.3", "@vitest/coverage-istanbul": "^4.1.12", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "react-dom18": "npm:react-dom@18.3.1", + "react18": "npm:react@18.3.1" } } diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 9cb9ad4d5f..f5efbbe737 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -1,6 +1,6 @@ 'use client' -import { useRef, useSyncExternalStore } from 'react' +import { useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react' import { UnhashableQueryIRError, createLiveQueryObserver, @@ -45,6 +45,11 @@ import type { SingleResult, } from '@tanstack/db' +// Use the client layout commit to schedule window moves. Server rendering has +// no commit; React18 diagnoses a layout effect there even when it is empty. +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. @@ -214,11 +219,8 @@ function warnDerivedIdentityHotPath( } function getExplicitDbClient(value: unknown): DbClient | undefined { - return value && - typeof value === `object` && - `client` in value && - (value as { client?: unknown }).client !== undefined - ? (value as { client: DbClient }).client + return value && typeof value === `object` + ? (value as { client?: DbClient }).client : undefined } @@ -257,14 +259,18 @@ export function getIdentityScope( */ export function prepareDerivedQuery( value: unknown, - profiler: DerivedIdentityProfiler, + profiler: DerivedIdentityProfiler | undefined, scope: LiveQueryIdentityScope, deferredCollections: Set>, ): DerivedQueryPreparation { - const shouldProfile = - shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`) && - process.env.NODE_ENV !== `production` - const start = shouldProfile ? getCurrentTime() : 0 + // An absent timestamp means profiling is disabled. Keep the literal build + // guard beside the measured work so production bundlers erase the profiler. + const start = + /* @__PURE__ */ shouldWarnInDevelopment( + `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`, + ) && process.env.NODE_ENV !== `production` + ? getCurrentTime() + : undefined const preparedValue = prepareQueryValue( value, scope.client, @@ -277,7 +283,7 @@ export function prepareDerivedQuery( identityDeps: [`derived`, identifyLiveQuery(preparedValue, scope)], } } finally { - if (shouldProfile) { + if (start !== undefined && profiler) { const durationMs = getCurrentTime() - start profiler.renderCount += 1 profiler.totalMs += durationMs @@ -564,17 +570,27 @@ export function useLiveQueryForSuspense(configOrQueryOrCollection: any) { // What one hook instance keeps across renders. It lives in a single ref // slot, so a render neither looks up nor allocates more. function createHookInstance(dbClient: DbClient | undefined) { + let derivedIdentityProfiler: DerivedIdentityProfiler | undefined + // Unbundled browsers can have no process global. Bundled development builds + // inline NODE_ENV and keep diagnostics even without that global. + try { + if (process.env.NODE_ENV !== `production`) { + derivedIdentityProfiler = { + renderCount: 0, + totalMs: 0, + maxMs: 0, + warned: false, + } + } + } catch { + // Without an environment, development diagnostics stay off. + } return { collection: null as Collection | null, identityDeps: null as Array | null, config: null as unknown, client: dbClient, - derivedIdentityProfiler: { - renderCount: 0, - totalMs: 0, - maxMs: 0, - warned: false, - } as DerivedIdentityProfiler, + derivedIdentityProfiler, deferredCollections: new Set< CollectionImpl >(), @@ -636,16 +652,14 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { } let queryHash: string | undefined - if (streamIdentity !== undefined) { - try { - queryHash = getStableValueHash(streamIdentity, `queryKey`) - } catch (error) { - if (error instanceof UnhashableQueryIRError) { - if (queryKey !== undefined) throw error - identityError = error - } else { - throw error - } + try { + queryHash = getStableValueHash(streamIdentity, `queryKey`) + } catch (error) { + if (error instanceof UnhashableQueryIRError) { + if (queryKey !== undefined) throw error + identityError = error + } else { + throw error } } @@ -697,25 +711,25 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { !deepEquals(instance.identityDeps, identityDeps) // Check if we need to create/recreate the collection - const needsNewCollection = + let needsNewCollection = !instance.collection || (inputIsCollection && instance.config !== configOrQueryOrCollection) || (!inputIsCollection && (instance.client !== dbClient || identityChanged)) + let commitWindow: (() => void) | undefined + const previousCollection = instance.collection if (needsNewCollection) { 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 // Passing the collection directly doesn't provide any predicates, so no data loads - const syncMode = ( - configOrQueryOrCollection as { config?: { syncMode?: string } } - ).config?.syncMode if ( - syncMode === `on-demand` && - shouldWarnInDevelopment( + /* @__PURE__ */ shouldWarnInDevelopment( `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`, ) && - process.env.NODE_ENV !== `production` + process.env.NODE_ENV !== `production` && + (configOrQueryOrCollection as { config?: { syncMode?: string } }) + .config?.syncMode === `on-demand` ) { console.warn( `[useLiveQuery] Warning: Passing a collection with syncMode "on-demand" directly to useLiveQuery ` + @@ -753,6 +767,12 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { // Hydration and Suspense key the live-query Collection by identity, // so they never pool, even when the config passes a pool. pool: forSuspense || dbClient ? false : undefined, + previousCollection: + forSuspense || dbClient ? undefined : previousCollection, + windowOwner: instance, + onWindowChange: (move) => { + commitWindow = move + }, // Without a DbClient, Suspense starts it through a preload below. startSync: !forSuspense || dbClient !== undefined, }) as SuspenseCollection | null @@ -768,49 +788,34 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { } if (suspenseCollections && suspenseKey && instance.collection) { const collection = instance.collection - const removeCleanupListener = collection.on( - `status:cleaned-up`, - () => - releaseSuspenseCollection( - suspenseCollections, - suspenseKey, - collection, - ), - ) + const retire = () => + releaseSuspenseCollection( + suspenseCollections, + suspenseKey, + collection, + ) // 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( - () => - releaseSuspenseCollection( - suspenseCollections, - suspenseKey, - collection, - ), - 0, - ) - const removeErrorListener = collection.on( - `status:error`, - retireAfterRetry, - ) - const removeCrashListener = collection.on( - `status:crashed`, - retireAfterRetry, - ) + const retireAfterRetry = () => setTimeout(retire, 0) + const removeStatusListeners = [ + collection.on(`status:cleaned-up`, retire), + collection.on(`status:error`, retireAfterRetry), + collection.on(`status:crashed`, retireAfterRetry), + ] const entry: SuspenseCollectionEntry = { collection, removeStatusListeners: () => { - removeCleanupListener() - removeErrorListener() - removeCrashListener() + for (const remove of removeStatusListeners) remove() }, } suspenseCollections.set(suspenseKey, entry) } } instance.config = configOrQueryOrCollection - instance.identityDeps = [...identityDeps] + // 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.client = dbClient instance.queryHash = queryHash @@ -818,6 +823,15 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { instance.identityError = identityError } + if (commitWindow && instance.collection === previousCollection) + needsNewCollection = false + useWindowCommitEffect(() => { + if (commitWindow) { + instance.identityDeps = [...identityDeps] + commitWindow() + } + }) + // 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 diff --git a/packages/react-db/tests/ssr-effects-driver.tsx b/packages/react-db/tests/ssr-effects-driver.tsx new file mode 100644 index 0000000000..ff31f4c710 --- /dev/null +++ b/packages/react-db/tests/ssr-effects-driver.tsx @@ -0,0 +1,154 @@ +// This subprocess supplies a real matched React/server-renderer runtime to the +// shared SSR preparation judgment. It records raw diagnostics and public hook +// results; the oracle in ssr-effects-oracle.test.ts owns every expectation. +import { createElement, version as reactVersion } from 'react' +import { renderToString, version as rendererVersion } from 'react-dom/server' +import { + DbClient, + Query, + collectionOptions, + createCollection, + localOnlyCollectionOptions, +} from '@tanstack/db' +import { DbProvider } from '../src/DbProvider' +import { useLiveQuery } from '../src/useLiveQuery' +import { useLiveSuspenseQuery } from '../src/useLiveSuspenseQuery' +import type { SsrSnapshotHistory } from '../../db/tests/ssr-snapshot-admission-oracle' + +async function observe({ + history, + path, +}: { + history: SsrSnapshotHistory + path: string +}) { + const diagnostics: Array = [] + const cleanupFailures: Array = [] + const originalError = console.error + console.error = (...args: Array) => { + diagnostics.push(args.map(String).join(` `)) + } + const client = new DbClient() + client._setSsrServerCleanupEnabled(true) + const descriptor = collectionOptions(`ssr-effects`, () => ({ + id: `ssr-effects`, + getKey: (row: { id: string; name: string }) => row.id, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (const row of history.rows) + write({ type: `insert`, value: { ...row } }) + commit() + if (history.sourceState === `ready`) markReady() + }, + }, + })) + const query = new Query() + .from({ person: descriptor }) + .orderBy(({ person }) => person.id) + .select(({ person }) => ({ id: person.id, name: person.name })) + const concrete = + path === `standalone` || path === `supplied` + ? createCollection( + localOnlyCollectionOptions({ + id: `ssr-concrete`, + getKey: (row: { id: string; name: string }) => row.id, + initialData: history.rows.map((row) => ({ ...row })), + }), + ) + : undefined + let calls = 0 + let view: { rows: unknown; status?: string } | undefined + let failure: string | undefined + try { + if (!concrete && history.sourceState !== `idle`) { + const warming = client.preloadLiveQuery({ + query: new Query().from({ person: descriptor }), + }) + if (history.sourceState === `ready`) await warming + else void warming.catch(() => {}) + } + if (!concrete && history.preparation === `source`) + client.collection(descriptor) + if ( + !concrete && + (history.preparation === `query` || + history.preparation === `pending-query`) + ) { + const preload = client.preloadLiveQuery({ query }) + if (history.preparation === `query`) await preload + else void preload.catch(() => {}) + } + function Consumer() { + calls++ + const config = { query, ...(path.endsWith(`explicit`) ? { client } : {}) } + const result = + path === `disabled` + ? useLiveQuery(() => null) + : path.startsWith(`suspense`) + ? useLiveSuspenseQuery(config) + : concrete + ? path === `supplied` + ? useLiveQuery(concrete) + : useLiveQuery((q) => q.from({ person: concrete })) + : useLiveQuery(config) + view = { + // The shared admission model observes authored id/name fields. Public + // virtual metadata is outside that judgment; malformed fields still fail. + rows: result.data?.map((row: unknown) => { + if ( + typeof row !== `object` || + row === null || + !(`id` in row) || + typeof row.id !== `string` || + !(`name` in row) || + typeof row.name !== `string` + ) { + throw new Error(`SSR fixture received malformed selected fields`) + } + return { id: row.id, name: row.name } + }), + ...(`status` in result ? { status: result.status } : {}), + } + return createElement(`output`, null, JSON.stringify(view)) + } + try { + const consumer = createElement(Consumer) + renderToString( + path.endsWith(`provider`) + ? createElement(DbProvider, { client }, consumer) + : consumer, + ) + } catch (error) { + failure = String(error) + } + return { + reactVersion, + rendererVersion, + calls, + view, + failure, + diagnostics, + cleanupFailures, + } + } finally { + console.error = originalError + try { + await client.cleanup() + if (concrete) await concrete.cleanup({ dependents: true }) + } catch (error) { + cleanupFailures.push(String(error)) + } + } +} + +observe(JSON.parse(process.argv[2]!)).then( + // SSR can leave intentional cache/GC timers. Cleanup has completed; ending + // this isolated process releases those timers before the next history. + (observation) => + process.stdout.write(JSON.stringify(observation), () => process.exit(0)), + (error: unknown) => { + console.error(error) + process.exitCode = 1 + }, +) diff --git a/packages/react-db/tests/ssr-effects-oracle.test.ts b/packages/react-db/tests/ssr-effects-oracle.test.ts new file mode 100644 index 0000000000..3fc9ca3c5a --- /dev/null +++ b/packages/react-db/tests/ssr-effects-oracle.test.ts @@ -0,0 +1,326 @@ +// @vitest-environment node +/** + * Supported React server renders must not report a client-only layout effect. + * Authority: react-db supports React >=18 and prepared SSR (SSR guide and + * #2229); React18's diagnostic means a layout effect cannot run on that host. + * Moving a page is a client commit operation and must add no such server work. + * + * This extends the SSR admission owner's preparation grammar and judgment with + * raw server diagnostics through real React18 and installed React19 renderers. + * Provider and explicit-client ordinary/Suspense paths cross the client boundary; + * standalone builders and supplied Collections check the diagnostic law alone. + * The child bundle aliases every React import to one runtime, including the + * renderer's imports. It uses current production source, without browser globals. + * It does not establish hydration equality or client commit timing: those remain + * with the admission and window-render receiving owners. The server projection + * drops browser/write actions because this observation precedes those actions. + */ +import { execFileSync } from 'node:child_process' +import { mkdtemp, rm } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { build } from 'esbuild' +import { fc } from '@fast-check/vitest' +import { afterAll, beforeAll, expect, it } from 'vitest' +import { + expectedSsrSnapshot, + ssrSnapshotExamples, + ssrSnapshotHistoryArbitrary, +} from '../../db/tests/ssr-snapshot-admission-oracle' +import { declareOracle } from '../../db/tests/oracle-declaration' +import { createOracleReach } from '../../db/tests/oracle-reach' +import { + oraclePropertyOptions, + oracleReplayTest, + oracleRuns, +} from '../../db/tests/oracle-config' +import { + atOracleCheckpoint, + createOracleFailureFidelity, +} from '../../db/tests/oracle-failure-fidelity' +import type { SsrSnapshotHistory } from '../../db/tests/ssr-snapshot-admission-oracle' + +const here = dirname(fileURLToPath(import.meta.url)) +const require = createRequire(import.meta.url) +const root = resolve(here, `../../..`) +let temporary: string +const programs = new Map() +const runtimes = [ + { name: `react18`, react: `react18`, dom: `react-dom18`, version: `18.3.1` }, + { name: `current`, react: `react`, dom: `react-dom`, version: `19.2.4` }, +] as const + +beforeAll(async () => { + temporary = await mkdtemp(join(tmpdir(), `react-db-ssr-effects-`)) + for (const runtime of runtimes) { + const outfile = join(temporary, `${runtime.name}.cjs`) + await build({ + entryPoints: [join(here, `ssr-effects-driver.tsx`)], + outfile, + bundle: true, + platform: `node`, + format: `cjs`, + alias: { + react: dirname(require.resolve(`${runtime.react}/package.json`)), + 'react-dom': dirname(require.resolve(`${runtime.dom}/package.json`)), + '@tanstack/db': join(root, `packages/db/src/index.ts`), + '@tanstack/db-ivm': join(root, `packages/db-ivm/src/index.ts`), + }, + define: { 'process.env.NODE_ENV': `"development"` }, + logLevel: `silent`, + }) + programs.set(runtime.name, outfile) + } +}) +afterAll(async () => { + if (temporary) await rm(temporary, { recursive: true, force: true }) +}) + +// Project the shared grammar onto the server-render cut. Later writes and +// browser actions cannot affect this cut. Source/preparation/row boundaries +// retain the shared admission judgment and its exclusions. +const serverHistory = ({ + preparation, + sourceState, + rows, +}: SsrSnapshotHistory) => ({ preparation, sourceState, rows }) +const histories = ssrSnapshotHistoryArbitrary.map(serverHistory) +const examples = ssrSnapshotExamples.map(serverHistory) +type Observation = { + reactVersion: string + rendererVersion: string + calls: number + view?: { rows: unknown; status?: string } + failure?: string + diagnostics: Array + cleanupFailures: Array +} +const paths = [ + `ordinary-provider`, + `ordinary-explicit`, + `suspense-provider`, + `suspense-explicit`, + `standalone`, + `supplied`, +] as const +const isLayoutDiagnostic = (message: string) => + /useLayoutEffect does nothing on the server/i.test(message) + +// Preparation independently decides ordinary admission and rows/status. +// Completed-query Suspense shares the ready judgment. Standalone builders and +// supplied Collections check only return/diagnostics, without a transport claim. +// Keeping raw messages permits unrelated diagnostics and exposes exact warnings. +function checkRender( + history: SsrSnapshotHistory, + actual: Observation, + path: string, +) { + const expected = expectedSsrSnapshot(history) + try { + atOracleCheckpoint(`SSR path reached`, path, () => { + expect( + actual.calls, + `real server renderer invoked the hook`, + ).toBeGreaterThan(0) + }) + const descriptor = path.includes(`provider`) || path.includes(`explicit`) + atOracleCheckpoint(`SSR preparation admission`, path, () => { + if (descriptor && expected.rejects) { + expect(actual.failure, `untransported server view rejects`).toMatch( + /SSR.*(?:preload|hydrat)/i, + ) + } else { + expect(actual.failure, `legal SSR render`).toBeUndefined() + expect(actual.view, `hook returned its public view`).toBeDefined() + if (path.startsWith(`suspense`)) + expect(actual.view?.rows, `prepared Suspense rows`).toEqual( + expected.rows, + ) + else if (descriptor) + expect(actual.view, `server public view`).toEqual({ + rows: expected.rows, + status: expected.status, + }) + } + }) + if (!descriptor || !expected.rejects) + atOracleCheckpoint(`SSR client-layout diagnostic`, path, () => { + expect( + actual.diagnostics.filter(isLayoutDiagnostic), + `legal SSR emits no client-layout diagnostic: ${JSON.stringify(actual.diagnostics)}`, + ).toEqual([]) + }) + } catch (error) { + if (actual.cleanupFailures.length) + throw new AggregateError( + [error, ...actual.cleanupFailures.map((failure) => new Error(failure))], + `SSR comparison and cleanup failed`, + { cause: error }, + ) + throw error + } + atOracleCheckpoint(`SSR cleanup`, path, () => { + expect(actual.cleanupFailures, `server cleanup diagnostics`).toEqual([]) + }) +} + +declareOracle({ check: [checkRender], grammar: [histories] }) +for (const runtime of runtimes) { + for (const path of paths) { + const project = (history: SsrSnapshotHistory): SsrSnapshotHistory => + path.startsWith(`suspense`) + ? { ...history, preparation: `query`, sourceState: `ready` } + : path === `standalone` || path === `supplied` + ? { ...history, preparation: `none`, sourceState: `ready` } + : history + // Disabled has one legal input. Its bounded replay shares the checker and + // driver; other paths generate only the axes that can affect their render. + const grammar = histories.map(project) + const inputs = examples.map((history) => [project(history)]) + const property = `react.ssr-effects-${runtime.name}-${path}` + for (const campaign of [`fixed`, `random`] as const) { + const register = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + register( + `${runtime.name} ${path} renders without client layout diagnostics (${campaign})`, + async () => { + const states = path.startsWith(`ordinary`) + ? [ + `rendered`, + `cold`, + `prepared-ready`, + `prepared-loading`, + `rejected`, + ] + : [`rendered`] + const reach = createOracleReach( + `ssr-effects-${runtime.name}-${path}`, + states, + { fixedHistories: inputs.length }, + ) + const fidelity = createOracleFailureFidelity() + let index = 0 + try { + await fidelity.assert(() => + fc.assert( + fc.asyncProperty(grammar, (history) => + fidelity.run(history, () => { + reach.beginHistory({ fixed: index++ < inputs.length }) + const actual: Observation = JSON.parse( + execFileSync( + process.execPath, + [ + programs.get(runtime.name)!, + JSON.stringify({ history, path }), + ], + { encoding: `utf8`, timeout: 10000 }, + ), + ) + atOracleCheckpoint(`matched SSR runtime`, path, () => { + expect(actual.reactVersion).toBe(runtime.version) + expect(actual.rendererVersion).toBe(runtime.version) + expect(actual.calls).toBeGreaterThan(0) + }) + reach.reach(`rendered`) + if (path.startsWith(`ordinary`)) { + const expected = expectedSsrSnapshot(history) + const state = expected.rejects + ? `rejected` + : history.sourceState === `idle` + ? `cold` + : expected.status === `ready` + ? `prepared-ready` + : history.preparation === `source` + ? `prepared-loading` + : undefined + if (state) reach.reach(state) + } + checkRender(history, actual, path) + }), + ), + campaign === `fixed` + ? { + numRuns: oracleRuns(24) + inputs.length, + seed: 2291, + examples: inputs, + } + : oraclePropertyOptions(24, property, { examples: inputs }), + ), + ) + reach.assertReached() + } finally { + console.info( + `ssr-effects ${runtime.name} ${path} ${campaign}`, + Object.fromEntries(reach.counts()), + ) + } + }, + 30000, + ) + } + } +} + +// Disabled SSR has one input. Replay it through the same diagnostic judgment, +// without presenting repeated constant inputs as a generated history campaign. +for (const runtime of runtimes) { + oracleReplayTest(it)(`${runtime.name} disabled SSR diagnostic replay`, () => { + const history: SsrSnapshotHistory = { + preparation: `none`, + sourceState: `idle`, + rows: [], + } + const actual: Observation = JSON.parse( + execFileSync( + process.execPath, + [ + programs.get(runtime.name)!, + JSON.stringify({ history, path: `disabled` }), + ], + { encoding: `utf8`, timeout: 10000 }, + ), + ) + expect(actual.reactVersion).toBe(runtime.version) + expect(actual.rendererVersion).toBe(runtime.version) + checkRender(history, actual, `disabled`) + }) +} + +oracleReplayTest(it)( + `distinguishes a client-layout diagnostic from unrelated messages`, + () => { + const history: SsrSnapshotHistory = { + preparation: `none`, + sourceState: `idle`, + rows: [], + } + const expected = expectedSsrSnapshot(history) + const actual: Observation = { + reactVersion: `calibration`, + rendererVersion: `calibration`, + calls: 1, + view: { rows: expected.rows, status: expected.status }, + diagnostics: [`unrelated diagnostic`], + cleanupFailures: [], + } + expect(() => + checkRender(history, actual, `ordinary-provider`), + ).not.toThrow() + expect(() => + checkRender( + history, + { + ...actual, + diagnostics: [ + `Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded.`, + ], + }, + `ordinary-provider`, + ), + ).toThrow(/SSR client-layout diagnostic/) + }, +) diff --git a/packages/react-db/tests/window-render-oracle.test.tsx b/packages/react-db/tests/window-render-oracle.test.tsx new file mode 100644 index 0000000000..75d658d388 --- /dev/null +++ b/packages/react-db/tests/window-render-oracle.test.tsx @@ -0,0 +1,295 @@ +/** + * A mounted window moves at commit. React can abandon a render after the hook + * returns, so that render cannot mutate the committed Collection or acquire + * provider rows. This receives the shared window law at React's pre-commit cut. + * 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. + * 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 { describe, expect, it } from 'vitest' +import fc from 'fast-check' +import { + BTreeIndex, + createCollection, + localOnlyCollectionOptions, +} from '@tanstack/db' +import { useLiveQuery } from '../src/useLiveQuery' +import { makeInfiniteOnDemandSource } from '../../db/tests/conformance/infinite-on-demand' +import { ScenarioLifetime } from '../../db/tests/conformance/scenario-lifetime' +import { + checkWindowObservation, + windowRows, +} from '../../db/tests/conformance/window-history-oracle' +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 { InitialQueryBuilder } from '@tanstack/db' +import type { + WindowInput, + WindowRow, +} from '../../db/tests/conformance/window-history-oracle' + +const renderHistoryArbitrary = fc.record({ + abandoned: fc.integer({ min: 1, max: 4 }), + committed: fc.integer({ min: 5, max: 8 }), + limit: fc.integer({ min: 1, max: 3 }), + abandonedLimit: fc.integer({ min: 4, max: 6 }), + committedLimit: fc.integer({ min: 7, max: 9 }), +}) +const examples = [ + [ + { + abandoned: 2, + committed: 5, + limit: 2, + abandonedLimit: 4, + committedLimit: 7, + }, + ], +] as const +const states = [`suspended move`, `cancelled move`, `committed move`] as const +type RenderHistory = { + abandoned: number + committed: number + limit: number + abandonedLimit: number + committedLimit: number +} +type PageWindow = { offset: number; limit: number } +type Move = `offset` | `limit` | `both` + +async function checkRenderHistory( + history: RenderHistory, + remote: boolean, + keyed: boolean, + move: Move, + reach: ReturnType>, +) { + const lifetime = new ScenarioLifetime() + let unmount: (() => void) | undefined + lifetime.defer(() => unmount?.()) + await lifetime.run(async () => { + const rows: Array = Array.from({ length: 24 }, (_, id) => ({ + id: String(id).padStart(2, `0`), + rank: id, + })) + const provider = remote + ? makeInfiniteOnDemandSource({ createCollection, BTreeIndex }, rows) + : undefined + const source = + provider?.collection ?? + createCollection( + localOnlyCollectionOptions({ + getKey: (row: WindowRow) => row.id, + initialData: rows, + autoIndex: `eager`, + defaultIndexType: BTreeIndex, + }), + ) + lifetime.defer(() => source.cleanup({ dependents: true })) + const initial = { offset: 0, limit: history.limit } + const abandoned = { + offset: move === `limit` ? 0 : history.abandoned, + limit: move === `offset` ? history.limit : history.abandonedLimit, + } + const accepted = { + offset: move === `limit` ? 0 : history.committed, + limit: move === `offset` ? history.limit : history.committedLimit, + } + const input = (window: PageWindow): WindowInput => ({ + ...window, + minimum: -1, + nestedLimit: rows.length, + descending: true, + }) + const never = new Promise(() => {}) + let setPage: (value: PageWindow & { suspend: boolean }) => void = () => { + throw new Error(`Not mounted`) + } + let suspended = 0 + let committed: { rows: unknown; collection: unknown } | undefined + let readCollection: () => unknown = () => undefined + function Page() { + const [page, update] = useState({ ...initial, suspend: false }) + setPage = update + const query = (q: InitialQueryBuilder) => + q + .from({ row: source }) + .orderBy(({ row }) => row.rank, `desc`) + .limit(page.limit) + .offset(page.offset) + .select(({ row }) => ({ id: row.id, rank: row.rank })) + const result = useLiveQuery({ + query, + ...(keyed + ? { queryKey: [`window-render`, page.offset, page.limit] } + : {}), + }) + useLayoutEffect(() => { + committed = { rows: result.data, collection: result.collection } + readCollection = () => result.collection.toArray + }) + if (page.suspend) { + suspended++ + throw never + } + return null + } + const view = render( + + + , + ) + unmount = () => view.unmount() + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + const initialCollection = committed!.collection + const loads = provider?.calls.length ?? 0 + const failures: Array = [] + const capture = (check: () => void) => { + try { + check() + } catch (error) { + failures.push(error) + } + } + const compare = (window: PageWindow, cut: string, actual = committed!) => + checkWindowObservation( + actual, + { + rows: windowRows(rows, input(window), false), + previousCollection: initialCollection, + retain: true, + }, + cut, + ) + let interruption: { error: unknown } | undefined + try { + compare(initial, `initial`) + await act(async () => + startTransition(() => setPage({ ...abandoned, suspend: true })), + ) + atOracleCheckpoint(`render attempted`, `suspended`, () => + expect( + suspended, + `React attempted the suspended render`, + ).toBeGreaterThan(0), + ) + reach.reach(`suspended move`) + capture(() => + compare(initial, `suspended`, { + rows: readCollection(), + collection: initialCollection, + }), + ) + capture(() => + atOracleCheckpoint(`speculative acquisition`, `suspended`, () => + expect( + provider?.calls.length ?? 0, + `abandoned render acquires nothing`, + ).toBe(loads), + ), + ) + // Rival design: apply only the proposed width before commit. Supplying + // its rows to the same comparison must fail at this held render cut. + if (move !== `offset`) + expect(() => + compare(initial, `suspended width calibration`, { + rows: windowRows( + rows, + input({ ...initial, limit: abandoned.limit }), + false, + ), + collection: initialCollection, + }), + ).toThrow() + await act(async () => setPage({ ...initial, suspend: false })) + reach.reach(`cancelled move`) + capture(() => compare(initial, `cancelled`)) + await act(async () => setPage({ ...accepted, suspend: false })) + reach.reach(`committed move`) + capture(() => compare(accepted, `committed`)) + } catch (error) { + interruption = { error } + } + if (interruption) failures.push(interruption.error) + if (failures.length) + throw new AggregateError(failures, `React window commit mismatches`, { + cause: failures[0], + }) + }) +} + +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 campaign of [`fixed`, `fresh`] as const) { + const property = `react.window-render` + const test = + campaign === `fixed` + ? oracleReplayTest(it) + : oracleReplayTest(it, property) + test(`${remote ? `on-demand` : `eager`}/${keyed ? `keyed` : `derived`}/${move}/${campaign}`, async () => { + const reach = createOracleReach( + `${property}/${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, 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 ${remote}/${keyed}/${move}/${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], +}) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 92c69928c1..b4a9906b87 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -8,7 +8,6 @@ import { } from 'solid-js' import { ReactiveMap } from '@solid-primitives/map' import { - BaseQueryBuilder, createLiveQueryObserver, getLiveQueryStatusFlags, getPublicCollection, @@ -251,36 +250,27 @@ export function useLiveQuery< export function useLiveQuery( configOrQueryOrCollection: (queryFn?: any) => any, ) { + let previousCollection: ReturnType = null + const resolveNext = (value: unknown) => + (previousCollection = resolveLiveQueryValue(value, { + gcTime: DEFAULT_GC_TIME_MS, + previousCollection, + windowOwner: resolveNext, + })) const collection = createMemo( () => { - if (configOrQueryOrCollection.length === 1) { - // This is a query function - check if it returns null/undefined - const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder - const result = configOrQueryOrCollection(queryBuilder) - - if (result === undefined || result === null) { - // Disabled query - return null - return null - } - - return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS }) - } - - const innerCollection = configOrQueryOrCollection() - - if (innerCollection === undefined || innerCollection === null) { - // Disabled query - return null - return null - } + const innerCollection = + configOrQueryOrCollection.length === 1 + ? configOrQueryOrCollection + : configOrQueryOrCollection() if (isCollection(innerCollection)) { innerCollection.startSyncImmediate() + previousCollection = null return innerCollection as Collection } - return resolveLiveQueryValue(innerCollection, { - gcTime: DEFAULT_GC_TIME_MS, - }) + return resolveNext(innerCollection) }, undefined, { name: `TanstackDBCollectionMemo` }, diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index fe1ca718c9..5dced34236 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -335,7 +335,9 @@ export function useLiveQuery( // it unmounts. const sourceIdClaims = createLiveQuerySourceIdClaims() + let previousCollection: ReturnType = null const resolved = $derived.by(() => { + let commitWindow: (() => void) | undefined // First check if the original parameter might be a getter // by seeing if toValue returns something different than the original let unwrappedParam = configOrQueryOrCollection @@ -357,6 +359,7 @@ 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 @@ -384,6 +387,7 @@ export function useLiveQuery( `queryKey`, ), resumeDeferredCollections: () => {}, + move: undefined, } } @@ -453,9 +457,17 @@ export function useLiveQuery( collection = resolveLiveQueryValue(preparedValue, { gcTime: DEFAULT_GC_TIME_MS, pool: dbClient ? false : undefined, + previousCollection: dbClient ? undefined : previousCollection, + windowOwner: sourceIdClaims, + onWindowChange: (move) => { + commitWindow = move + }, }) } + 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. @@ -465,6 +477,7 @@ export function useLiveQuery( : undefined return { collection, + move: commitWindow, client: dbClient, queryHash, // The subscribing effect releases the starts, so it drops the @@ -532,9 +545,20 @@ export function useLiveQuery( }) } + // Source notifications write Svelte state. Apply moves outside the derived + // input and retain the observer when only its window changes. + $effect.pre(() => { + const next = resolved + untrack(() => next.move?.()) + }) + const observerInput = $derived.by(() => { + const next = resolved + return next.client ? next : next.collection + }) // Watch for collection changes and subscribe to updates $effect(() => { - const nextResolved = resolved + observerInput + const nextResolved = untrack(() => resolved) if (nextResolved !== currentResolved) { currentObserver.dispose() diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 4d06db8eaf..48875a0606 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -9,7 +9,6 @@ import { watchEffect, } from 'vue' import { - BaseQueryBuilder, createLiveQueryObserver, getLiveQueryStatusFlags, getPublicCollection, @@ -275,6 +274,13 @@ export function useLiveQuery( configOrQueryOrCollection: any, deps: Array> = [], ): UseLiveQueryReturn | UseLiveQueryReturnWithCollection { + let previousCollection: ReturnType = null + const resolveNext = (value: unknown) => + (previousCollection = resolveLiveQueryValue(value, { + gcTime: DEFAULT_GC_TIME_MS, + previousCollection, + windowOwner: resolveNext, + })) const resolveInput = () => { // First check if the original parameter might be a ref/getter // by seeing if toValue returns something different than the original @@ -315,24 +321,15 @@ export function useLiveQuery( if (unwrappedParam.status === `idle`) { unwrappedParam.startSyncImmediate() } + previousCollection = null return unwrappedParam } // Reference deps to make computed reactive to them deps.forEach((dep) => toValue(dep)) - // Ensure we always start sync for Vue hooks - if (typeof unwrappedParam === `function`) { - // A query function returning null or undefined disables the query. - return resolveLiveQueryValue( - unwrappedParam(new BaseQueryBuilder() as InitialQueryBuilder), - { gcTime: DEFAULT_GC_TIME_MS }, - ) - } else { - return resolveLiveQueryValue(unwrappedParam, { - gcTime: DEFAULT_GC_TIME_MS, - }) - } + // The shared resolver prepares query callbacks and disabled inputs. + return resolveNext(unwrappedParam) } // The input's live-query Collection, or the error resolving it threw. A // Vue computed that throws serves its previous value on the next read, so diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 386af2630a..4aa5e91b41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1624,6 +1624,12 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + react-dom18: + specifier: npm:react-dom@18.3.1 + version: react-dom@18.3.1(react@19.2.4) + react18: + specifier: npm:react@18.3.1 + version: react@18.3.1 packages/react-native-db-sqlite-persistence: dependencies: @@ -11398,6 +11404,11 @@ packages: react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -11479,6 +11490,10 @@ packages: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + react@19.0.0: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} @@ -11742,6 +11757,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} @@ -24150,6 +24168,12 @@ snapshots: - bufferutil - utf-8-validate + react-dom@18.3.1(react@19.2.4): + dependencies: + loose-envify: 1.4.0 + react: 19.2.4 + scheduler: 0.23.2 + react-dom@19.2.4(react@19.2.0): dependencies: react: 19.2.0 @@ -24446,6 +24470,10 @@ snapshots: react-refresh@0.18.0: {} + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + react@19.0.0: {} react@19.2.0: {} @@ -24818,6 +24846,10 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + scheduler@0.25.0: {} scheduler@0.27.0: {}