This change introduces the observation term first paint. A first paint is the first value a framework binding publishes to its consumer after mount. The term is not in the project glossary yet. Each driver records the first paint through its own native commit hook, because the frameworks reach it through different cuts:
The term describes a cross-framework observation. It does not combine or split a production concept.
| Requirement | Result |
|---|---|
| ORC-001 | Pass. The law: for a synchronously loaded source, useLiveInfiniteQuery shows the ready first page on the first paint, with no empty idle paint before data. Limit: synchronous eager sources and the query-callback input form. The recorder observes a framework commit value, not a browser paint. |
| ORC-002 | Pass. The expected first page ['1','2','3'] comes from the source rows and the orderBy(rank desc) plus pageSize arguments, independent of the hook output. The parity test adds useLiveQuery as a documented prior-behavior reference. Neither reads the controller snapshot logic to compute the expectation. |
| ORC-003 | Pass. The scenario prose states the law and why current() cannot observe it. The contract comment states the first-paint obligation and the per-framework cut. The driver records the real hook; the checkpoint reads firstPaint() after flush. |
| ORC-004 | Not applicable. The trigger is a generated property that claims history coverage. This is a fixed example scenario over one synchronous source. It makes no generated-history coverage claim. |
| ORC-005 | Pass. Each driver mounts the real framework hook and records the real first paint through the framework commit hook, then flushes to ready. The checkpoint is the first recorded paint. |
| ORC-006 | Pass. Mutant calibration: reverting React or Svelte to startSync: false makes [first-paint-ready] fail at the first-paint checkpoint with { status: 'idle', ids: [] }. This is an assertion failure at the intended checkpoint, not a setup error. The fixed startSync: true passes. |
| ORC-007 | Not applicable. The trigger is an important generated property. This scenario has no random campaign. Direct replay is running the named [first-paint-ready] scenario in each driver package. |
| ORC-008 | Not applicable. The change adds no production state and no stateful model. firstPaint() is a single test-only observation; it does not introduce, remove, combine, or split a modeled state. |
| ORC-009 | Pass. The one new term is "first paint", declared above with its per-framework mapping. No production concept is combined or split. |
| ORC-010 | Pass. The React recorder keeps the first observed commit and throws when no commit was observed, so it cannot silently report an empty paint. Cleanup uses the suite's existing track() and lifetime teardown; it does not replace the assertion. |
| ORC-011 | Pass. The parity test is an independent second formulation: it asserts that useLiveQuery and useLiveInfiniteQuery agree on the first commit for the same synchronous source. No further distinct semantic classifier was identified. |
| ORC-012 | Pass. This versioned record accounts for ORC-001 through ORC-014 at the reviewed semantic commit above. The pull-request description alone did not satisfy this requirement. |
| ORC-013 | Pass. The scenario protects a reusable cross-framework boundary law. The distinguishing witness is the mutant: startSync: false fails for React and Svelte at the first-paint checkpoint, while the fix passes. Vue's distinct sync-subscribe cut stays green under both, which the law allows. |
| ORC-014 | Not applicable. No controlled provider or host supplies a premise. The source is a standard synchronous mock collection shared with the rest of the suite. |
The claim is bounded, not universal.
Unresolved in-scope cells remain open under the owner above:
Out-of-scope cells, where an empty or loading first paint is correct:
A follow-up review found a regression from startSync: true: because the hooks built a new collection per render and only recorded it at commit, a duplicate pre-commit render (React StrictMode, a discarded concurrent render, or a Suspense retry) started a second collection and loaded an on-demand source's first page twice. useLiveQuery avoids this with a render-time instance memo (its pool excludes window and on-demand queries, so the pool is not the cause).
Fix: React now records each render's state in a render-time ref and reuses it when every identity input matches, while committed state is still recorded at subscribe so an abandoned render cannot overwrite preserved pages. Svelte's $derived controller now reuses the previous controller when nothing that defines it changed, instead of rebuilding on every recompute.
Regression: packages/react-db/tests/infinite-query-strictmode-dedup.test.tsx mounts an on-demand source under StrictMode and asserts the first peek-ahead window loads once. It fails (two loads) when the memo is recorded only at commit, and passes with the render-time reuse. This closes the StrictMode part of the previously unresolved cell. The pre-created-collection input form and Suspense or concurrent first paint remain open under the owner above.
A later review found that the React reuse could bind a collection that GC had already cleaned up. A committed component keeps its refs across a suspended update, so the update's uncommitted collection survived to the retry, and the retry committed an empty cleaned-up page before recovering. The reuse now requires a collection that is not cleaned-up. packages/react-db/tests/infinite-query-stale-reuse.test.tsx covers both a fresh-mount retry and a mounted suspended update after GC. The mounted-update case fails without the liveness check and passes with it.
A further review found that the two reuse mechanisms above caused new defects. In Svelte, the reuse guard returned the previous controller when the deps getters were unchanged, so the hook ignored reactive state read directly inside the query callback. In React, renderedRef could keep a stale page count. It did not dedupe React 18 StrictMode renders, and it duplicated the identity rules.
Both mechanisms are removed. The duplicate page requests came only from on-demand sources, which load asynchronously and gain nothing from an early start. canStartLiveQueryWindowSyncInRender now starts sync during render only when no source collection is on-demand. Eager sources keep the ready first paint. On-demand queries start when the subscription commits.
Evidence at this change:
Known limit: an eager collection started during render has only the 50 ms unsubscribed GC floor before its commit. If a commit arrives later than that, GC may clean the collection up first. useLiveQuery shares this exposure. No test reaches this case.
A review of 725e37a28ac81e3d5041d990eb4abeebedc60399 reproduced two regressions from starting sync in render:
Evidence, produced on the working tree above that commit:
Svelte's retained-page window was fixed by the same rule, but no test observes an intermediate Svelte value. The coverage map lists that cell.
This entry moves the laws found in the earlier reviews into the shared infinite-query suite where they are framework-independent, and leaves only pre-commit work to the drivers.
| Law | Owner |
|---|---|
| A synchronous source's first published value is ready with the first page. | Shared first-paint-ready |
| Every ready value in a fixed-source, fixed-query scenario equals the source prefix for its own page count, with matching pages and continuation. | Shared, in 11 scenarios including equal-dependency-depth and circular-dependency |
| A mount requests an on-demand first window once. | Shared on-demand-paging |
| A render that never commits, or a superseded pre-commit recompute, does not acquire an on-demand source, direct or wrapped; commit does. Retired by the addendum "On-demand sources follow useLiveQuery"; replaced by acquisition parity with useLiveQuery. | React infinite-query-render-cuts-oracle.test.tsx, Svelte hook tests |
| A StrictMode double render requests an on-demand first window once. Narrowed by the same addendum to React 19, where both hooks keep refs across the double render. | React render cuts |
| After GC reclaims an abandoned render's collection, the retry's first commit is the ready first page. | React render cuts |
Each shared handle now records every value its framework published, which replaces the single first-paint recorder. Pre-commit work stays in drivers, because Vue's setup is its commit.
Evidence, produced on the working tree above 0f0e05cbb:
A probe found one more open cell. A supplied collection that has not started publishes an idle first value in React and Svelte, and a ready one in Vue. useLiveQuery starts a supplied collection in render. The infinite hook does not, and a React test requires that an abandoned render leave a supplied collection unsubscribed. Closing this cell changes that contract, so it is recorded for decision rather than changed here.
Decision: the maintainer chose to make useLiveInfiniteQuery behave like useLiveQuery for a supplied collection, and to reverse the rule from #1675 that no supplied collection starts before commit. That rule came from a #1675 review, which asked that collection construction be inert so that a render that never commits leaves no resources behind. This PR keeps the purpose of that rule with two safeguards: GC reclaims an abandoned start, and on-demand sources still wait for commit. The contract test does not activate a supplied collection for an abandoned render is replaced by reclaims a supplied collection that an abandoned render started.
A supplied collection starts during render only when no source behind it is on-demand and its window already holds the requested rows from offset 0. A shifted or narrower window waits for the controller to adjust it at commit. Both hooks now validate a query or collection before starting it, so a rejected .findOne() query never reads its source.
Evidence, produced on the working tree above 083247d18:
Still open: an on-demand source that loads synchronously publishes ready first in useLiveQuery and idle first in useLiveInfiniteQuery, because the infinite hook defers on-demand sources to commit. The two hooks differ there until that choice is made. With a DbClient, a source that is first created during render waits for commit in both hooks, by the deferral contract from #1564.
Decision: the maintainer chose that useLiveInfiniteQuery follow useLiveQuery for on-demand sources too, unless that is plainly wrong. The two rules conflict for an on-demand source. Ready on the first paint needs a request during render, and no request before commit forbids one. useLiveQuery starts on-demand sources in render, so the infinite hook now does too. The start gate is removed, and the law that a render that never commits sends no on-demand request is retired. A parity law replaces it: under the same history, the infinite hook acquires an on-demand source as often as useLiveQuery does, and commits the same first value.
Two parts of useLiveQuery were matched in substance, not copied:
Evidence, produced on the working tree above e6a31f6eb:
Remaining exposure, shared with useLiveQuery by this decision: a render that never commits, including a Suspense render that throws, sends on-demand requests. On React 18, StrictMode does not keep refs across the double render, so both hooks send two first-window requests there. Removing early on-demand requests from both hooks needs a change in the live-query Collection itself.
An external review of c3341af0f raised ten findings. This entry records the behavioral ones.
Law: a mount requests no on-demand rows beyond the window the hook needs, for either input form. Authority: established behavior on main, where a supplied collection waits for the controller to set its window at commit. The shared on-demand-paging scenario covered only query callbacks, and the supplied scenarios used an exact .limit(4) window, so a wider window was never generated. The new shared scenario on-demand-collection-window declares a wider .limit(10) and an exact .limit(4), and checks the rows the on-demand source holds after mount, which are the rows it was asked for.
A second external review of 16a34e061 reproduced two React regressions in the render-time collection cache, both against useLiveQuery as the reference.
A third review of f490f3d4d found that the cache also reused a collection that entered terminal error after it was cached, for example when its source restarted after cleanup, so selecting its query again showed stale rows with status: error. useLiveQuery builds a fresh collection there. The cache now rejects a collection in terminal error. The render-cut oracle compares both hooks on that history and checks the restarted source's rows; dropping the error check fails the infinite case. A synchronous load failure still throws from render in both hooks, and an asynchronous one settles after commit, so neither path reaches the cache.
The deferred-acquisition follow-up must revisit the new first-value assertion for on-demand-paging: before a subscriber, its ordered on-demand window is unpublished, so that law and this one conflict and need a decision there.
Each entry's evidence applies to the revision named here. The first section's table and closure apply to 415a8d4a1.
| Entry | Revision |
|---|---|
| Record and requirement outcomes | 415a8d4a1 (record added in 1c74a335d) |
| Duplicate pre-commit render fix | a4af6a251 |
| Cleaned-up reuse liveness check | fc5f136a5 |
| Start-sync gate replaces render-time reuse | b31b84ff9 |
| Nested on-demand sources and retained-page windows | 2b461f8cd |
| Publication laws in the shared oracle | 083247d18 |
| Supplied collections match useLiveQuery | e6a31f6eb |
| On-demand sources follow useLiveQuery | c3341af0f |
| Supplied windows request only what the hook needs | ff7549054 |
| Render-time reuse follows useLiveQuery's identity and error laws | the commit that adds this entry, on top of 16a34e061 |