Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/verdict-born-into-other-flush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

A verdict reader born into a hold reads that hold's value of a node it holds in every flush, not only the hold's own: one run by another transaction's flush was served the committed value (#4426).
2 changes: 1 addition & 1 deletion packages/signals/docs/RULES-INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:111` | — | 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:303` | — | 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:95` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 seat.ts×3 types.ts×1 verdict.ts×2 store.ts×5 | continuation-sibling-reader-4146.test.ts×1 createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 issue-3800-repro.test.ts×1 latest-held-till-flush.test.ts×1 latest-memo-inline-parity.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:119` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×14 effect.ts×1 lanes.ts×1 scheduler.ts×4 seat.ts×1 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-branch-verdict-reader-4010.test.ts×1 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 verdict-mount-loading-flush-loop.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 … |
| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:119` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×14 effect.ts×1 lanes.ts×1 scheduler.ts×4 seat.ts×1 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-branch-verdict-reader-4010.test.ts×1 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-born-into-other-flush-4426.test.ts×1 verdict-mount-first-pass-3851.test.ts×2 verdict-mount-loading-flush-loop.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:259` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 projection.ts×1 | 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 projection-peek-loop-4342.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:133` | boundaries.ts×1 core.ts×1 lanes.ts×1 seat.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 unowned-memo-ispending-reader-4366.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:141` | core.ts×4 lanes.ts×1 seat.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… |
Expand Down
19 changes: 14 additions & 5 deletions packages/signals/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2022,7 +2022,7 @@ export function read<T>(el: Signal<T> | Computed<T>): T {
// transaction, reads the screen too.
if (
((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane?.folded)) &&
stagedScreen(c as Computed<any>)
stagedScreen(c as Computed<any>, el)
) {
if (__TEST__) shadowActual(SLOT_C);
return el._value as T;
Expand Down Expand Up @@ -2270,7 +2270,7 @@ export function serve(el: Signal<any> | Computed<any>, c: Computed<any> | null):
(observeFlightHook &&
c!._config & CONFIG_VERDICT &&
!((el as Computed<any>)._statusFlags & STATUS_UNINITIALIZED) &&
stagedScreen(c!) &&
stagedScreen(c!, el) &&
!throwShown(el))
) {
if (__TEST__) shadowActual(SLOT_C);
Expand Down Expand Up @@ -2338,9 +2338,18 @@ export function stagedRead(c: Computed<any>, el?: Signal<any> | Computed<any>):
* transaction the answer is the seam's and the reader watches it
* (verdict.ts). A node born into the future has no screen: its staging is
* its only value (A29). Nor has a reader born into the flush's transaction
* (`bornInto`, #4010): its branch exists only there. */
export function stagedScreen(c: Computed<any>): boolean {
if (flushTransaction === null || bornInto(c, flushTransaction)) return false;
* (`bornInto`, #4010): its branch exists only there — nor, reading `el` a
* hold holds, one born into that hold, whichever flush runs it (D-25: the
* hold's own work reads plainly). A held node `read` brings here is one
* `frameRead` found the reader born into: every other it served the screen
* (a lane's node it never asks — CONFIG_OVERRIDE; #4426). */
export function stagedScreen(c: Computed<any>, el?: Signal<any> | Computed<any>): boolean {
if (
flushTransaction === null ||
bornInto(c, flushTransaction) ||
(el?._config! & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD
)
return false;
// A transaction only a `latest()` read opened holds nothing: the staging
// is the frame's read for a pass in the frame's seat, as it is before the
// frame has one, and the seam re-derives the pass once only if the flush
Expand Down
99 changes: 99 additions & 0 deletions packages/signals/tests/verdict-born-into-other-flush-4426.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* #4426: a verdict reader born into a hold `T`, re-run by a flush whose
* transaction is not `T`, reads `T`'s values (D-25: the hold's own work —
* a pass born in its world — reads plainly; D-27, A29: a node born into a
* hold has no screen). `read`'s two deciders disagreed: `frameRead` asked
* "born into `el`'s hold" and joined the pass, `stagedScreen` asked "born
* into the flush's transaction" and served the committed value — so the
* effect a `<Show when={s()}>` content mounted for `s = 3` read `s() === 0`,
* the value its branch does not exist for.
*
* Shape: async-gen seed 3289 #911 —
* `<Show when={s0()} fallback><span>{isPending(m0)}</span>
* <span fx={[s0(), isPending(m0)]} /></Show>`, `m0 = async(latest(s0) + s0)`.
*/
import { expect, it } from "vitest";
import {
createLoadingBoundary,
createMemo,
createRenderEffect,
createRoot,
createSignal,
flush,
isPending,
latest
} from "../src/index.js";

const tick = async () => {
for (let i = 0; i < 6; i++) await Promise.resolve();
flush();
await new Promise(r => setTimeout(r, 0));
};

it("a verdict reader born into a hold reads the hold's value in another transaction's flush", async () => {
const flights: { v: number; resolve: (v: number) => void }[] = [];
const settle = () => {
while (flights.length) {
const f = flights.shift()!;
f.resolve(f.v);
}
};
const [s, setS] = createSignal(3);
const m = createMemo(
() =>
new Promise<number>(resolve => {
flights.push({ v: 1 + latest(s) + s(), resolve });
})
);
const seen: number[] = [];
createRoot(() => {
const out = createLoadingBoundary(
() => {
// <Show when={s()} fallback="[F]">
const value = createMemo(() => s());
const condition = createMemo(value, { equals: (a, b) => !a === !b, sync: true });
return createMemo(
() => {
if (!condition()) return "[F]";
createRenderEffect(
() => isPending(m),
() => {}
);
createRenderEffect(
() => {
seen.push(s());
return isPending(m);
},
() => {}
);
return "content";
},
{ sync: true }
);
},
() => "[L]"
);
createRenderEffect(
() => {
let v: unknown = out;
while (typeof v === "function") v = v();
return v;
},
() => {}
);
});
flush();
setS(0);
flights.shift()!.resolve(7);
flush();
seen.length = 0;
setS(3);
for (let r = 0; r < 5; r++) {
for (let i = 0; i < 4; i++) await tick();
flush();
settle();
}
// The content exists only for a truthy `s`: every pass of its effect reads 3.
expect(seen.length).toBeGreaterThan(0);
expect(seen.filter(v => v !== 3)).toEqual([]);
});
11 changes: 10 additions & 1 deletion packages/web/test/errored-recovery-nested-loading-4425.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@
* refresh landing is the new answer (D-39).
*/
import { expect, test } from "vitest";
import { Errored, For, Loading, Show, createMemo, flush, refresh, type Refreshable } from "solid-js";
import {
Errored,
For,
Loading,
Show,
createMemo,
flush,
refresh,
type Refreshable
} from "solid-js";
import { render } from "../src/index.js";

const tick = async () => {
Expand Down
8 changes: 4 additions & 4 deletions scripts/size/floor-caps.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
"minified": 53585
},
"page: base server components (hydrating + dynamic + frames + sf reference)": {
"cap": "33.66 KB",
"minified": 101605
"cap": "33.77 KB",
"minified": 101638
},
"page: live server components (base + live/GET + action + isPending/latest)": {
"cap": "38.76 KB",
"minified": 117227
"cap": "38.83 KB",
"minified": 117331
},
"server: floor (getRequestEvent + isServer)": {
"cap": "1.29 KB",
Expand Down
7 changes: 5 additions & 2 deletions scripts/size/scenarios.js
Original file line number Diff line number Diff line change
Expand Up @@ -5244,6 +5244,7 @@ module.exports = [
// 33.66 KB, recorded minified 101,586 -> 101,605 B; CI-measured at 33,645 B
// (101,605 B minified). Lower only: cap at measured + 10 B rounded up to
// 0.01 KB; recorded minified never raised.
// A verdict reader born into a hold reads it whichever flush runs it (#4426, D-25/D-27; re-measured over next @ 9c4e4a8): 33.66 KB -> 33.77 KB, recorded minified 101,605 -> 101,638 B, measured locally (linux) at 33,755 B brotli. `stagedScreen`'s held-node check (+20 B minified in core.js) on top of next's +13 B over this floor's record (#4452); the brotli is layout. Correctness; a frozen floor, Size-Exception in the PR body. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured.
limit: floorCaps["page: base server components (hydrating + dynamic + frames + sf reference)"],
capMinified:
floorMinified["page: base server components (hydrating + dynamic + frames + sf reference)"],
Expand Down Expand Up @@ -5598,6 +5599,7 @@ module.exports = [
// (117,143 B minified). Lower only: cap at measured + 10 B rounded up to
// 0.01 KB; recorded minified never raised.
// A held memo's parked reader re-derives at the seam (#4439, 2026-10-30, over next @ b249ad96): 38.74 KB -> 38.76 KB, recorded minified 117,143 -> 117,227 B (+84 B), measured locally (linux) at 38,741 B brotli. `laneSeams` also re-derives a reader whose pass read a held node, so `isPending` of a memo parked behind a held one turns true. Correctness; a frozen floor, +15 B brotli is under the maintainer's 20 B flag line (2026-10-29), Size-Exception in the PR body. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured.
// A verdict reader born into a hold reads it whichever flush runs it (#4426, D-25/D-27; re-measured over next @ 9c4e4a8): 38.81 KB -> 38.83 KB, recorded minified 117,251 -> 117,331 B, measured locally (linux) at 38,817 B brotli. `stagedScreen`'s held-node check (+20 B minified in core.js) on top of next's +13 B over this floor's record (#4452); the brotli is layout. Correctness; a frozen floor, Size-Exception in the PR body. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured.
limit: floorCaps["page: live server components (base + live/GET + action + isPending/latest)"],
capMinified:
floorMinified["page: live server components (base + live/GET + action + isPending/latest)"],
Expand Down Expand Up @@ -5767,8 +5769,9 @@ module.exports = [
// 35.66 KB, recorded minified 107,955 -> 107,974 B; CI-measured at 35,645 B
// (107,974 B minified). Lower only: cap at measured + 10 B rounded up to
// 0.01 KB; recorded minified never raised.
limit: "35.66 KB",
capMinified: 107974,
// A verdict reader born into a hold reads it whichever flush runs it (#4426, D-25/D-27; re-measured over next @ 9c4e4a8): 35.66 KB -> 35.72 KB, recorded minified 107,974 -> 108,007 B (+33 B), measured locally (linux) at 35,702 B brotli. `stagedScreen`'s held-node check (+20 B minified in core.js) on top of next's +13 B; the brotli is layout. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured.
limit: "35.72 KB",
capMinified: 108007,
alias: pageAlias
},
{
Expand Down
Loading