diff --git a/.changeset/lane-mount-binding-reads-screen.md b/.changeset/lane-mount-binding-reads-screen.md new file mode 100644 index 000000000..6f162511e --- /dev/null +++ b/.changeset/lane-mount-binding-reads-screen.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +An element an optimistic guess mounts now lands together with its bindings (#3835). A first pass created by a guess's lane reads as the lane's, not just routes as it: a binding the lane mounts reads a write the action still holds as the screen (its committed value) and re-derives when the action lands. Before, a binding inside a memo the lane created was born held while the memo's element showed, so `
Hello
` appeared without its `style` until the action finished; directly under the lane pass the binding showed the held value early instead. diff --git a/.changeset/lane-mount-boundary-over-flight.md b/.changeset/lane-mount-boundary-over-flight.md new file mode 100644 index 000000000..f870b9176 --- /dev/null +++ b/.changeset/lane-mount-boundary-over-flight.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +A `Loading` mounted by an optimistic write over a flight shows its fallback again (#3835's seat regressed it). Lane work with no committed value yet reads a pending flight the way a mainline mount's memo does: it enters and throws, so the boundary it mounts catches the pending (A29's boundary exemption), instead of being served the flight's committed value. A render effect is still a stale reader of a held flight (rule 3). Approach and tests from #3843 by @brenelz. diff --git a/.changeset/verdict-mount-first-pass-mainline.md b/.changeset/verdict-mount-first-pass-mainline.md new file mode 100644 index 000000000..f408e56bb --- /dev/null +++ b/.changeset/verdict-mount-first-pass-mainline.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +A mount made by a verdict reader stays mainline (#3851). Inside an action, ` 0}>` opens at once, but a `` it mounts used to show `content 1` beside the committed `x = 0`: the content's first pass was staged in the verdict lane its creator was in, and in the action's own flush it read the staged `x` before the seam held it. A first pass now takes its creator's lane only for an optimistic lane (head and tail of its pass alike); a mount a verdict lane makes whose first pass reads a staging of a flush with a transaction is born held like one that read a held node (other new memos do not join the transaction by being created); and verdict-lane work that read the frame's stagings re-derives on the committed world when the frame stays parked. The boundary shows its fallback and `content 1` at the landing. diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index f054c3a7c..6fcc434c6 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -45,65 +45,65 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:313` | — | onCleanup.test.ts×2 transitionEntanglement.test.ts×4 | [ruled 2026-07-06] Effect error interception is compute-phase only — `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none… | -| A2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:321` | — | onCleanup.test.ts×2 | [ruled] Unhandled compute-phase errors in user effects are logged and skipped — Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. | -| A3 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:329` | — | derived-write-then-derivation-3733.test.ts×5 equals-comparator-errors.test.ts×1 | [ruled] Comparator throws are compute-phase errors — Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). | -| A4 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:337` | — | derived-write-then-derivation-3733.test.ts×1 equals-comparator-errors.test.ts×1 | [ruled] A custom `equals` never sees `undefined` prev on first commit — A custom `equals` is never invoked with `undefined` previous value on a node's first commit. | -| A5 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:345` | — | derived-write-then-derivation-3733.test.ts×1 errorHalt.test.ts×1 | [ruled] An error escaping every boundary halts the system — An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". | -| A6 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:353` | boundaries.ts×2 | derived-write-then-derivation-3733.test.ts×1 enforceLoadingBoundary.test.ts×1 | [ruled] `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is a warn-only diagnostic; an `Errored` above must not swallow it and must not show its fallback for a pending. | -| A7 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:159` | — | fuzz-findings-l2.test.ts×1 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled, amended in place] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. **Amende… | -| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:167` | — | createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… | -| A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:175` | projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… | -| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:183` | core.ts×3 verdict.ts×2 | createMemo.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 verdict-contract.test.ts×2 | [ruled] `[isPending(x), x()]` is atomic within one scope — `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. | +| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:315` | — | onCleanup.test.ts×2 transitionEntanglement.test.ts×4 | [ruled 2026-07-06] Effect error interception is compute-phase only — `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none… | +| A2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:323` | — | onCleanup.test.ts×2 | [ruled] Unhandled compute-phase errors in user effects are logged and skipped — Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. | +| A3 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:331` | — | derived-write-then-derivation-3733.test.ts×5 equals-comparator-errors.test.ts×1 | [ruled] Comparator throws are compute-phase errors — Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). | +| A4 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:339` | — | derived-write-then-derivation-3733.test.ts×1 equals-comparator-errors.test.ts×1 | [ruled] A custom `equals` never sees `undefined` prev on first commit — A custom `equals` is never invoked with `undefined` previous value on a node's first commit. | +| A5 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:347` | — | derived-write-then-derivation-3733.test.ts×1 errorHalt.test.ts×1 | [ruled] An error escaping every boundary halts the system — An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". | +| A6 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:355` | boundaries.ts×2 | derived-write-then-derivation-3733.test.ts×1 enforceLoadingBoundary.test.ts×1 | [ruled] `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is a warn-only diagnostic; an `Errored` above must not swallow it and must not show its fallback for a pending. | +| A7 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:161` | — | fuzz-findings-l2.test.ts×1 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled, amended in place] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. **Amende… | +| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:169` | — | createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… | +| A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:177` | projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… | +| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:185` | core.ts×3 verdict.ts×2 | createMemo.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 verdict-contract.test.ts×2 | [ruled] `[isPending(x), x()]` is atomic within one scope — `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. | | A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:101` | — | latest-isPending-consistency.test.ts×1 loading-fallback-in-flush-3540.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… | -| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:191` | — | createOptimistic.test.ts×2 spec-async-semantics.test.ts×1 | [ruled, amended in place] Resting optimistic nodes report pending like a plain memo — A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optim… | -| A13 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:199` | — | spec-async-semantics.test.ts×7 | [ruled 2026-07-06 (promoted from B1)] Resting optimistic ≡ plain async memo at every checkpoint — (was B1) A resting optimistic node (no active override) is observationally identical to a plain async … | -| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:207` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… | -| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:243` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×4 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … | -| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:215` | scheduler.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… | +| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:193` | — | createOptimistic.test.ts×2 spec-async-semantics.test.ts×1 | [ruled, amended in place] Resting optimistic nodes report pending like a plain memo — A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optim… | +| A13 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:201` | — | spec-async-semantics.test.ts×7 | [ruled 2026-07-06 (promoted from B1)] Resting optimistic ≡ plain async memo at every checkpoint — (was B1) A resting optimistic node (no active override) is observationally identical to a plain async … | +| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:209` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… | +| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:245` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×4 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … | +| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:217` | scheduler.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… | | A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:69` | async.ts×3 constants.ts×1 lanes.ts×6 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… | | A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:79` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×5 flight-owned-transaction.test.ts×1 lane-authority-twins.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×19 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… | -| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:143` | async.ts×1 constants.ts×1 core.ts×6 scheduler.ts×2 types.ts×1 verdict.ts×4 store.ts×1 | fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… | -| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:402` | — | question-scoped-pending.test.ts×2 spec-async-semantics.test.ts×3 createOptimisticStore.test.ts×1 | [superseded 2026-07-13 by A24] (superseded) Optimistic writes announce a store-wide pending — (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the… | -| A21 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:409` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | [superseded 2026-07-13 by A24] (superseded) The store-wide mask — (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective… | -| A22 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:223` | store.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled, amended in place 2026-07-08] Pending is per-node; store-wide only for the firewall's own work — **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) … | -| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:233` | — | spec-async-semantics.test.ts×1 | [ruled 2026-07-08] The `isPending` probe is reads-only — **The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `fals… | -| A24 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:151` | affects.ts×2 constants.ts×1 lanes.ts×2 types.ts×1 verdict.ts×3 affects.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×3 kanban-a17-fixture.test.ts×1 signal-store-twins-qd.test.ts×1 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled 2026-07-13] Question-scoped pending: pending iff a value change is in flight or an `affects()` mark is live — (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#272… | -| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:303` | projection.ts×1 | derived-presence-async-3726.test.ts×2 uninitialized-visibility.test.ts×3 visibility-oracle-store.states.ts×7 visibility-oracle-store.test.ts×1 | [ruled 2026-07-16] A derived store's seed is a draft, never an observable value — (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for th… | +| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:145` | async.ts×1 constants.ts×1 core.ts×6 scheduler.ts×2 types.ts×1 verdict.ts×4 store.ts×1 | fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… | +| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:404` | — | question-scoped-pending.test.ts×2 spec-async-semantics.test.ts×3 createOptimisticStore.test.ts×1 | [superseded 2026-07-13 by A24] (superseded) Optimistic writes announce a store-wide pending — (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the… | +| A21 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:411` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | [superseded 2026-07-13 by A24] (superseded) The store-wide mask — (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective… | +| A22 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:225` | store.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled, amended in place 2026-07-08] Pending is per-node; store-wide only for the firewall's own work — **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) … | +| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:235` | — | spec-async-semantics.test.ts×1 | [ruled 2026-07-08] The `isPending` probe is reads-only — **The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `fals… | +| A24 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:153` | affects.ts×2 constants.ts×1 lanes.ts×2 types.ts×1 verdict.ts×3 affects.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×3 kanban-a17-fixture.test.ts×1 signal-store-twins-qd.test.ts×1 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled 2026-07-13] Question-scoped pending: pending iff a value change is in flight or an `affects()` mark is live — (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#272… | +| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:305` | projection.ts×1 | derived-presence-async-3726.test.ts×2 uninitialized-visibility.test.ts×3 visibility-oracle-store.states.ts×7 visibility-oracle-store.test.ts×1 | [ruled 2026-07-16] A derived store's seed is a draft, never an observable value — (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for th… | | A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:109` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… | -| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:295` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… | +| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:297` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… | | A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:93` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×2 store.ts×4 | createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 latest-held-till-flush.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… | -| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:117` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×10 effect.ts×1 lanes.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×6 loading-on-frame-following-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … | -| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:251` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… | -| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:125` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … | -| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:133` | core.ts×4 lanes.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… | -| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:267` | boundaries.ts×1 scheduler.ts×2 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 fuzz-findings-l2.test.ts×4 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… | -| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:277` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×4 | a34-writes-then-derivations.test.ts×2 createMemo.test.ts×1 derived-write-then-derivation-3733.test.ts×1 finalize-reentry.test.ts×2 fuzz-findings-l2.test.ts×5 held-derivation-not-a-proposal-3612.test.ts×6 optimistic-list-mutation-matrix.test.ts×1 unchanged-presence-no-hold-3743.test.ts×3 woken-transaction-adopts-staged-bump.test.ts×1 transition-corpse-revival.test.ts×1 treeshake.test.ts×2 visibility-oracle.states.ts×2 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. **Held, both are suggestion… | +| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:117` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×12 effect.ts×1 lanes.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×6 loading-on-frame-following-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-mount-nested-memo-3835.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 verdict-mount-first-pass-3851.test.ts×2 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … | +| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:253` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… | +| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:127` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … | +| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:135` | core.ts×4 lanes.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… | +| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:269` | boundaries.ts×1 scheduler.ts×2 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 fuzz-findings-l2.test.ts×4 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… | +| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:279` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×4 | a34-writes-then-derivations.test.ts×2 createMemo.test.ts×1 derived-write-then-derivation-3733.test.ts×1 finalize-reentry.test.ts×2 fuzz-findings-l2.test.ts×5 held-derivation-not-a-proposal-3612.test.ts×6 optimistic-list-mutation-matrix.test.ts×1 unchanged-presence-no-hold-3743.test.ts×3 woken-transaction-adopts-staged-bump.test.ts×1 transition-corpse-revival.test.ts×1 treeshake.test.ts×2 visibility-oracle.states.ts×2 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. **Held, both are suggestion… | ## V — fixed violations | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:472` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | -| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:482` | — | spec-async-semantics.test.ts×2 | - **V2 (violated A7/A13) — FIXED.** `latest()`'s verdict in the window was | -| V3 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:488` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | -| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:495` | — | spec-async-semantics.test.ts×5 | - **V4 (violated the old A20's three-form algebra) — FIXED, then the rule it | -| V5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:507` | scheduler.ts×1 | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, | +| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:474` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | +| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:484` | — | spec-async-semantics.test.ts×2 | - **V2 (violated A7/A13) — FIXED.** `latest()`'s verdict in the window was | +| V3 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:490` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:497` | — | spec-async-semantics.test.ts×5 | - **V4 (violated the old A20's three-form algebra) — FIXED, then the rule it | +| V5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:509` | scheduler.ts×1 | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, | ## B — tier B | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:199` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's section carries the ruling). | -| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:207` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's section carries the ruling). | -| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:243` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | +| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:201` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's section carries the ruling). | +| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:209` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's section carries the ruling). | +| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:245` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | | B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:79` | — | derived-write-then-derivation-3733.test.ts×1 spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). | -| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:215` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×3 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | +| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:217` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×3 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | ## C — tier C | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:143` | — | onCleanup.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A19 (A19's section carries the ruling). | -| C2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:438` | — | onCleanup.test.ts×2 | - [x] **C2 — RULED (2026-07-07): reverts do not trump other live lanes.** A | -| C3 | closed | `docs/SPEC-ASYNC-SEMANTICS.md:448` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | +| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:145` | — | onCleanup.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A19 (A19's section carries the ruling). | +| C2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:440` | — | onCleanup.test.ts×2 | - [x] **C2 — RULED (2026-07-07): reverts do not trump other live lanes.** A | +| C3 | closed | `docs/SPEC-ASYNC-SEMANTICS.md:450` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | | C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:69` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). | ## INV — invariants diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index 60dd7cd7a..a73a4f0a4 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -34,7 +34,7 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t 1. **One frame concept.** A node is committed or staged for this flush; a flush that goes async _parks_ — its stagings are held by a transaction, its passes' children keep computing and die with the commit that replaces them (ruling A, O2: a held pass's children compute but do not effect). There is no store-side or lane-side transition state: a transaction holds **nodes**; a store's container, leaf and presence nodes are nodes (A22 per key, A29 per pass). 2. **Reading the future joins it** (A29 generalized). A pass served a held node's staging derives from that transaction and is its work — tracked or untracked (`untrack` is about dependencies, not about which world a pass derives from). **Frame readers** — a render effect, a verdict reader (`isPending`/`latest`), lane work — see the screen: the committed value, and are re-derived at the holder's landing (A15's stale reader). A children-forbidden reader sees the frame, never the graph (A32). -3. **A staging read is the frame's** (REACTIVE_STAGED_READ). A pass that read this flush's staging as the screen re-derives on the committed world if the flush parks; a verdict read cannot break it out. A verdict reader whose probe was answered before the flush had a transaction runs again at the seam even if a later read routed it into the verdict lane — that was not the probe's answer. +3. **A staging read is the frame's** (REACTIVE_STAGED_READ). A pass that read this flush's staging as the screen re-derives on the committed world if the flush parks; a verdict read cannot break it out. A verdict reader whose probe was answered before the flush had a transaction runs again at the seam even if a later read routed it into the verdict lane — that was not the probe's answer. **Amended 2026-10-07 (#3851, maintainer ruling):** verdict-lane work that is not itself a verdict reader is a staging reader like guess-lane work — when the transaction stays parked it re-runs against the committed values (it no longer reads the frame's proposal); a transaction landing at the same seam makes what it read the screen, and it does not run twice. A verdict reader still answers for itself and re-derives at the holder's landing (#3322, #3540). 4. **Lanes** (A17/A18 on L2). A lane is a new base of a transition, made by a guess; it sees the screen plus its own guesses, breaks out of the parent's hold (its effects run now), and holds itself while a derivation of a guess is in flight. **Lane contract 2:** a guess written with no parent transaction is void. A correction dissolves the lane — the guess and what derived from it are void, the truth is the parent's held write, re-derived there; a corrected lane that had shown keeps its runs. A confirmation is silent. The body's end starts the correction for a guess whose own truth is not in flight (A18 body-end, #3427) — **for a store guess exactly as for a signal's** (2026-10-03, #3662 re-pinned). 5. **Provenance** (Q-D, 2026-10-03, #3331 generalized to stores). Every flight carries the question it answers (`question` / `MAINLINE_QUESTION`). A landing asking an **older** question than the guess it lands beneath is not its answer: held beneath the guess for the commit, judged by the guess's own question; nothing moves on screen. A keyed store landing beneath an arrangement guess is re-based over the older truth by row key (`_laneRebase`; the positional residue of an unkeyed list is documented, not ruled). One predicate for both paths (`stale(el, q)`). 6. **Verdicts are display-ahead** (A19/A24 on L2). A verdict reader of a held node is the holder's verdict lane's — shown now, re-derived at the landing. A24 stands: `isPending` is TRUE over a differing stale truth held beneath a guess (the older action's landing). A quiet re-ask (`refresh()`, a poll) stays quiet through its reveal, on a store's leaves as on a memo (`_reask` stamped onto a held landing's leaf stagings); a `refresh()` during a flight asking a **new** question does not launder it. @@ -116,12 +116,14 @@ Sync derivations of transition-held sources are visible through `latest()`/`isPe ### A29. A tracked read served a live transaction's staged value enters that transaction -**Status:** **ruled, amended in place** 2026-09-13 (#3408) — maintainer ruling, Cluster 4 triage: a value derived from the held world is that transaction's work, whichever path first read it; amended 2026-09-14 (creation-time form — "born held"; the entry is the pass's, never the mainline block's); amended 2026-09-18 (#3540, maintainer ruling — boundaries are exempt: a pass under a loading boundary that has not revealed is born held into the transaction and the boundary shows its fallback now); **generalized 2026-10-01 (L2, ruling A)** — every pass served a held staging joins, tracked or untracked (`untrack` is about dependencies, not about which world a pass derives from); a held pass's children compute but do not effect and die with the commit that replaces their frame (so a derive's `onCleanup` from a run a staged re-run never committed fires when a frame replaces it — #3585 re-pinned 2026-10-03) +**Status:** **ruled, amended in place** 2026-09-13 (#3408) — maintainer ruling, Cluster 4 triage: a value derived from the held world is that transaction's work, whichever path first read it; amended 2026-09-14 (creation-time form — "born held"; the entry is the pass's, never the mainline block's); amended 2026-09-18 (#3540, maintainer ruling — boundaries are exempt: a pass under a loading boundary that has not revealed is born held into the transaction and the boundary shows its fallback now); **generalized 2026-10-01 (L2, ruling A)** — every pass served a held staging joins, tracked or untracked (`untrack` is about dependencies, not about which world a pass derives from); a held pass's children compute but do not effect and die with the commit that replaces their frame (so a derive's `onCleanup` from a run a staged re-run never committed fires when a frame replaces it — #3585 re-pinned 2026-10-03); amended 2026-10-07 (#3851, maintainer ruling — a transition holds on any unready read, and a new memo does not join it by being created: a verdict lane's mount that read a staging of a flush with a transaction is born held; no other mount joins for that read) **Pinned by:** `tests/held-conditional-memo.test.ts` (#3408: a memo whose branch flips mainline and starts reading a held signal reveals with it, not before); `tests/born-held.test.ts` (creation-time form: fresh memo + effect held, fresh direct effect shows committed, unrelated write after the mount stays mainline, untracked read throws until commit); `tests/visibility-oracle.test.ts` (published column, held and superseded states); `tests/boundary-not-born-held-3540.test.ts` (boundary exemption: a `Loading` mounted mainline over a held value shows its fallback and reveals at the commit; content created behind a fallback is collected, not published; the `on` key lands where its change lands) **Mechanism (L2, 2026-10-04):** `read()`: a reader served a held node's staging **joins** — `joinPass(c, el)` marks the pass `REACTIVE_JOINED` and, for a non-render reader, `joinPassTx(txOf(el))` (inside a flush `joinFuture`; outside, `passTx` for the flush the join schedules). Tracked or untracked (`untrack` is about dependencies). A render effect, a verdict reader or lane work is a **frame reader** instead (`frameRead`: served `_value`, `staleReader` → re-derived at the landing); a children-forbidden reader sees `_value` and joins nothing (A32); a node born into the future (uninitialized, never committed) has no committed value, so every reader derives from it — a render effect included — and joins. **Creation-time form (born held):** a first pass that read the future is held with its result (`CONFIG_STAGED`, `holdNode` at the seam; an effect's first run is the commit's, A29 — `commitPendingNode` queues it). **Ruling A / O2:** a held pass's children keep computing from the heap (`REACTIVE_ZOMBIE`, `deferredZombies`) and die with the commit that replaces their frame (`commitPendingNode` disposes the parked frame; a frame never has a gap). **Boundary exemption (#3540):** `boundaries.ts` — a pass under a loading boundary that has not revealed is born held and the boundary shows its fallback (`_hidden`: a reader behind a fallback is not on screen for `blocked`). A tracked computation served a node's staged `_pendingValue` — a value a live transaction holds — derives from that transaction's world, so its pass enters the transaction and its result is held with it. This is the read twin of the two entries that already existed, `setSignal` on a stamped node and `recompute` of a stamped node, and closes the gap between them: a conditional memo whose branch flipped mainline (`panel = show() ? count() : "hidden"`) started reading a held `count`, was served the staged value (non-stale readers keep speculation), and published a value derived from the held world into the mainline frame — `Panel: 1` beside `Count: 0`. A stale (render) reader is unchanged: the reveal carve-out (A15) serves it the committed value with no entanglement, which is why the same shape written as a plain JSX expression already showed a coherent frame. A probe (`isPending(() => x())`) observes, it does not derive, so it enters nothing (A23). **Creation-time form (amended 2026-09-14, "born held"):** the same rule for a memo or effect CREATED from mainline code while the hold is live — a component mounting on a click while an action is in flight. Its creation pass is served the staged value and derives from the transaction's world, so its result is the transaction's: staged into it, committed with it, and — for an effect — first run by its commit; it has no committed value until then (an untracked read of it throws `NotReadyError`, A19 exception 1; a stale reader of it cannot fall back to the committed frame and enters instead). A fresh reader that reads the held node DIRECTLY is a stale reader of a parallel transaction and shows the committed value as before (A15) — so a mainline mount shows the committed frame for direct bindings and holds derived ones until the commit reveals both. The entry is the pass's alone: `activeTransition` and the ambient batch are never touched from creation code, so a write made after the mount is a mainline write. (Maintainer, on the earlier direct-commit: "new creation wouldn't be on screen anyway… we could just be preemptive" — true for nodes the transaction itself creates, which stay inside it; a mainline mount IS on screen.) Before this the creation pass direct-committed the staged value into the mainline frame, and the ambient entry swallowed the handler's later writes into the action. **Boundaries are exempt (amended 2026-09-18, #3540):** born held is right for a plain memo or effect — published, its value would tear the frame. A loading boundary that has not revealed yet is the exception by definition: its job is to catch what is not ready under it rather than let it hold. A pass created under such a boundary that derives from a held value is staged into the transaction as above, and the boundary is told: it shows its fallback NOW and reveals the staged result at the commit (a `Loading` mounted mainline over a held `count()` shows its fallback beside `Count: 0`, then `count 1` beside `Count: 1`). The hold stays with readers that have content to keep — a boundary already showing content is not told, and holds like any reader. Entering instead adopted the whole flush into the hold, and the creation-time form had an in-flush escape: content created behind a fallback over a held value direct-committed the held value and ran its effect behind the fallback (`posture-store-parity` S2, closed by this amendment — nothing publishes until the commit). The boundary's `on` key is the same rule seen from outside (`createLoadingBoundary` docblock): a tracked expression evaluated as the condition of a `` wrapping the boundary would be — it lives outside the boundary, holds like a plain reader, and a change resets the boundary where the change lands (`on={count()}` with the write's commit, `on={latest(count)}` ahead of it). +**A verdict lane's mount over a staging (amended 2026-10-07, #3851).** Maintainer ruling: "we don't want to add size for these unseen things.. that being said in general transitions can hold on any unready read.. But new memos created don't need to be part of it." A transition holds on any unready read; a memo does not join it because it is new. Inside an action, ` 0}>` opens ahead of the action (A19, display-ahead) and its condition is verdict-lane work; the `` it mounts is a mainline mount (the creator's verdict lane is not inherited). Its content's first pass runs in the action's own flush and reads `x`'s staging before the seam holds it — the same world the transaction is about to hold, so the pass is born held as if it had read the held node, and the boundary shows its fallback beside the committed `x = 0` and `content 1` at the landing (`tests/verdict-mount-first-pass-3851.test.ts`). Only a verdict lane's mount: any other first pass that read a staging of the flush publishes as before and, if the flush parks, re-derives on the committed world (the staging-read rule, L2 statement 3). In particular a boundary a held memo mounts in the same flush is not made the transaction's by its creation — its content does not join, the boundary reports no pending of its own, and it appears with the memo at the commit (`tests/loading-fallback-in-flush-3540.test.ts`, "a boundary mounted by the hold"). An action body's read of an unflushed write is served committed (A28) and never counts. + ### A31. A memo computes under its own lane posture, never its puller's **Status:** **live** 2026-09-14 (#3442) — stated by the fix; the lane-side twin of A29's "a value derived from the held world is that transaction's work" diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index 861c7f124..65a50d3e7 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -257,17 +257,28 @@ export function recompute(el: Computed, create: boolean = false): void { // boundary reset, a frame rerun — so its children and its result are the // lane's; so does a member the lane's own re-staging dirtied // (REACTIVE_LANE_DIRTY, lanes.ts — a leaf the lane owns). A written guess's - // own pass is its truth arriving (A18), the frame's. A tracked read of a - // lane's value moves a derivation's pass into the lane (`read`); a leaf's - // never moves. Restored at the end, after this pass's staging and runs - // have been routed. + // own pass is its truth arriving (A18), the frame's. A first pass is its + // creator's (ruling A: a lane pass's children are the lane's frame), and + // under a guess's lane it reads as the lane's too — a binding the lane + // mounts sees the screen, like the pass that mounted it (#3835). Not + // under a verdict lane, for its reads or its result: a verdict lane holds + // verdicts, and a mount it makes is a mainline mount (A29's boundary + // exemption, #3851). A tracked read of a lane's value moves a + // derivation's pass into the lane (`read`); a leaf's never moves. + // Restored at the end, after this pass's staging and runs have been + // routed. const prevLane = passLane; setPassLane( (el._flags & REACTIVE_LANE_DIRTY || (el._config & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE) && el._x?._transaction?._lane ? el._x._transaction - : null + : // Loose `!=`: false with no creator lane (undefined == null). + create && + prevLane?._parent!._verdict != prevLane && + (creatorPass(context)?._flags ?? 0) & REACTIVE_RECOMPUTING_DEPS + ? prevLane + : null ); // Attribution hook: fired before this run touches the dep list — `_deps` // still holds the previous run's links (the subscriptions that could have @@ -492,17 +503,12 @@ export function recompute(el: Computed, create: boolean = false): void { setPassLane(prevLane); return; } - // The lane this pass is work of, if any: its seat (the node's, or the one - // a lane read moved it into), or — a first pass — its creator's (ruling A: - // a lane pass's children are the lane's frame). A pass in a lane's seat - // that read none of the lane's world has left it: its result is the + // The lane this pass is work of, if any: its seat (the node's, its + // creator's, or the one a lane read moved it into). A pass in a lane's + // seat that read none of the lane's world has left it: its result is the // frame's (a derivation whose branch no longer reaches the guess). A // guess is written, not derived — it never leaves this way. - let lane = - passLane ?? - (create && (creatorPass(oldcontext)?._flags ?? 0) & REACTIVE_RECOMPUTING_DEPS - ? prevLane - : null); + let lane = passLane; // Listed before its staging, a pending pass included (the lane's own // flight is the lane's); false: the pass left the lane (lanes.ts). const errored = !!el._x?._error; @@ -611,8 +617,12 @@ export function recompute(el: Computed, create: boolean = false): void { // skips the synchronous first run on a staged value) — when the flush // has joined a transaction and either the pass read a held node (it // derives from that world — the join merged the node's transaction - // into the flush's) or a pass created it (ruling A: a held pass's - // children are the transaction's). A node created outside any pass + // into the flush's), or — a verdict lane's mount, mainline — read a + // staging of this flush (the same world before the seam parks it; the + // mount is not shown ahead of it: #3851), or a pass created it (ruling + // A: a held pass's children are the transaction's). Any other mount + // that read a staging does not join by being new (2026-10-07): it + // publishes and re-derives if the flush parks. A node created outside any pass // that read only the committed world (root setup, a mount, an effect // callback) is nobody's frame and publishes directly, as does a first // pass that runs before anything joins: the pass's input, not a verdict @@ -632,7 +642,9 @@ export function recompute(el: Computed, create: boolean = false): void { create ? !( (flushTransaction !== null || passTx !== null) && - (joined || (creatorPass(oldcontext)?._flags ?? 0) & REACTIVE_JOINED) + (joined || + (prevLane && el._flags & REACTIVE_STAGED_READ) || + (creatorPass(oldcontext)?._flags ?? 0) & REACTIVE_JOINED) ) : isEffect && el._pendingValue === NOT_PENDING ) { @@ -990,10 +1002,14 @@ function frameRead(c: Computed, el: Signal | Computed): boolean { // mount's memo (2026-10-02, considered and reverted): a memo is not a // leaf — the transaction's own later passes read it — so a mainline mount's // derivations carry the future (A29, born held); only its direct bindings - // read the screen. + // read the screen. Lane work with no committed value yet reads a flight + // as a mount's memo does: it enters, and the boundary it mounts catches + // the pending (#3540). const verdict = c._config & CONFIG_VERDICT; if ( - passLane === null && + (passLane === null || + (c._statusFlags & STATUS_UNINITIALIZED && + (el as Computed)._statusFlags & STATUS_PENDING)) && !verdict && ((c as any)._type !== EFFECT_RENDER || el._config & CONFIG_INPUTS_PUBLISHED) ) @@ -1696,7 +1712,10 @@ export function read(el: Signal | Computed): T { !(el._config & CONFIG_OVERRIDE) && !(owner._statusFlags & STATUS_UNINITIALIZED) ) { - if (passLane !== null) committed = true; + // A reader with no committed value yet sees the flight pending: a + // boundary a lane mounts shows its fallback (A29's boundary exemption). + if (passLane !== null && !((c as Computed | null)?._statusFlags! & STATUS_UNINITIALIZED)) + committed = true; else if (c !== null && c._config & CONFIG_VERDICT) committed = GlobalQueue._observeFlight!(c as Computed, owner); } @@ -1833,11 +1852,12 @@ export function serve(el: Signal | Computed, c: Computed | null): * the frame commits — one pass, the common case — and a held write if it * parks, which the seam repairs (`stagedReaders`): the pass re-derives on * the committed world and its lane's runs wait that round, so the held - * write never shows through the lane. A verdict lane's work reads the - * frame's proposal like a frame reader (verdict.ts). */ + * write never shows through the lane. A verdict lane's work likewise: the + * lane holds verdicts, not the frame's other stagings (#3851) — except a + * verdict reader, which answered for itself (the lane seam, lanes.ts). */ export function stagedRead(c: Computed): void { c._flags |= REACTIVE_STAGED_READ; - if (passLane !== null && passLane._parent?._verdict !== passLane) stagedReaders.push(c); + if (passLane !== null) stagedReaders.push(c); } /** A10 for a staged node: a verdict reader (the pass entered a window) that diff --git a/packages/signals/src/core/lanes.ts b/packages/signals/src/core/lanes.ts index 737ad7541..c7fd01d0f 100644 --- a/packages/signals/src/core/lanes.ts +++ b/packages/signals/src/core/lanes.ts @@ -38,6 +38,7 @@ import { CONFIG_OVERRIDE, CONFIG_SLOT_NODE, CONFIG_STAGED, + CONFIG_VERDICT, EFFECT_RENDER, NOT_PENDING, REACTIVE_CHECK, @@ -799,10 +800,16 @@ GlobalQueue._laneSeams = leaks => { const l = r._x?._transaction; // A verdict reader a later read of the same pass routed into a verdict // lane answered for itself: it re-derives at the holder's landing, not - // now (one run — #3322, #3540). + // now (one run — #3322, #3540). Other verdict-lane work re-derives only + // if the frame stays parked: a transaction landing at this seam makes + // what it read the screen (one run). if ( r._flags & REACTIVE_DISPOSED || - (l != null && l._parent?._verdict === l && !(r._flags & REACTIVE_PROBE_UNANSWERED)) + (l != null && + l._parent?._verdict === l && + (r._config & CONFIG_VERDICT + ? !(r._flags & REACTIVE_PROBE_UNANSWERED) + : !blocked(l._parent))) ) continue; enqueueSub(r); diff --git a/packages/signals/tests/optimistic-mount-binding-3835.test.ts b/packages/signals/tests/optimistic-mount-binding-3835.test.ts new file mode 100644 index 000000000..b16839891 --- /dev/null +++ b/packages/signals/tests/optimistic-mount-binding-3835.test.ts @@ -0,0 +1,91 @@ +/** + * #3835: a binding created by a lane's mount reads a held write as the + * screen. An action holds `enabled = true` and guesses `visible = true`; + * the guess mounts a view whose binding reads `enabled`. A first pass is + * its creator's — reads and result (ruling A) — so the binding is the + * lane's stale reader: it shows the committed `false` with the element and + * re-derives at the action's landing. Before, the first pass read as the + * action's own pass: inside a memo the binding was born held while the + * memo's element showed (an element without its binding), and directly + * under the lane pass it showed the held `true` beside the committed world. + */ +import { describe, expect, it } from "vitest"; +import { + action, + createMemo, + createOptimistic, + createRenderEffect, + createRoot, + createSignal, + flush, + untrack +} from "../src/index.js"; + +type El = { style: string }; + +const settle = async () => { + for (let i = 0; i < 20; i++) await Promise.resolve(); + flush(); +}; + +async function mountUnderGuess(wrapInMemo: boolean) { + const [enabled, setEnabled] = createSignal(false); + const [visible, setVisible] = createOptimistic(enabled); + let release!: () => void; + const run = action(function* () { + setEnabled(true); + setVisible(true); + yield new Promise(r => (release = r)); + }); + let shown: El | undefined; + const inserted: string[] = []; + const view = (): El => { + const el = { style: "unset" }; + createRenderEffect( + () => (enabled() ? "blue" : "red"), + v => void (el.style = v) + ); + return el; + }; + createRoot(() => { + const condition = createMemo(() => !!visible()); + const content = createMemo(() => + condition() ? untrack(() => (wrapInMemo ? createMemo(view) : view())) : undefined + ); + createRenderEffect( + () => { + const v = content(); + return typeof v === "function" ? (v as () => El)() : v; + }, + el => { + shown = el; + inserted.push(el ? `inserted with style ${el.style}` : "none"); + } + ); + }); + flush(); + + const p = run(); + flush(); + await settle(); + const running = { inserted: [...inserted], style: shown?.style }; + + release(); + await p; + await settle(); + return { running, done: { inserted: [...inserted], style: shown?.style } }; +} + +describe("#3835: a lane's mount reads a held write as the screen", () => { + it("a binding inside a memo the lane creates lands with its element", async () => { + const { running, done } = await mountUnderGuess(true); + expect(running).toEqual({ inserted: ["none", "inserted with style red"], style: "red" }); + expect(done).toEqual({ inserted: ["none", "inserted with style red"], style: "blue" }); + }); + + it("a binding the lane pass creates directly shows the committed value", async () => { + const { running, done } = await mountUnderGuess(false); + expect(running).toEqual({ inserted: ["none", "inserted with style red"], style: "red" }); + expect(done).toEqual({ inserted: ["none", "inserted with style red"], style: "blue" }); + }); +}); diff --git a/packages/signals/tests/optimistic-mount-nested-memo-3835.test.ts b/packages/signals/tests/optimistic-mount-nested-memo-3835.test.ts new file mode 100644 index 000000000..8c25508be --- /dev/null +++ b/packages/signals/tests/optimistic-mount-nested-memo-3835.test.ts @@ -0,0 +1,156 @@ +/** + * #3835 — a subtree mounted by an optimistic write is the lane's frame + * (ruling A), all the way down. A memo created by the lane pass used to sit + * outside the lane while its own body ran, so the render effect it created + * read the transaction's staged value and was born held: the lane revealed + * the memo's element with the effect's first run still waiting on the + * transaction. + */ +import { afterEach, describe, expect, it } from "vitest"; +import { + action, + createMemo, + createOptimistic, + createRenderEffect, + createRoot, + createSignal, + flush, + createLoadingBoundary, + untrack +} from "../src/index.js"; + +afterEach(() => flush()); + +const tick = () => new Promise(r => setTimeout(r, 0)); + +function setup(nested: boolean) { + type El = { color?: string }; + let shown: El | undefined; + let run!: () => Promise; + let finish!: () => void; + let dispose!: () => void; + createRoot(d => { + dispose = d; + const [enabled, setEnabled] = createSignal(false); + const [visible, setVisible] = createOptimistic(enabled); + const content = () => { + const el: El = {}; + createRenderEffect( + () => (enabled() ? "blue" : "red"), + v => { + el.color = v; + } + ); + return el; + }; + const view = createMemo(() => { + if (!visible()) return undefined; + return nested ? createMemo(content) : () => content(); + }); + createRenderEffect( + () => view()?.(), + el => { + shown = el; + } + ); + const act = action(function* () { + setEnabled(true); + setVisible(true); + yield new Promise(r => (finish = r)); + }); + run = () => act(); + }); + flush(); + return { + shown: () => (shown ? { ...shown } : undefined), + run: () => run(), + finish: () => finish(), + dispose: () => dispose() + }; +} + +describe("#3835 optimistic mount of a nested memo", () => { + for (const nested of [true, false]) + it(`${nested ? "memo-wrapped" : "direct"} content reveals with the screen value`, async () => { + const s = setup(nested); + expect(s.shown()).toBeUndefined(); + const p = s.run(); + flush(); + await tick(); + flush(); + expect(s.shown()).toEqual({ color: "red" }); + s.finish(); + await p; + await tick(); + flush(); + expect(s.shown()).toEqual({ color: "blue" }); + s.dispose(); + }); +}); + +describe("#3835 a Loading mounted by an optimistic write over a parent flight", () => { + // A29's boundary exemption holds for lane work too: the boundary's first + // pass sees the flight pending, held or not, and shows its fallback; it is + // not handed the flight's committed value. + for (const order of ["same tick", "after the park"]) + it(`shows its fallback (${order})`, async () => { + const log: unknown[] = []; + let run!: () => Promise; + let finish!: () => void; + let dispose!: () => void; + const lands: ((v: number) => void)[] = []; + createRoot(d => { + dispose = d; + const [count, setCount] = createSignal(1); + const [show, setShow] = createSignal(false); + const data = createMemo(() => { + const v = count(); + return v === 1 ? v : new Promise(r => lands.push(r)); + }); + const [visible, setVisible] = createOptimistic(show); + const view = createMemo(() => + visible() + ? untrack(() => + createLoadingBoundary( + () => `data ${data()}`, + () => "fallback" + ) + ) + : () => "hidden" + ); + createRenderEffect( + () => view()(), + v => { + log.push(v); + } + ); + const act = action(function* () { + setCount(2); + if (order === "after the park") yield tick(); + setShow(true); + setVisible(true); + yield new Promise(r => (finish = r)); + }); + run = () => act(); + }); + flush(); + const p = run(); + for (let i = 0; i < 2; i++) { + flush(); + await tick(); + } + flush(); + expect(log).toEqual(["hidden", "fallback"]); + lands.forEach(land => land(2)); + await tick(); + flush(); + expect(log).not.toContain("data 1"); + finish(); + await p; + await tick(); + flush(); + expect(log.at(-1)).toBe("data 2"); + expect(log).not.toContain("data 1"); + dispose(); + }); +}); diff --git a/packages/signals/tests/verdict-mount-first-pass-3851.test.ts b/packages/signals/tests/verdict-mount-first-pass-3851.test.ts new file mode 100644 index 000000000..02c5ef014 --- /dev/null +++ b/packages/signals/tests/verdict-mount-first-pass-3851.test.ts @@ -0,0 +1,99 @@ +/** + * #3851: a mount a verdict reader makes is a mainline mount. An action + * holds `x = 1`; ` 0}>` opens at once (display-ahead) + * and mounts a `` whose content reads `x`. A verdict lane computes + * the real outcome and its mounts stay mainline (A29's boundary exemption), + * so the content reads the held `x` as the frame does: it waits behind the + * fresh boundary's fallback and appears at the action's landing (A29's + * boundary scope, 2026-10-06). Before, the content's first pass was staged + * in the verdict lane its creator was in and showed `content 1` beside the + * committed `x = 0`. + */ +import { describe, expect, it } from "vitest"; +import { + action, + createLoadingBoundary, + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush, + latest, + untrack +} from "../src/index.js"; + +const settle = async () => { + for (let i = 0; i < 3; i++) { + await new Promise(r => setTimeout(r, 0)); + flush(); + } +}; + +async function mountUnderVerdict(contentInMemo: boolean) { + const [x, setX] = createSignal(0); + let screenX: unknown; + let slot: unknown; + createRoot(() => { + //

{x()}

+ createRenderEffect(x, v => { + screenX = v; + }); + // 0}> + // content {x()} + // + const when = createMemo(() => latest(x) > 0); + const children = createMemo(() => + when() + ? untrack(() => + createLoadingBoundary( + contentInMemo ? createMemo(() => `content ${x()}`) : () => `content ${x()}`, + () => "fallback" + ) + ) + : "closed" + ); + createRenderEffect( + () => { + const c = children(); + return typeof c === "function" ? c() : c; + }, + v => { + slot = v; + } + ); + }); + flush(); + const screens: unknown[] = [[screenX, slot]]; + + let release!: () => void; + const done = action(function* () { + setX(1); + yield new Promise(r => (release = r)); + })(); + await settle(); + screens.push([screenX, slot]); + + release(); + await done; + await settle(); + screens.push([screenX, slot]); + return screens; +} + +describe("#3851: a verdict reader's mount stays mainline", () => { + it("the mounted boundary shows its fallback while the action holds the value", async () => { + expect(await mountUnderVerdict(false)).toEqual([ + [0, "closed"], + [0, "fallback"], + [1, "content 1"] + ]); + }); + + it("same with the content in a memo", async () => { + expect(await mountUnderVerdict(true)).toEqual([ + [0, "closed"], + [0, "fallback"], + [1, "content 1"] + ]); + }); +}); diff --git a/packages/web/test/optimistic-mount-style-3835.spec.tsx b/packages/web/test/optimistic-mount-style-3835.spec.tsx new file mode 100644 index 000000000..594183f8b --- /dev/null +++ b/packages/web/test/optimistic-mount-style-3835.spec.tsx @@ -0,0 +1,74 @@ +/** + * @jsxImportSource @solidjs/web + * @vitest-environment jsdom + */ +import { expect, test } from "vitest"; +import { Show, action, createMemo, createOptimistic, createSignal, flush } from "solid-js"; +import { render } from "../src/index.js"; +import type { JSX } from "@solidjs/web"; + +async function settle() { + for (let i = 0; i < 8; i++) await Promise.resolve(); + flush(); +} + +// #3835: an element an optimistic guess mounts lands with its bindings. Its +// style reads a signal the action holds, so it shows the committed value +// until the action lands — with or without a memo around the element. +for (const shape of ["memo", "no memo", "memo, static text"] as const) { + test(`an optimistically mounted element lands with its style (${shape}, #3835)`, async () => { + const root = document.createElement("div"); + const [enabled, setEnabled] = createSignal(false); + const [visible, setVisible] = createOptimistic(enabled); + let release!: () => void; + const run = action(function* () { + setEnabled(true); + setVisible(true); + yield new Promise(r => (release = r)); + }); + + function Content(props: { enabled: () => boolean }): JSX.Element { + if (shape === "no memo") + return ( +
+ Hello{props.enabled() ? " world" : ""} +
+ ); + if (shape === "memo, static text") + return createMemo(() => ( +
Hello world
+ )) as unknown as JSX.Element; + return createMemo(() => ( +
+ Hello{props.enabled() ? " world" : ""} +
+ )) as unknown as JSX.Element; + } + + const dispose = render( + () => ( + + + + ), + root + ); + flush(); + expect(root.innerHTML).toBe(""); + + const pending = run(); + flush(); + await settle(); + const div = root.querySelector("div")!; + expect(div.style.color).toBe("red"); + expect(div.textContent).toBe(shape === "memo, static text" ? "Hello world" : "Hello"); + + release(); + await pending; + await settle(); + expect(root.querySelector("div")).toBe(div); + expect(div.style.color).toBe("blue"); + expect(div.textContent).toBe("Hello world"); + dispose(); + }); +} diff --git a/packages/web/test/runtime/chunk-reader-hardening.spec.ts b/packages/web/test/runtime/chunk-reader-hardening.spec.ts index b92944d7a..dfd90c6b2 100644 --- a/packages/web/test/runtime/chunk-reader-hardening.spec.ts +++ b/packages/web/test/runtime/chunk-reader-hardening.spec.ts @@ -9,11 +9,7 @@ * - An oversized store is released once nothing in it is unread. */ import { describe, expect, it } from "vitest"; -import { - ChunkReader, - createChunk, - deserializeStream -} from "../../server-functions/src/shared.js"; +import { ChunkReader, createChunk, deserializeStream } from "../../server-functions/src/shared.js"; const encoder = new TextEncoder(); @@ -188,7 +184,9 @@ describe("ChunkReader cleanup", () => { it("releases a large frame's store as soon as the stream goes idle", async () => { // No frame follows: the connection waits, as a live source does. - const reader = new ChunkReader(source(reads(createChunk("x".repeat(8 << 20))), { hold: true }).stream); + const reader = new ChunkReader( + source(reads(createChunk("x".repeat(8 << 20))), { hold: true }).stream + ); expect((await reader.next()).value).toHaveLength(8 << 20); expect(storeOf(reader).length).toBe(0); reader.cancel(undefined); @@ -210,7 +208,9 @@ describe("ChunkReader cleanup", () => { }); it("keeps a store at or under 64 KiB across frames", async () => { - const frames = Array.from({ length: 20 }, (_, i) => createChunk(`frame-${i}-` + "y".repeat(300))); + const frames = Array.from({ length: 20 }, (_, i) => + createChunk(`frame-${i}-` + "y".repeat(300)) + ); const reader = new ChunkReader(source([concat(frames)], { hold: true }).stream); await reader.next(); const store = storeOf(reader); diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index a80fdc7ce..142b7db2d 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -1,23 +1,23 @@ { "signals: core floor (createSignal/Memo/Effect/Root/flush)": { - "cap": "7.35 KB", - "minified": 20133 + "cap": "7.40 KB", + "minified": 20178 }, "app: render + one signal (the simple-app floor)": { - "cap": "9.86 KB", - "minified": 27687 + "cap": "9.88 KB", + "minified": 27731 }, "app: hydrating (no stores) with Show/For/Loading/Errored/lazy": { "cap": "17.73 KB", "minified": 52626 }, "page: base server components (hydrating + dynamic + frames + sf reference)": { - "cap": "45.12 KB", - "minified": 146295 + "cap": "45.14 KB", + "minified": 146424 }, "page: live server components (base + live/GET + action + isPending/latest)": { - "cap": "48.89 KB", - "minified": 158377 + "cap": "48.91 KB", + "minified": 158537 }, "server: floor (getRequestEvent + isServer)": { "cap": "1.34 KB", diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 46be9773b..b9e44c26d 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -496,6 +496,16 @@ module.exports = [ // unchanged at 7.35 KB, recorded minified 20,133 B (first record); // CI-measured at 7,361 B (20,133 B minified). Lower only: cap at measured + // 10 B rounded up to 0.01 KB; recorded minified never raised. + // Size-Exception (first-pass lane seating, #3869 — #3835/#3851, + // 2026-10-07): 7.35 KB -> 7.40 KB (floor-caps.json), measured at 7,384 B + // by CI (Size run 37642512669) against `next` @ 53ef0e69e's 7,374 (+10 B; + // 34 B over the cap; +34 B minified, 20,144 -> 20,178; recorded 20,133 -> + // 20,178) — the signals core: a first pass seated in its creator's guess + // lane, the verdict-lane filter, lane work over a pending flight entering + // (#3843), a verdict lane's mount born held for a staged read, and + // verdict-lane stagedReaders. Cap set at measured + 10 B rounded up to + // 0.01 KB. Accepted by the maintainer 2026-10-07. The cap is frozen again + // at 7.40 KB. limit: floorCaps["signals: core floor (createSignal/Memo/Effect/Root/flush)"], capMinified: floorMinified["signals: core floor (createSignal/Memo/Effect/Root/flush)"], alias @@ -919,8 +929,17 @@ module.exports = [ // unchanged at 14.56 KB, recorded minified 44,396 B (first record); // CI-measured at 14,540 B (44,396 B minified). Lower only: cap at measured // + 10 B rounded up to 0.01 KB; recorded minified never raised. - limit: "14.56 KB", - capMinified: 44396, + // Size-Exception (first-pass lane seating, #3869 — #3835/#3851, + // 2026-10-07): 14.56 KB -> 14.62 KB, measured at 14,606 B by CI (Size run + // 37642512669) against `next` @ 53ef0e69e's 14,603 (+3 B; 46 B over the + // cap; +32 B minified, 44,407 -> 44,439; recorded 44,396 -> 44,439) — the + // signals core: a first pass seated in its creator's guess lane, the + // verdict-lane filter, lane work over a pending flight entering (#3843), a + // verdict lane's mount born held for a staged read, and verdict-lane + // stagedReaders. Cap set at measured + 10 B rounded up to 0.01 KB. + // Accepted by the maintainer 2026-10-07. + limit: "14.62 KB", + capMinified: 44439, alias }, { @@ -1369,6 +1388,16 @@ module.exports = [ // unchanged at 9.86 KB, recorded minified 27,687 B (first record); // CI-measured at 9,856 B (27,687 B minified). Lower only: cap at measured + // 10 B rounded up to 0.01 KB; recorded minified never raised. + // Size-Exception (first-pass lane seating, #3869 — #3835/#3851, + // 2026-10-07): 9.86 KB -> 9.88 KB (floor-caps.json), measured at 9,867 B + // by CI (Size run 37642512669) against `next` @ 53ef0e69e's 9,884 (-17 B; + // 7 B over the cap; +33 B minified, 27,698 -> 27,731; recorded 27,687 -> + // 27,731) — the signals core: a first pass seated in its creator's guess + // lane, the verdict-lane filter, lane work over a pending flight entering + // (#3843), a verdict lane's mount born held for a staged read, and + // verdict-lane stagedReaders. Cap set at measured + 10 B rounded up to + // 0.01 KB. Accepted by the maintainer 2026-10-07. The cap is frozen again + // at 9.88 KB. limit: floorCaps["app: render + one signal (the simple-app floor)"], capMinified: floorMinified["app: render + one signal (the simple-app floor)"], alias @@ -2384,8 +2413,17 @@ module.exports = [ // unchanged at 12.86 KB, recorded minified 36,568 B (first record); // CI-measured at 12,905 B (36,568 B minified). Lower only: cap at measured // + 10 B rounded up to 0.01 KB; recorded minified never raised. - limit: "12.86 KB", - capMinified: 36568, + // Size-Exception (first-pass lane seating, #3869 — #3835/#3851, + // 2026-10-07): 12.86 KB -> 12.88 KB, measured at 12,866 B by CI (Size run + // 37642512669) against `next` @ 53ef0e69e's 12,890 (-24 B; 6 B over the + // cap; +33 B minified, 36,584 -> 36,617; recorded 36,568 -> 36,617) — the + // signals core: a first pass seated in its creator's guess lane, the + // verdict-lane filter, lane work over a pending flight entering (#3843), a + // verdict lane's mount born held for a staged read, and verdict-lane + // stagedReaders. Cap set at measured + 10 B rounded up to 0.01 KB. + // Accepted by the maintainer 2026-10-07. + limit: "12.88 KB", + capMinified: 36617, alias }, { @@ -3103,8 +3141,17 @@ module.exports = [ // unchanged at 10.05 KB, recorded minified 28,218 B (first record); // CI-measured at 10,031 B (28,218 B minified). Lower only: cap at measured // + 10 B rounded up to 0.01 KB; recorded minified never raised. - limit: "10.05 KB", - capMinified: 28218, + // Size-Exception (first-pass lane seating, #3869 — #3835/#3851, + // 2026-10-07): 10.05 KB -> 10.07 KB, measured at 10,058 B by CI (Size run + // 37642512669) against `next` @ 53ef0e69e's 10,042 (+16 B; 8 B over the + // cap; +32 B minified, 28,229 -> 28,261; recorded 28,218 -> 28,261) — the + // signals core: a first pass seated in its creator's guess lane, the + // verdict-lane filter, lane work over a pending flight entering (#3843), a + // verdict lane's mount born held for a staged read, and verdict-lane + // stagedReaders. Cap set at measured + 10 B rounded up to 0.01 KB. + // Accepted by the maintainer 2026-10-07. + limit: "10.07 KB", + capMinified: 28261, alias }, { @@ -3144,8 +3191,17 @@ module.exports = [ // unchanged at 25.13 KB, recorded minified 78,898 B (first record); // CI-measured at 25,129 B (78,898 B minified). Lower only: cap at measured // + 10 B rounded up to 0.01 KB; recorded minified never raised. - limit: "25.13 KB", - capMinified: 78898, + // Size-Exception (first-pass lane seating, #3869 — #3835/#3851, + // 2026-10-07): 25.13 KB -> 25.22 KB, measured at 25,207 B by CI (Size run + // 37642512669) against `next` @ 53ef0e69e's 25,197 (+10 B; 77 B over the + // cap; +33 B minified, 78,913 -> 78,946; recorded 78,898 -> 78,946) — the + // signals core: a first pass seated in its creator's guess lane, the + // verdict-lane filter, lane work over a pending flight entering (#3843), a + // verdict lane's mount born held for a staged read, and verdict-lane + // stagedReaders. Cap set at measured + 10 B rounded up to 0.01 KB. + // Accepted by the maintainer 2026-10-07. + limit: "25.22 KB", + capMinified: 78946, alias }, { @@ -3188,8 +3244,17 @@ module.exports = [ // measured + 10 B; recorded minified 99,257 B. Accepted by the maintainer // (2026-10-06, "pay the cost for correctness"). The cap is frozen again // at 31.03 KB. - limit: "31.03 KB", - capMinified: 99257, + // Size-Exception (first-pass lane seating, #3869 — #3835/#3851, + // 2026-10-07): 31.03 KB -> 31.06 KB, measured at 31,050 B by CI (Size run + // 37642512669) against `next` @ 53ef0e69e's 30,958 (+92 B; 20 B over the + // cap; +33 B minified, 99,272 -> 99,305; recorded 99,257 -> 99,305) — the + // signals core: a first pass seated in its creator's guess lane, the + // verdict-lane filter, lane work over a pending flight entering (#3843), a + // verdict lane's mount born held for a staged read, and verdict-lane + // stagedReaders. Cap set at measured + 10 B rounded up to 0.01 KB. + // Accepted by the maintainer 2026-10-07. + limit: "31.06 KB", + capMinified: 99305, alias }, { @@ -3618,6 +3683,16 @@ module.exports = [ // at measured + 10 B rounded up to 0.01 KB; recorded minified 146,295 B. // Accepted by the maintainer (2026-10-07: server-function/frames reader // correctness). The cap is frozen again at 45.12 KB. + // Size-Exception (first-pass lane seating, #3869 — #3835/#3851, + // 2026-10-07): 45.12 KB -> 45.14 KB (floor-caps.json), measured at 45,130 + // B by CI (Size run 37642512669) against `next` @ 53ef0e69e's 45,117 (+13 + // B; 10 B over the cap; +33 B minified, 146,391 -> 146,424; recorded + // 146,295 -> 146,424) — the signals core: a first pass seated in its + // creator's guess lane, the verdict-lane filter, lane work over a pending + // flight entering (#3843), a verdict lane's mount born held for a staged + // read, and verdict-lane stagedReaders. Cap set at measured + 10 B rounded + // up to 0.01 KB. Accepted by the maintainer 2026-10-07. The cap is frozen + // again at 45.14 KB. limit: floorCaps["page: base server components (hydrating + dynamic + frames + sf reference)"], capMinified: floorMinified["page: base server components (hydrating + dynamic + frames + sf reference)"], @@ -3754,6 +3829,16 @@ module.exports = [ // retains. Cap set at measured + 10 B rounded up to 0.01 KB; recorded // minified 158,377 B. Accepted by the maintainer 2026-10-07. The cap is // frozen again at 48.89 KB. + // Size-Exception (first-pass lane seating, #3869 — #3835/#3851, + // 2026-10-07): 48.89 KB -> 48.91 KB (floor-caps.json), measured at 48,892 + // B by CI (Size run 37642512669) against `next` @ 53ef0e69e's 48,831 (+61 + // B; 2 B over the cap; +60 B minified, 158,477 -> 158,537; recorded + // 158,377 -> 158,537) — the signals core: a first pass seated in its + // creator's guess lane, the verdict-lane filter, lane work over a pending + // flight entering (#3843), a verdict lane's mount born held for a staged + // read, and verdict-lane stagedReaders. Cap set at measured + 10 B rounded + // up to 0.01 KB. Accepted by the maintainer 2026-10-07. The cap is frozen + // again at 48.91 KB. limit: floorCaps["page: live server components (base + live/GET + action + isPending/latest)"], capMinified: floorMinified["page: live server components (base + live/GET + action + isPending/latest)"],