From 355a812660125708aa8ab5bbd27a1c9d0eabc3f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 10:36:44 +0000 Subject: [PATCH 01/10] fix(signals): a staged error reaches the memos that derive from it (D-41) An error a held node's pass (or flight) found is staged with the frame (CONFIG_STAGED_ERROR), but nothing told its dependents: a staged value re-runs them (insertSubs), a staged error took the errored branch, which leaves notification to a publish that only comes at the commit. A memo over it kept its old value for the rest of the hold, so `latest()` of it returned that value where `latest()` of the source threw the proposed error (D-22), and `isPending()` of it read false (D-23 iii). The settle walk stageError already runs now marks the derivations it reaches as carrying the staging (`stageMark`), the staged twin of propagateStatus's error mark: marked, not re-run (a re-run would replace the frame a boundary's content owns, where a published error only marks), queued so the seam holds them with the frame, and walked on so their own verdict readers re-derive. A pending mark on a node carrying a staged error re-derives it through propagateStatus's verdict branch, so a source that re-asks in the same hold does not publish the old error (#4109). Only verdicts read a derivation's staging before its commit, so all of it runs with the verdict engine alone: the compiled floor is unchanged. Found by the async-gen rerun on next 5e68925c (E2: latest and isPending through an identity memo). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- .../staged-error-reaches-derivations.md | 5 + packages/signals/src/core/async.ts | 43 ++++- .../tests/staged-error-derivation.test.ts | 156 ++++++++++++++++++ .../e2-staged-error-derivation.spec.ts | 66 ++++++++ 4 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 .changeset/staged-error-reaches-derivations.md create mode 100644 packages/signals/tests/staged-error-derivation.test.ts create mode 100644 packages/web/test/async-gen/e2-staged-error-derivation.spec.ts diff --git a/.changeset/staged-error-reaches-derivations.md b/.changeset/staged-error-reaches-derivations.md new file mode 100644 index 0000000000..a0b391d76a --- /dev/null +++ b/.changeset/staged-error-reaches-derivations.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +A memo's error staged with a hold now reaches the memos that derive from it: `latest()` of them throws the proposed error and `isPending()` of them is `true` until the hold commits, as for the memo itself (D-41). diff --git a/packages/signals/src/core/async.ts b/packages/signals/src/core/async.ts index 415dc4fc72..befc426190 100644 --- a/packages/signals/src/core/async.ts +++ b/packages/signals/src/core/async.ts @@ -214,12 +214,38 @@ export function stageError(el: Computed, error: any, inPass?: boolean): boo el._pendingValue = error instanceof StatusError ? error : new StatusError(el, error); el._config |= CONFIG_STAGED | CONFIG_STAGED_ERROR; // Readers parked on the flight re-run and derive from the staging; the - // verdict readers the walk wakes need a flush (#4106). + // verdict readers the walk wakes need a flush (#4106); the derivations it + // reaches carry the staging (`settlePendingSource`). settlePendingSource(el); schedule(); return true; } +/** D-41: a derivation of a staged error carries it, staged with the same + * frame — marked, not re-run, as a published error marks it (a re-run would + * replace the frame its content owns) — and its own verdict readers + * re-derive (the settle walk). Only a verdict reads a derivation's staging + * before its commit, so only the verdict engine marks. Not a boundary's + * output, an async memo, an effect or a boundary's tree (`EFFECT_TRACKED << + * 23` spans CONFIG_FRAME and every effect type), one already carrying a + * staged error, nor a derived store's derive, whose readers learn its status + * through its family (#4189): the commit's publish reaches them. A pending + * mark supersedes the staging (`propagateStatus`). */ +function stageMark(node: Computed, el: Computed): void { + if ( + !( + node._config & + (CONFIG_REDERIVE | CONFIG_STAGED_ERROR | CONFIG_ASYNC | (EFFECT_TRACKED << 23)) || + (node as any).fam + ) + ) { + node._pendingValue = el._pendingValue; + node._config |= CONFIG_STAGED_ERROR; + queuePendingNode(node); + settlePendingSource(node); + } +} + export function settleErroredDependents(el: Computed, error: any): void { let scheduled = false; const visited = new NewSet>(); @@ -318,7 +344,13 @@ export function settlePendingSource(el: Computed, source: Computed = e // equal to the committed value notifies nobody — the source settling // is its verdict changing: it runs again. (Verdict readers exist only // with the verdict engine installed.) - return holdWorkHook && node._config & CONFIG_VERDICT && enqueueSub(node); + // D-41: a derivation of a staged error carries it (`stageMark`). + return ( + holdWorkHook && + (node._config & CONFIG_VERDICT + ? enqueueSub(node) + : el._config & CONFIG_STAGED_ERROR && stageMark(node, el)) + ); visited.add(node); node._time = clock; const remaining = node._x?._pendingSources?.values().next().value; @@ -1006,9 +1038,14 @@ export function propagateStatus(el: Computed, status: number, error: any): // it is served the committed value it already derived from (A10) and // asks that question again. It waits on the source like any derivation // (F-9), and asks its next question once, at the landing (#4397). + // A node carrying a staged error re-derives on a pending mark: its + // source asks again, and the staging it carried is not the answer + // (#4109, `stageMark`). if ( sub._config & CONFIG_REDERIVE || - (holdWorkHook && sub._config & CONFIG_VERDICT && !(pendingSource && holdWorkHook(sub, el))) || + (holdWorkHook && + (sub._config & CONFIG_VERDICT || (pendingSource && sub._config & CONFIG_STAGED_ERROR)) && + !(pendingSource && holdWorkHook(sub, el))) || (status === STATUS_PENDING && link._gen !== sub._depGen) ) { enqueueSub(sub); diff --git a/packages/signals/tests/staged-error-derivation.test.ts b/packages/signals/tests/staged-error-derivation.test.ts new file mode 100644 index 0000000000..5ef2dade2a --- /dev/null +++ b/packages/signals/tests/staged-error-derivation.test.ts @@ -0,0 +1,156 @@ +/** + * D-41: "A derivation that reads a staged error throws it, as it would read + * a staged value: its own pass errors and stages with the same frame." Found + * by the async-gen rerun on next 5e68925c (E2, E3): a memo whose held pass + * staged an error did not reach its derivations — an identity memo over it + * kept the old value for `latest()` and read `isPending() === false` for the + * rest of the hold, while the memo itself threw the proposed error from + * `latest()` and read pending (D-22, D-23 iii). A staged error now marks its + * derivations staged with it, as a published error marks them errored. + */ +import { afterEach, expect, it } from "vitest"; +import { + action, + createEffect, + createErrorBoundary, + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush, + isPending, + latest, + resetErrorHalt +} from "../src/index.js"; + +afterEach(() => resetErrorHalt()); + +const tick = async () => { + await new Promise(r => setTimeout(r, 0)); + flush(); +}; +const attempt = (fn: () => unknown) => { + try { + return fn(); + } catch (e: any) { + return `throws ${e?.cause?.message ?? e?.message}`; + } +}; + +for (const identity of [false, true]) + it(`latest() of a derivation throws the error a held pass staged${identity ? " (identity memo)" : ""}`, async () => { + const [s, setS] = createSignal(2); + let m!: () => number; + let screen: unknown; + let release!: () => void; + const dispose = createRoot(d => { + const m0 = createMemo(() => { + const v = s() + 1; + if (v % 4 === 1) throw new Error(`m0:${v}`); + return v; + }); + m = identity ? createMemo(() => m0()) : m0; + const shown = createErrorBoundary( + () => latest(m), + e => `E:${(e() as Error).message}` + ); + createRenderEffect(shown, v => void (screen = v)); + return d; + }); + flush(); + expect(screen).toBe(3); + const act = action(function* () { + setS(0); + yield new Promise(r => (release = r)); + }); + act(); + await tick(); + // The hold is open: the screen's value is committed, the proposal is + // the error. + expect([attempt(() => latest(m)), isPending(m), attempt(() => m())]).toEqual([ + "throws m0:1", + true, + 3 + ]); + release(); + await tick(); + await tick(); + expect([attempt(() => m()), isPending(m)]).toEqual(["throws m0:1", false]); + dispose(); + }); + +it("isPending of a derivation over an errored memo whose held re-run errors again is true", async () => { + // m0 = s + 1 (throws at 2 mod 4), m1 = m0 + m0 + 1 (throws at 3 mod 4): + // errored at s = 0 (m1:3) and again at s = 2 (m1:7). + const [s, setS] = createSignal(0); + let m1!: () => number; + let id!: () => number; + const seen: boolean[] = []; + const dispose = createRoot(d => { + const m0 = createMemo(() => { + const v = s() + 1; + if (v % 4 === 2) throw new Error(`m0:${v}`); + return v; + }); + m1 = createMemo(() => { + const v = m0() + m0() + 1; + if (v % 4 === 3) throw new Error(`m1:${v}`); + return v; + }); + id = createMemo(() => m1()); + createRenderEffect( + () => isPending(id), + v => void seen.push(v) + ); + return d; + }); + flush(); + const act = action(function* () { + setS(2); + yield new Promise(() => {}); + }); + act(); + await tick(); + expect([isPending(m1), isPending(id), seen.at(-1)]).toEqual([true, true, true]); + dispose(); +}); + +it("a derivation's staged error is superseded when its source re-runs to a pending value in the same flush (#4109)", async () => { + const log: unknown[] = []; + const [s, setS] = createSignal(0); + const [fix, setFix] = createSignal(false, { ownedWrite: true }); + let m!: () => number; + const dispose = createRoot(d => { + const m0 = createMemo(() => { + const v = s(); + if (v === 1 && !fix()) throw new Error("boom"); + return v === 0 ? 0 : Promise.resolve(v * 10); + }); + m = createMemo(() => m0()); + // Taller than `m0`: seeing s === 1 it writes `fix`, so `m0` re-runs in + // the same flush, after its throw was staged and marked onto `m`. + const a = createMemo(() => s() + 1); + const b = createMemo(() => { + const x = a(); + if (x === 2) setFix(true); + return x; + }); + createEffect(m, { + effect: v => void log.push(["value", v]), + error: e => void log.push(["error", (e as any)?.cause?.message ?? (e as any)?.message]) + }); + createRenderEffect(b, () => {}); + return d; + }); + flush(); + setS(1); + flush(); + await tick(); + await tick(); + expect(log).toEqual([ + ["value", 0], + ["value", 10] + ]); + expect(m()).toBe(10); + dispose(); +}); diff --git a/packages/web/test/async-gen/e2-staged-error-derivation.spec.ts b/packages/web/test/async-gen/e2-staged-error-derivation.spec.ts new file mode 100644 index 0000000000..b091e327d2 --- /dev/null +++ b/packages/web/test/async-gen/e2-staged-error-derivation.spec.ts @@ -0,0 +1,66 @@ +/** + * @vitest-environment jsdom + */ +// async-gen rerun on next 5e68925c, seed 3289 (E2): reading every memo +// through an identity memo shows the same screens (D-41: a derivation of a +// staged error carries it, staged with the same frame). +// - The action makes m0 throw: `latest(m0)` throws the proposed error, so the +// leaf errors to [ROOT-E] (D-22); through the identity memo it showed the +// old `lm0:3`. +// - An identity memo over an errored m1 whose held re-run errors again: +// `isPending` is true for both (D-23 iii); through the identity it was false. +import { expect, test } from "vitest"; +import { runScenario } from "./run.js"; + +const latestThrows = { + signals: [1, 2], + memos: [{ async: false, deps: [{ src: { k: "s", i: 1 }, how: "plain" }], poison: 1 }], + tree: { + t: "show", + when: { how: "plain", src: { k: "s", i: 1 } }, + child: { + t: "leaf", + text: [], + attrs: [{ how: "latest", src: { k: "m", i: 0 } }], + grouped: true + }, + fallback: false + }, + steps: [{ t: "action", s: 1, v: 0 }, { t: "task" }] +} as any; + +const pendingOverError = { + signals: [0], + memos: [ + { async: false, deps: [{ src: { k: "s", i: 0 }, how: "plain" }], poison: 2 }, + { + async: false, + deps: [ + { src: { k: "m", i: 0 }, how: "plain" }, + { src: { k: "m", i: 0 }, how: "plain" } + ], + poison: 3 + } + ], + tree: { + t: "leaf", + text: [], + attrs: [{ how: "pending", src: { k: "m", i: 1 } }], + grouped: true + }, + steps: [{ t: "action", s: 0, v: 2 }, { t: "task" }] +} as any; + +test("minimized: latest() of a memo the action makes throw errors through an identity memo too", async () => { + const direct = await runScenario(latestThrows, {}); + const identity = await runScenario(latestThrows, { identity: true }); + expect(identity.screens).toEqual(direct.screens); + expect(identity.screens.at(-1)).toBe("[ROOT-E]"); +}); + +test("minimized: isPending of an identity memo over a held re-run's new error is true", async () => { + const direct = await runScenario(pendingOverError, {}); + const identity = await runScenario(pendingOverError, { identity: true }); + expect(identity.screens).toEqual(direct.screens); + expect(identity.screens.at(-1)).toBe(""); +}); From 6b3e9f33d673aca9a37162d366312ed7d4002496 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 10:36:48 +0000 Subject: [PATCH 02/10] chore(size): raise caps for the staged-error derivation marks The marks run with the verdict engine alone: +109 B minified on the isPending/latest bundles, 0 B on the floors. Raised the non-frozen caps that crossed at measured + 10 B rounded up to 0.01 KB, recorded minified = measured (isPending/latest, the compiled live page; the observe tier with attribution is brotli layout only, +0 B minified). The live page's frozen floor stays under its cap. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- scripts/size/scenarios.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index ff93f301a5..553e257cc4 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1438,9 +1438,10 @@ module.exports = [ // (30,408 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): 11.04 KB -> 11.07 KB, recorded minified 30,408 -> 30,492 B (+84 B), measured locally (linux) at 11,057 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. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "11.07 KB", + // A staged error reaches the memos that derive from it (D-41, async-gen rerun on 5e68925c) (2026-10-30, over next @ 7135a77a): 11.07 -> 11.11 KB, recorded minified 30,492 -> 30,601 B (+109 B), measured locally (linux) at 11,100 B brotli. The settle walk marks the derivations of a staged error (`stageMark`) and a pending mark re-derives one, with the verdict engine alone; the floors carry none of it. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "11.11 KB", // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 28,693 -> 29,086 B (+343 B minified over next), measured locally (linux) at 10,565 B brotli (+135 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 30492, + capMinified: 30601, alias }, { @@ -3890,7 +3891,8 @@ module.exports = [ // 29.74 KB, recorded minified 88,302 -> 88,303 B; CI-measured at 29,727 B // (88,303 B minified). Lower only: cap at measured + 10 B rounded up to // 0.01 KB; recorded minified never raised. - limit: "29.74 KB", + // A staged error reaches the memos that derive from it (D-41, async-gen rerun on 5e68925c) (2026-10-30, over next @ 7135a77a): 29.74 -> 29.76 KB, recorded minified 88,303 -> 88,303 B (+0 B), measured locally (linux) at 29,747 B brotli. No minified change here: brotli layout. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "29.76 KB", capMinified: 88303, alias: observeAlias }, @@ -5904,11 +5906,12 @@ module.exports = [ // Re-record after the 2026-10-29 afternoon batch (#4418, #4414; 2026-10-29, over next @ 5d57e12a): 42.62 KB -> 42.65 KB, recorded minified 125,165 -> 125,266 B (+101 B), measured in CI at 42,640 B brotli. Growth is #4414 (a truncated fragment's fresh render computes what the server never sent: the truncation sweep deletes unsettled records at DOMContentLoaded, +~100 B minified in solid's hydration client); the maintainer said merge (2026-10-29). Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Batch re-record (2026-10-30, next @ 32163a9, size job 123956613767): cap unchanged, recorded minified 125,266 -> 125,285 B, measured in CI at 42,636 B brotli. Growth is the 2026-10-30 merges (#4424, #4436, #4416, #4442); #4416's truncation sweep is the +19 B minified on the hydrating scenarios. Cap at measured + 10 B rounded up to 0.01 KB where over; recorded minified = measured. // A held memo's parked reader re-derives at the seam (#4439, 2026-10-30, over next @ b249ad96): 42.65 KB -> 42.67 KB, recorded minified 125,285 -> 125,374 B (+89 B), measured locally (linux) at 42,655 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. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "42.67 KB", + // A staged error reaches the memos that derive from it (D-41, async-gen rerun on 5e68925c) (2026-10-30, over next @ 7135a77a): 42.67 -> 42.72 KB, recorded minified 125,374 -> 125,483 B (+109 B), measured locally (linux) at 42,701 B brotli. The settle walk marks the derivations of a staged error (`stageMark`) and a pending mark re-derives one, with the verdict engine alone; the floors carry none of it. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "42.72 KB", // An Errored heals when the last reader that caught its error is disposed (#4373, #4384, 2026-10-28, re-recorded over next @ cbc0eebd): recorded minified 123,191 -> 123,249 B (+58 B minified over next's record), measured locally (linux) at 41,899 B brotli. Recorded minified = measured; CI re-bases. // Boundary readers keyed by the source they wait on (#4373), with enterLane's tail folded into one else-chain as an offset (-29 B on lane pages) (#4384, 2026-10-28, re-recorded over next @ 1b32ff2e): recorded minified 123,203 -> 123,224 B (+21 B minified over next's record), measured locally (linux) at 41,966 B brotli. Recorded minified = measured; CI re-bases. // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 123,224 -> 123,733 B (+403 B minified over next), measured locally (linux) at 42,119 B brotli (+169 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 125374, + capMinified: 125483, alias: pageAlias }, { From 5781932e048964fd6d0a44a330d50a86c6faed1c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 11:12:05 +0000 Subject: [PATCH 03/10] fix(signals): a staged error reaches a derived store's readers A derived store over a memo whose error is staged with a hold now shows the staging to its readers: latest() of the store throws the proposed error and isPending() of it is true until the hold commits, as for a plain memo (D-41). The derive is marked like any other derivation and its family is told the error status, so the family's readers re-derive from it (#4189). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- .../staged-error-reaches-derivations.md | 2 +- packages/signals/src/core/async.ts | 13 ++-- .../async-gen/e3-store-staged-error.spec.ts | 62 +++++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 packages/web/test/async-gen/e3-store-staged-error.spec.ts diff --git a/.changeset/staged-error-reaches-derivations.md b/.changeset/staged-error-reaches-derivations.md index a0b391d76a..42c070c2eb 100644 --- a/.changeset/staged-error-reaches-derivations.md +++ b/.changeset/staged-error-reaches-derivations.md @@ -2,4 +2,4 @@ "@solidjs/signals": patch --- -A memo's error staged with a hold now reaches the memos that derive from it: `latest()` of them throws the proposed error and `isPending()` of them is `true` until the hold commits, as for the memo itself (D-41). +A memo's error staged with a hold now reaches the memos and derived stores that derive from it: `latest()` of them throws the proposed error and `isPending()` of them is `true` until the hold commits, as for the memo itself (D-41). diff --git a/packages/signals/src/core/async.ts b/packages/signals/src/core/async.ts index befc426190..b8e0402ac5 100644 --- a/packages/signals/src/core/async.ts +++ b/packages/signals/src/core/async.ts @@ -227,21 +227,22 @@ export function stageError(el: Computed, error: any, inPass?: boolean): boo * re-derive (the settle walk). Only a verdict reads a derivation's staging * before its commit, so only the verdict engine marks. Not a boundary's * output, an async memo, an effect or a boundary's tree (`EFFECT_TRACKED << - * 23` spans CONFIG_FRAME and every effect type), one already carrying a - * staged error, nor a derived store's derive, whose readers learn its status - * through its family (#4189): the commit's publish reaches them. A pending - * mark supersedes the staging (`propagateStatus`). */ + * 23` spans CONFIG_FRAME and every effect type), nor one already carrying a + * staged error: the commit's publish reaches them. A pending mark supersedes + * the staging (`propagateStatus`). */ function stageMark(node: Computed, el: Computed): void { if ( !( node._config & - (CONFIG_REDERIVE | CONFIG_STAGED_ERROR | CONFIG_ASYNC | (EFFECT_TRACKED << 23)) || - (node as any).fam + (CONFIG_REDERIVE | CONFIG_STAGED_ERROR | CONFIG_ASYNC | (EFFECT_TRACKED << 23)) ) ) { node._pendingValue = el._pendingValue; node._config |= CONFIG_STAGED_ERROR; queuePendingNode(node); + // A derived store's readers are its family's (store/projection.ts): they + // re-derive from the staging, as from the derive's status (#4189). + (node as any).fam?.st(STATUS_ERROR); settlePendingSource(node); } } diff --git a/packages/web/test/async-gen/e3-store-staged-error.spec.ts b/packages/web/test/async-gen/e3-store-staged-error.spec.ts new file mode 100644 index 0000000000..3d100e0d8b --- /dev/null +++ b/packages/web/test/async-gen/e3-store-staged-error.spec.ts @@ -0,0 +1,62 @@ +/** + * @vitest-environment jsdom + */ +// async-gen rerun on rc.20, E3 (a derived store `createStore(d => { d.v = m() })` +// reads like `m`): an action makes the derive's source stage an error. The +// staged error reaches a memo's derivations (D-41), but a derive was skipped: +// its leaves' readers learn its status through its family, and nothing told +// the family. `latest(store.v)` kept the old value where `latest(m)` throws, +// and `isPending` over the store read false where over a memo it reads true. +import { expect, test } from "vitest"; +import { runScenario } from "./run.js"; + +const cases: Record = { + // [s0=1 s1=2 m0=memo(s1 !%4=1)] :: A(s1=0) t + "latest of the store throws the staged error": { + signals: [1, 2], + memos: [{ async: false, deps: [{ src: { k: "s", i: 1 }, how: "plain" }], poison: 1 }], + tree: { + t: "show", + when: { how: "plain", src: { k: "s", i: 1 } }, + child: { + t: "leaf", + text: [], + attrs: [{ how: "latest", src: { k: "m", i: 0 } }], + grouped: true + }, + fallback: false + }, + steps: [{ t: "action", s: 1, v: 0 }, { t: "task" }] + }, + // [s0=0 m0=memo(s0 !%4=2) m1=memo(m0+m0 !%4=3)] :: A(s0=2) t + "isPending of a store over the staged error is true": { + signals: [0], + memos: [ + { async: false, deps: [{ src: { k: "s", i: 0 }, how: "plain" }], poison: 2 }, + { + async: false, + deps: [ + { src: { k: "m", i: 0 }, how: "plain" }, + { src: { k: "m", i: 0 }, how: "plain" } + ], + poison: 3 + } + ], + tree: { + t: "leaf", + text: [], + attrs: [{ how: "pending", src: { k: "m", i: 1 } }], + grouped: true + }, + steps: [{ t: "action", s: 0, v: 2 }, { t: "task" }] + } +}; + +for (const [name, sc] of Object.entries(cases)) + test(`E3: ${name}`, async () => { + const memo = await runScenario(sc, {}); + const store = await runScenario(sc, { store: true }); + expect(store.screens).toEqual(memo.screens); + expect(store.final).toBe(memo.final); + expect(store.findings).toEqual([]); + }); From 4fce711dcd75437d147b12d0e9a305f0b7847fa5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 11:15:05 +0000 Subject: [PATCH 04/10] chore(size): raise the live page caps for the derived-store marks The derived-store family wake of a staged error adds 6 B minified over the memo marks; on the live server-component page it lands at 38,799 B brotli, past the frozen floor. Raised the live page's frozen floor to 38.81 KB (recorded minified 117,343) and live + router to 48.45 KB (recorded minified 142,747), at measured + 10 B rounded up to 0.01 KB. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- scripts/size/floor-caps.json | 4 ++-- scripts/size/scenarios.js | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index 8cc82c36de..5e26a4aca7 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -16,8 +16,8 @@ "minified": 101605 }, "page: live server components (base + live/GET + action + isPending/latest)": { - "cap": "38.76 KB", - "minified": 117227 + "cap": "38.81 KB", + "minified": 117343 }, "server: floor (getRequestEvent + isServer)": { "cap": "1.29 KB", diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 553e257cc4..541be7a868 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -5594,6 +5594,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 staged error reaches derived stores (#4456, 2026-10-30, over next @ 7135a77a): frozen floor 38.76 KB -> 38.81 KB, recorded minified 117,227 -> 117,343 B (+116 B), measured locally (linux) at 38,799 B brotli. The derive of a derived store is marked like any derivation of a staged error and its family is told the error status (`fam.st`), so the store's readers see the staging (D-41); +6 B minified over the memo marks, the rest brotli layout. Correctness; Size-Exception in #4456. 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)"], @@ -6308,11 +6309,12 @@ module.exports = [ // Re-record after the 2026-10-29 afternoon batch (#4418, #4414; 2026-10-29, over next @ 5d57e12a): 48.31 KB -> 48.31 KB (unchanged), recorded minified 142,427 -> 142,528 B (+101 B), measured in CI at 48,293 B brotli. Growth is #4414 (a truncated fragment's fresh render computes what the server never sent: the truncation sweep deletes unsettled records at DOMContentLoaded, +~100 B minified in solid's hydration client); the maintainer said merge (2026-10-29). Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Batch re-record (2026-10-30, next @ 32163a9, size job 123956613767): 48.31 KB -> 48.38 KB, recorded minified 142,528 -> 142,547 B, measured in CI at 48,368 B brotli. Growth is the 2026-10-30 merges (#4424, #4436, #4416, #4442); #4416's truncation sweep is the +19 B minified on the hydrating scenarios. Cap at measured + 10 B rounded up to 0.01 KB where over; recorded minified = measured. // A held memo's parked reader re-derives at the seam (#4439, 2026-10-30, over next @ b249ad96): 48.38 KB -> 48.42 KB, recorded minified 142,547 -> 142,631 B (+84 B), measured locally (linux) at 48,402 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. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "48.42 KB", + // A staged error reaches derived stores (#4456, 2026-10-30, over next @ 7135a77a): 48.42 KB -> 48.45 KB, recorded minified 142,631 -> 142,747 B (+116 B), measured locally (linux) at 48,436 B brotli. The memo marks and the derived-store family wake of a staged error (D-41); see the live page note. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "48.45 KB", // An Errored heals when the last reader that caught its error is disposed (#4373, #4384, 2026-10-28, re-recorded over next @ cbc0eebd): recorded minified 140,451 -> 140,509 B (+58 B minified over next's record), measured locally (linux) at 47,516 B brotli. Recorded minified = measured; CI re-bases. // Boundary readers keyed by the source they wait on (#4373), with enterLane's tail folded into one else-chain as an offset (-29 B on lane pages) (#4384, 2026-10-28, re-recorded over next @ 1b32ff2e): recorded minified 140,463 -> 140,485 B (+22 B minified over next's record), measured locally (linux) at 47,547 B brotli. Recorded minified = measured; CI re-bases. // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 140,485 -> 140,983 B (+405 B minified over next), measured locally (linux) at 47,722 B brotli (+162 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 142631, + capMinified: 142747, alias: pageAlias, conditions: solidConditions, compile: { hydratable: true } From b8af6b9cbd5ff07853d01d9649c8a53e0d538b5c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 13:30:28 +0000 Subject: [PATCH 05/10] fix(signals): a derivation of a staged error re-runs instead of carrying it D-41: a derivation that reads a staged error throws it, as it would read a staged value: its own pass errors and stages with the same frame. The earlier change marked every derivation under a staged error with that error without running it, so a memo or derived-store derive that catches the error showed an error it never threw for the whole hold, and a derived store's keys that never read the source threw it too (D-30). Read only through latest(), both ended errored after the commit. The settle walk now re-runs those derivations; a pass that catches answers with its own value. A boundary's output and tree, async memos, effects and nodes that own children (a boundary's content, #3945) are left to the commit's publish as before. A projection's derive stages its pass's error like a memo. A published error no longer marks a node that holds its own pass's answer to the frame, which also fixes a catching memo ending errored when the error's flush has no hold. The silent-recovery pin counts the dependent's own errored pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- .../staged-error-reaches-derivations.md | 2 +- packages/signals/src/core/async.ts | 63 +++++----- .../tests/error-silent-recovery.test.ts | 5 +- .../staged-error-catching-derivation.test.ts | 109 ++++++++++++++++++ ...rror-derived-store-catching-derive.test.ts | 98 ++++++++++++++++ 5 files changed, 240 insertions(+), 37 deletions(-) create mode 100644 packages/signals/tests/staged-error-catching-derivation.test.ts create mode 100644 packages/signals/tests/staged-error-derived-store-catching-derive.test.ts diff --git a/.changeset/staged-error-reaches-derivations.md b/.changeset/staged-error-reaches-derivations.md index 42c070c2eb..2d4ae44c2d 100644 --- a/.changeset/staged-error-reaches-derivations.md +++ b/.changeset/staged-error-reaches-derivations.md @@ -2,4 +2,4 @@ "@solidjs/signals": patch --- -A memo's error staged with a hold now reaches the memos and derived stores that derive from it: `latest()` of them throws the proposed error and `isPending()` of them is `true` until the hold commits, as for the memo itself (D-41). +A memo's error staged with a hold now reaches the memos and derived stores that derive from it: they re-run against it, so `latest()` of them throws the proposed error and `isPending()` of them is `true` until the hold commits, as for the memo itself (D-41). A memo or derived-store derive that catches the error answers with its own value instead, during the hold and after it: it no longer ends errored with an error its pass never threw, and a derived store's other keys keep their values (D-30). diff --git a/packages/signals/src/core/async.ts b/packages/signals/src/core/async.ts index b8e0402ac5..9c0b4c82a4 100644 --- a/packages/signals/src/core/async.ts +++ b/packages/signals/src/core/async.ts @@ -195,12 +195,13 @@ export function stageError(el: Computed, error: any, inPass?: boolean): boo // Staged whether or not the flush has joined a hold yet: a pass that runs // before its async sibling joins is the seam's to decide, like a value // (#4109). A flush that parks nothing publishes it at its commit (S-38). - // Not a projection's derive: its readers learn its error through the - // family, which the commit's publish never reaches (#4189). + // A projection's derive too (D-41): its pass's throw has already sent the + // family's readers to re-derive (store/projection.ts `errorFamily`), and + // they derive from the staging like a memo's readers (#4189). if ( !( el._config & CONFIG_HELD || - (inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running && !(el as any).fam) + (inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running) ) || passLane ) @@ -215,38 +216,12 @@ export function stageError(el: Computed, error: any, inPass?: boolean): boo el._config |= CONFIG_STAGED | CONFIG_STAGED_ERROR; // Readers parked on the flight re-run and derive from the staging; the // verdict readers the walk wakes need a flush (#4106); the derivations it - // reaches carry the staging (`settlePendingSource`). + // reaches derive from it (`settlePendingSource`). settlePendingSource(el); schedule(); return true; } -/** D-41: a derivation of a staged error carries it, staged with the same - * frame — marked, not re-run, as a published error marks it (a re-run would - * replace the frame its content owns) — and its own verdict readers - * re-derive (the settle walk). Only a verdict reads a derivation's staging - * before its commit, so only the verdict engine marks. Not a boundary's - * output, an async memo, an effect or a boundary's tree (`EFFECT_TRACKED << - * 23` spans CONFIG_FRAME and every effect type), nor one already carrying a - * staged error: the commit's publish reaches them. A pending mark supersedes - * the staging (`propagateStatus`). */ -function stageMark(node: Computed, el: Computed): void { - if ( - !( - node._config & - (CONFIG_REDERIVE | CONFIG_STAGED_ERROR | CONFIG_ASYNC | (EFFECT_TRACKED << 23)) - ) - ) { - node._pendingValue = el._pendingValue; - node._config |= CONFIG_STAGED_ERROR; - queuePendingNode(node); - // A derived store's readers are its family's (store/projection.ts): they - // re-derive from the staging, as from the derive's status (#4189). - (node as any).fam?.st(STATUS_ERROR); - settlePendingSource(node); - } -} - export function settleErroredDependents(el: Computed, error: any): void { let scheduled = false; const visited = new NewSet>(); @@ -345,12 +320,24 @@ export function settlePendingSource(el: Computed, source: Computed = e // equal to the committed value notifies nobody — the source settling // is its verdict changing: it runs again. (Verdict readers exist only // with the verdict engine installed.) - // D-41: a derivation of a staged error carries it (`stageMark`). + // D-41: a derivation that reads a staged error throws it, as it would + // read a staged value — its own pass errors and stages with the same + // frame. So it re-runs: the pass decides, and one that catches answers + // with its own value (a derived store's derive included: each key is + // its own, D-30). Only a verdict reads a derivation's staging before + // its commit, so only the verdict engine re-runs it. Not a boundary's + // output, an async memo, an effect or a boundary's tree (`EFFECT_TRACKED + // << 23` spans CONFIG_FRAME and every effect type), nor a node that + // owns children (a boundary's content, #3945: a re-run would replace + // the frame they are): the commit's publish reaches them. return ( holdWorkHook && (node._config & CONFIG_VERDICT ? enqueueSub(node) - : el._config & CONFIG_STAGED_ERROR && stageMark(node, el)) + : el._config & CONFIG_STAGED_ERROR && + !(node._config & (CONFIG_REDERIVE | CONFIG_ASYNC | (EFFECT_TRACKED << 23))) && + !node._firstChild && + enqueueSub(node)) ); visited.add(node); node._time = clock; @@ -1040,8 +1027,8 @@ export function propagateStatus(el: Computed, status: number, error: any): // asks that question again. It waits on the source like any derivation // (F-9), and asks its next question once, at the landing (#4397). // A node carrying a staged error re-derives on a pending mark: its - // source asks again, and the staging it carried is not the answer - // (#4109, `stageMark`). + // source asks again, and the staging it re-derived from is not the + // answer (#4109). if ( sub._config & CONFIG_REDERIVE || (holdWorkHook && @@ -1072,7 +1059,13 @@ export function propagateStatus(el: Computed, status: number, error: any): } if ( (status === STATUS_PENDING && pendingSource) || - (status !== STATUS_PENDING && (sub._x?._error !== error || sub._x?._pendingSources)) + // An error does not mark a node holding an answer of its own pass to + // this frame (D-41: it derived from the staging this commit + // publishes): that answer commits with the frame, a caught error a + // value. + (status !== STATUS_PENDING && + sub._pendingValue === NOT_PENDING && + (sub._x?._error !== error || sub._x?._pendingSources)) ) { // The marked subscriber is queued for the commit sweep so its status // bookkeeping (loading window, uninitialized clear) runs with the flush diff --git a/packages/signals/tests/error-silent-recovery.test.ts b/packages/signals/tests/error-silent-recovery.test.ts index 954780aa3d..140971e858 100644 --- a/packages/signals/tests/error-silent-recovery.test.ts +++ b/packages/signals/tests/error-silent-recovery.test.ts @@ -72,13 +72,16 @@ describe("silent error recovery (#2949)", () => { setB(-1); flush(); expect(out()).toBe("err:b=-1"); + // The dependent's own pass meets the error (D-41: a derivation that + // reads a staged error throws it — its own pass errors). + expect(runs()).toBe(runsBefore + 1); setB(0); flush(); expect(out()).toBe("ok:0"); // The sweep re-runs the errored dependent once; it commits an unchanged // value and everything settles. - expect(runs()).toBe(runsBefore + 1); + expect(runs()).toBe(runsBefore + 2); }); test("changed-value recovery still rides normal notification", () => { diff --git a/packages/signals/tests/staged-error-catching-derivation.test.ts b/packages/signals/tests/staged-error-catching-derivation.test.ts new file mode 100644 index 0000000000..0e37fd9ae2 --- /dev/null +++ b/packages/signals/tests/staged-error-catching-derivation.test.ts @@ -0,0 +1,109 @@ +/** + * D-41: a derivation that reads a staged error throws it, as it would read a + * staged value — its own pass errors and stages with the same frame. A memo + * whose pass catches the error does not error: its answer is its own value, + * staged with the frame and committed with it. + * + * `m0` throws for the action's write, which a hold keeps; `m` catches it. + */ +import { afterEach, expect, it } from "vitest"; +import { + action, + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush, + isPending, + latest, + resetErrorHalt +} from "../src/index.js"; + +afterEach(() => resetErrorHalt()); +const tick = async () => { + await new Promise(r => setTimeout(r, 0)); + flush(); +}; +const attempt = (fn: () => unknown) => { + try { + return fn(); + } catch (e: any) { + return `throws ${e?.cause?.message ?? e?.message}`; + } +}; + +function setup(plainReader: boolean) { + const [s, setS] = createSignal(2); + let m!: () => unknown; + const latestScreens: unknown[] = []; + const dispose = createRoot(d => { + const m0 = createMemo(() => { + const v = s() + 1; + if (v % 4 === 1) throw new Error(`m0:${v}`); + return v; + }); + m = createMemo(() => { + try { + return `ok:${m0()}`; + } catch { + return "caught"; + } + }); + createRenderEffect( + () => attempt(() => latest(m)), + v => void latestScreens.push(v) + ); + if (plainReader) + createRenderEffect( + () => [attempt(() => m()), isPending(m)], + () => {} + ); + return d; + }); + flush(); + return { setS, m: () => m, latestScreens, dispose }; +} + +it("a catching memo never shows its source's staged error", async () => { + const { setS, m, latestScreens, dispose } = setup(true); + let release!: () => void; + action(function* () { + setS(0); + yield new Promise(r => (release = r)); + })(); + await tick(); + // Its own pass's answer to the frame, seen early through `latest`. + expect(attempt(() => latest(m()))).toBe("caught"); + expect(attempt(() => m()())).toBe("ok:3"); + release(); + await tick(); + await tick(); + expect(latestScreens).toEqual(["ok:3", "caught"]); + expect(attempt(() => m()())).toBe("caught"); + dispose(); +}); + +it("a catching memo read only through latest() commits its own answer", async () => { + const { setS, m, latestScreens, dispose } = setup(false); + let release!: () => void; + action(function* () { + setS(0); + yield new Promise(r => (release = r)); + })(); + await tick(); + release(); + await tick(); + await tick(); + expect(attempt(() => m()())).toBe("caught"); + expect(latestScreens).toEqual(["ok:3", "caught"]); + dispose(); +}); + +it("a catching memo over an error of a flush with no hold commits its own answer", () => { + const { setS, m, latestScreens, dispose } = setup(false); + setS(0); + flush(); + expect(attempt(() => m()())).toBe("caught"); + expect(latestScreens).toEqual(["ok:3", "caught"]); + dispose(); +}); diff --git a/packages/signals/tests/staged-error-derived-store-catching-derive.test.ts b/packages/signals/tests/staged-error-derived-store-catching-derive.test.ts new file mode 100644 index 0000000000..1663e8c9b5 --- /dev/null +++ b/packages/signals/tests/staged-error-derived-store-catching-derive.test.ts @@ -0,0 +1,98 @@ +/** + * D-41 and D-30: a derived store whose derive catches its source's staged + * error answers with its own pass: no key throws the error, and a key that + * never read the source (`a`) keeps its value — pending is per key. + * + * `d => { d.a = 7; try { d.b = m0() } catch { d.b = "caught" } }`; an action + * makes `m0` throw and a hold keeps the write. + */ +import { afterEach, expect, it } from "vitest"; +import { + action, + createMemo, + createRenderEffect, + createRoot, + createSignal, + createStore, + flush, + latest, + resetErrorHalt +} from "../src/index.js"; + +afterEach(() => resetErrorHalt()); +const tick = async () => { + await new Promise(r => setTimeout(r, 0)); + flush(); +}; +const attempt = (fn: () => unknown) => { + try { + return fn(); + } catch (e: any) { + return `throws ${e?.cause?.message ?? e?.message}`; + } +}; + +function setup(plainReader: boolean) { + const [s, setS] = createSignal(2); + let st!: { a?: number; b?: unknown }; + const dispose = createRoot(d => { + const m0 = createMemo(() => { + const v = s() + 1; + if (v % 4 === 1) throw new Error(`m0:${v}`); + return v; + }); + const [store] = createStore<{ a?: number; b?: unknown }>(dr => { + dr.a = 7; + try { + dr.b = m0(); + } catch { + dr.b = "caught"; + } + }, {}); + st = store; + createRenderEffect( + () => [attempt(() => latest(() => store.a)), attempt(() => latest(() => store.b))], + () => {} + ); + if (plainReader) + createRenderEffect( + () => [attempt(() => store.a), attempt(() => store.b)], + () => {} + ); + return d; + }); + flush(); + return { setS, st: () => st, dispose }; +} + +it("latest() of a key that never read the source does not throw its staged error", async () => { + const { setS, st, dispose } = setup(true); + let release!: () => void; + action(function* () { + setS(0); + yield new Promise(r => (release = r)); + })(); + await tick(); + expect(attempt(() => latest(() => st().a))).toBe(7); + expect(attempt(() => latest(() => st().b))).toBe("caught"); + release(); + await tick(); + await tick(); + expect([attempt(() => st().a), attempt(() => st().b)]).toEqual([7, "caught"]); + dispose(); +}); + +it("a store read only through latest() commits its derive's own answer", async () => { + const { setS, st, dispose } = setup(false); + let release!: () => void; + action(function* () { + setS(0); + yield new Promise(r => (release = r)); + })(); + await tick(); + release(); + await tick(); + await tick(); + expect([attempt(() => st().a), attempt(() => st().b)]).toEqual([7, "caught"]); + dispose(); +}); From ae09a9f2d697ba56e119793f43bef81b07536ebb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 13:39:43 +0000 Subject: [PATCH 06/10] chore(size): raise caps for the staged-error re-runs Over next @ 563b6b0f: the settle walk's re-run of a staged error's derivations is +60 B minified with the verdict engine (isPending/latest 11.10 -> 11.12 KB, recorded minified 30,656); the published-error check is +2 B minified everywhere (createStore 14.95 -> 14.98 KB, recorded minified 43,926, which next already measures at 43,924). Caps at measured + 10 B rounded up to 0.01 KB. Every frozen floor stays under its cap. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- scripts/size/scenarios.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index e233bdebe3..957a450ae1 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1118,8 +1118,9 @@ module.exports = [ // (43,875 B minified). Lower only: cap at measured + 10 B rounded up to // 0.01 KB; recorded minified never raised. // A tick whose writes net to the committed value pends no errored re-run (D-8, async-gen rerun on 5e68925c) (2026-10-30, over next @ 7135a77a): 14.89 -> 14.95 KB, recorded minified 43,875 -> 43,888 B (+13 B), measured locally (linux) at 14,932 B brotli. A signal read whose staging equals its committed value is not a read of the frame's future (`read`): +13 B minified, the rest brotli layout. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "14.95 KB", - capMinified: 43888, + // A staged error reaches derived stores and memos that catch it (#4456, 2026-10-30, over next @ 563b6b0f): 14.95 -> 14.98 KB, recorded minified 43,888 -> 43,926 B (+2 B over next, which measures 43,924 B), measured locally (linux) at 14,963 B brotli. A published error does not mark a node holding its own pass's answer (`propagateStatus`): +2 B minified, the rest brotli layout. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "14.98 KB", + capMinified: 43926, alias }, { @@ -1440,9 +1441,10 @@ module.exports = [ // 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): 11.04 KB -> 11.07 KB, recorded minified 30,408 -> 30,492 B (+84 B), measured locally (linux) at 11,057 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. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A probe beside `latest()` outside any Loading no longer loops on a refetch (F-6, D-23; 2026-10-30, over next @ 7135a77a): 11.07 KB -> 11.10 KB, recorded minified 30,492 -> 30,569 B (+77 B), measured locally (linux) at 11,085 B brotli. `pendingVerdict` remembers the transaction a `latest` read of an unheld flight opened, and a probe in it joins the verdict lane without asking to re-run at its landing (the re-run re-opened it every flush). Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "11.10 KB", + // A staged error reaches derived stores and memos that catch it (#4456, 2026-10-30, over next @ 563b6b0f): 11.10 -> 11.12 KB, recorded minified 30,569 -> 30,656 B (+60 B over next, which measures 30,596 B), measured locally (linux) at 11,105 B brotli. The settle walk re-runs a derivation of a staged error instead of leaving it to the commit (D-41), with the verdict engine alone. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "11.12 KB", // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 28,693 -> 29,086 B (+343 B minified over next), measured locally (linux) at 10,565 B brotli (+135 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 30569, + capMinified: 30656, alias }, { From f9e2ddf37cdc4b5a80cd373cd4a3664a8f458fde Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 14:16:17 +0000 Subject: [PATCH 07/10] Revert "chore(size): raise caps for the staged-error re-runs" This reverts commit ae09a9f2d697ba56e119793f43bef81b07536ebb. --- scripts/size/scenarios.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 8385165e74..c926372c0d 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1442,10 +1442,9 @@ module.exports = [ // A held memo's parked reader re-derives at the seam (#4439, 2026-10-30, over next @ b249ad96): 11.04 KB -> 11.07 KB, recorded minified 30,408 -> 30,492 B (+84 B), measured locally (linux) at 11,057 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. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // A probe beside `latest()` outside any Loading no longer loops on a refetch (F-6, D-23; 2026-10-30, over next @ 7135a77a): 11.07 KB -> 11.10 KB, recorded minified 30,492 -> 30,569 B (+77 B), measured locally (linux) at 11,085 B brotli. `pendingVerdict` remembers the transaction a `latest` read of an unheld flight opened, and a probe in it joins the verdict lane without asking to re-run at its landing (the re-run re-opened it every flush). Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Batch re-record (2026-10-30, over next @ 563b6b0): cap unchanged, recorded minified 30,569 -> 30,596 B (+27 B), measured locally (linux) at 11,092 B brotli. Growth is the 2026-10-30 merges (#4454, #4457, #4461). Recorded minified = measured. - // A staged error reaches derived stores and memos that catch it (#4456, 2026-10-30, over next @ cddaa76): 11.10 -> 11.12 KB, recorded minified 30,596 -> 30,656 B (+60 B), measured locally (linux) at 11,105 B brotli. The settle walk re-runs a derivation of a staged error instead of leaving it to the commit (D-41), with the verdict engine alone. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "11.12 KB", + limit: "11.10 KB", // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 28,693 -> 29,086 B (+343 B minified over next), measured locally (linux) at 10,565 B brotli (+135 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 30656, + capMinified: 30596, alias }, { From 42150e3da6991322dfd70cb7d2604804109dfd0e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 14:16:19 +0000 Subject: [PATCH 08/10] Revert "fix(signals): a derivation of a staged error re-runs instead of carrying it" This reverts commit b8af6b9cbd5ff07853d01d9649c8a53e0d538b5c. --- .../staged-error-reaches-derivations.md | 2 +- packages/signals/src/core/async.ts | 63 +++++----- .../tests/error-silent-recovery.test.ts | 5 +- .../staged-error-catching-derivation.test.ts | 109 ------------------ ...rror-derived-store-catching-derive.test.ts | 98 ---------------- 5 files changed, 37 insertions(+), 240 deletions(-) delete mode 100644 packages/signals/tests/staged-error-catching-derivation.test.ts delete mode 100644 packages/signals/tests/staged-error-derived-store-catching-derive.test.ts diff --git a/.changeset/staged-error-reaches-derivations.md b/.changeset/staged-error-reaches-derivations.md index 2d4ae44c2d..42c070c2eb 100644 --- a/.changeset/staged-error-reaches-derivations.md +++ b/.changeset/staged-error-reaches-derivations.md @@ -2,4 +2,4 @@ "@solidjs/signals": patch --- -A memo's error staged with a hold now reaches the memos and derived stores that derive from it: they re-run against it, so `latest()` of them throws the proposed error and `isPending()` of them is `true` until the hold commits, as for the memo itself (D-41). A memo or derived-store derive that catches the error answers with its own value instead, during the hold and after it: it no longer ends errored with an error its pass never threw, and a derived store's other keys keep their values (D-30). +A memo's error staged with a hold now reaches the memos and derived stores that derive from it: `latest()` of them throws the proposed error and `isPending()` of them is `true` until the hold commits, as for the memo itself (D-41). diff --git a/packages/signals/src/core/async.ts b/packages/signals/src/core/async.ts index 9c0b4c82a4..b8e0402ac5 100644 --- a/packages/signals/src/core/async.ts +++ b/packages/signals/src/core/async.ts @@ -195,13 +195,12 @@ export function stageError(el: Computed, error: any, inPass?: boolean): boo // Staged whether or not the flush has joined a hold yet: a pass that runs // before its async sibling joins is the seam's to decide, like a value // (#4109). A flush that parks nothing publishes it at its commit (S-38). - // A projection's derive too (D-41): its pass's throw has already sent the - // family's readers to re-derive (store/projection.ts `errorFamily`), and - // they derive from the staging like a memo's readers (#4189). + // Not a projection's derive: its readers learn its error through the + // family, which the commit's publish never reaches (#4189). if ( !( el._config & CONFIG_HELD || - (inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running) + (inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running && !(el as any).fam) ) || passLane ) @@ -216,12 +215,38 @@ export function stageError(el: Computed, error: any, inPass?: boolean): boo el._config |= CONFIG_STAGED | CONFIG_STAGED_ERROR; // Readers parked on the flight re-run and derive from the staging; the // verdict readers the walk wakes need a flush (#4106); the derivations it - // reaches derive from it (`settlePendingSource`). + // reaches carry the staging (`settlePendingSource`). settlePendingSource(el); schedule(); return true; } +/** D-41: a derivation of a staged error carries it, staged with the same + * frame — marked, not re-run, as a published error marks it (a re-run would + * replace the frame its content owns) — and its own verdict readers + * re-derive (the settle walk). Only a verdict reads a derivation's staging + * before its commit, so only the verdict engine marks. Not a boundary's + * output, an async memo, an effect or a boundary's tree (`EFFECT_TRACKED << + * 23` spans CONFIG_FRAME and every effect type), nor one already carrying a + * staged error: the commit's publish reaches them. A pending mark supersedes + * the staging (`propagateStatus`). */ +function stageMark(node: Computed, el: Computed): void { + if ( + !( + node._config & + (CONFIG_REDERIVE | CONFIG_STAGED_ERROR | CONFIG_ASYNC | (EFFECT_TRACKED << 23)) + ) + ) { + node._pendingValue = el._pendingValue; + node._config |= CONFIG_STAGED_ERROR; + queuePendingNode(node); + // A derived store's readers are its family's (store/projection.ts): they + // re-derive from the staging, as from the derive's status (#4189). + (node as any).fam?.st(STATUS_ERROR); + settlePendingSource(node); + } +} + export function settleErroredDependents(el: Computed, error: any): void { let scheduled = false; const visited = new NewSet>(); @@ -320,24 +345,12 @@ export function settlePendingSource(el: Computed, source: Computed = e // equal to the committed value notifies nobody — the source settling // is its verdict changing: it runs again. (Verdict readers exist only // with the verdict engine installed.) - // D-41: a derivation that reads a staged error throws it, as it would - // read a staged value — its own pass errors and stages with the same - // frame. So it re-runs: the pass decides, and one that catches answers - // with its own value (a derived store's derive included: each key is - // its own, D-30). Only a verdict reads a derivation's staging before - // its commit, so only the verdict engine re-runs it. Not a boundary's - // output, an async memo, an effect or a boundary's tree (`EFFECT_TRACKED - // << 23` spans CONFIG_FRAME and every effect type), nor a node that - // owns children (a boundary's content, #3945: a re-run would replace - // the frame they are): the commit's publish reaches them. + // D-41: a derivation of a staged error carries it (`stageMark`). return ( holdWorkHook && (node._config & CONFIG_VERDICT ? enqueueSub(node) - : el._config & CONFIG_STAGED_ERROR && - !(node._config & (CONFIG_REDERIVE | CONFIG_ASYNC | (EFFECT_TRACKED << 23))) && - !node._firstChild && - enqueueSub(node)) + : el._config & CONFIG_STAGED_ERROR && stageMark(node, el)) ); visited.add(node); node._time = clock; @@ -1027,8 +1040,8 @@ export function propagateStatus(el: Computed, status: number, error: any): // asks that question again. It waits on the source like any derivation // (F-9), and asks its next question once, at the landing (#4397). // A node carrying a staged error re-derives on a pending mark: its - // source asks again, and the staging it re-derived from is not the - // answer (#4109). + // source asks again, and the staging it carried is not the answer + // (#4109, `stageMark`). if ( sub._config & CONFIG_REDERIVE || (holdWorkHook && @@ -1059,13 +1072,7 @@ export function propagateStatus(el: Computed, status: number, error: any): } if ( (status === STATUS_PENDING && pendingSource) || - // An error does not mark a node holding an answer of its own pass to - // this frame (D-41: it derived from the staging this commit - // publishes): that answer commits with the frame, a caught error a - // value. - (status !== STATUS_PENDING && - sub._pendingValue === NOT_PENDING && - (sub._x?._error !== error || sub._x?._pendingSources)) + (status !== STATUS_PENDING && (sub._x?._error !== error || sub._x?._pendingSources)) ) { // The marked subscriber is queued for the commit sweep so its status // bookkeeping (loading window, uninitialized clear) runs with the flush diff --git a/packages/signals/tests/error-silent-recovery.test.ts b/packages/signals/tests/error-silent-recovery.test.ts index 140971e858..954780aa3d 100644 --- a/packages/signals/tests/error-silent-recovery.test.ts +++ b/packages/signals/tests/error-silent-recovery.test.ts @@ -72,16 +72,13 @@ describe("silent error recovery (#2949)", () => { setB(-1); flush(); expect(out()).toBe("err:b=-1"); - // The dependent's own pass meets the error (D-41: a derivation that - // reads a staged error throws it — its own pass errors). - expect(runs()).toBe(runsBefore + 1); setB(0); flush(); expect(out()).toBe("ok:0"); // The sweep re-runs the errored dependent once; it commits an unchanged // value and everything settles. - expect(runs()).toBe(runsBefore + 2); + expect(runs()).toBe(runsBefore + 1); }); test("changed-value recovery still rides normal notification", () => { diff --git a/packages/signals/tests/staged-error-catching-derivation.test.ts b/packages/signals/tests/staged-error-catching-derivation.test.ts deleted file mode 100644 index 0e37fd9ae2..0000000000 --- a/packages/signals/tests/staged-error-catching-derivation.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * D-41: a derivation that reads a staged error throws it, as it would read a - * staged value — its own pass errors and stages with the same frame. A memo - * whose pass catches the error does not error: its answer is its own value, - * staged with the frame and committed with it. - * - * `m0` throws for the action's write, which a hold keeps; `m` catches it. - */ -import { afterEach, expect, it } from "vitest"; -import { - action, - createMemo, - createRenderEffect, - createRoot, - createSignal, - flush, - isPending, - latest, - resetErrorHalt -} from "../src/index.js"; - -afterEach(() => resetErrorHalt()); -const tick = async () => { - await new Promise(r => setTimeout(r, 0)); - flush(); -}; -const attempt = (fn: () => unknown) => { - try { - return fn(); - } catch (e: any) { - return `throws ${e?.cause?.message ?? e?.message}`; - } -}; - -function setup(plainReader: boolean) { - const [s, setS] = createSignal(2); - let m!: () => unknown; - const latestScreens: unknown[] = []; - const dispose = createRoot(d => { - const m0 = createMemo(() => { - const v = s() + 1; - if (v % 4 === 1) throw new Error(`m0:${v}`); - return v; - }); - m = createMemo(() => { - try { - return `ok:${m0()}`; - } catch { - return "caught"; - } - }); - createRenderEffect( - () => attempt(() => latest(m)), - v => void latestScreens.push(v) - ); - if (plainReader) - createRenderEffect( - () => [attempt(() => m()), isPending(m)], - () => {} - ); - return d; - }); - flush(); - return { setS, m: () => m, latestScreens, dispose }; -} - -it("a catching memo never shows its source's staged error", async () => { - const { setS, m, latestScreens, dispose } = setup(true); - let release!: () => void; - action(function* () { - setS(0); - yield new Promise(r => (release = r)); - })(); - await tick(); - // Its own pass's answer to the frame, seen early through `latest`. - expect(attempt(() => latest(m()))).toBe("caught"); - expect(attempt(() => m()())).toBe("ok:3"); - release(); - await tick(); - await tick(); - expect(latestScreens).toEqual(["ok:3", "caught"]); - expect(attempt(() => m()())).toBe("caught"); - dispose(); -}); - -it("a catching memo read only through latest() commits its own answer", async () => { - const { setS, m, latestScreens, dispose } = setup(false); - let release!: () => void; - action(function* () { - setS(0); - yield new Promise(r => (release = r)); - })(); - await tick(); - release(); - await tick(); - await tick(); - expect(attempt(() => m()())).toBe("caught"); - expect(latestScreens).toEqual(["ok:3", "caught"]); - dispose(); -}); - -it("a catching memo over an error of a flush with no hold commits its own answer", () => { - const { setS, m, latestScreens, dispose } = setup(false); - setS(0); - flush(); - expect(attempt(() => m()())).toBe("caught"); - expect(latestScreens).toEqual(["ok:3", "caught"]); - dispose(); -}); diff --git a/packages/signals/tests/staged-error-derived-store-catching-derive.test.ts b/packages/signals/tests/staged-error-derived-store-catching-derive.test.ts deleted file mode 100644 index 1663e8c9b5..0000000000 --- a/packages/signals/tests/staged-error-derived-store-catching-derive.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * D-41 and D-30: a derived store whose derive catches its source's staged - * error answers with its own pass: no key throws the error, and a key that - * never read the source (`a`) keeps its value — pending is per key. - * - * `d => { d.a = 7; try { d.b = m0() } catch { d.b = "caught" } }`; an action - * makes `m0` throw and a hold keeps the write. - */ -import { afterEach, expect, it } from "vitest"; -import { - action, - createMemo, - createRenderEffect, - createRoot, - createSignal, - createStore, - flush, - latest, - resetErrorHalt -} from "../src/index.js"; - -afterEach(() => resetErrorHalt()); -const tick = async () => { - await new Promise(r => setTimeout(r, 0)); - flush(); -}; -const attempt = (fn: () => unknown) => { - try { - return fn(); - } catch (e: any) { - return `throws ${e?.cause?.message ?? e?.message}`; - } -}; - -function setup(plainReader: boolean) { - const [s, setS] = createSignal(2); - let st!: { a?: number; b?: unknown }; - const dispose = createRoot(d => { - const m0 = createMemo(() => { - const v = s() + 1; - if (v % 4 === 1) throw new Error(`m0:${v}`); - return v; - }); - const [store] = createStore<{ a?: number; b?: unknown }>(dr => { - dr.a = 7; - try { - dr.b = m0(); - } catch { - dr.b = "caught"; - } - }, {}); - st = store; - createRenderEffect( - () => [attempt(() => latest(() => store.a)), attempt(() => latest(() => store.b))], - () => {} - ); - if (plainReader) - createRenderEffect( - () => [attempt(() => store.a), attempt(() => store.b)], - () => {} - ); - return d; - }); - flush(); - return { setS, st: () => st, dispose }; -} - -it("latest() of a key that never read the source does not throw its staged error", async () => { - const { setS, st, dispose } = setup(true); - let release!: () => void; - action(function* () { - setS(0); - yield new Promise(r => (release = r)); - })(); - await tick(); - expect(attempt(() => latest(() => st().a))).toBe(7); - expect(attempt(() => latest(() => st().b))).toBe("caught"); - release(); - await tick(); - await tick(); - expect([attempt(() => st().a), attempt(() => st().b)]).toEqual([7, "caught"]); - dispose(); -}); - -it("a store read only through latest() commits its derive's own answer", async () => { - const { setS, st, dispose } = setup(false); - let release!: () => void; - action(function* () { - setS(0); - yield new Promise(r => (release = r)); - })(); - await tick(); - release(); - await tick(); - await tick(); - expect([attempt(() => st().a), attempt(() => st().b)]).toEqual([7, "caught"]); - dispose(); -}); From 14e563974b14cdb3b870f1b3a0ba7a36b61db51c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 14:29:28 +0000 Subject: [PATCH 09/10] chore(size): raise caps for the staged-error marks over next @ 131ceb8c The marks run with the verdict engine alone: +115 B minified on the isPending/latest bundles, 0 B on the floors. isPending/latest 11.12 -> 11.16 KB, the live server-component page's frozen floor 38.85 -> 38.90 KB, compiled live 42.73 -> 42.81 KB, each at measured + 10 B rounded up to 0.01 KB with recorded minified = measured; live + router re-records its minified only (still under its cap). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- scripts/size/floor-caps.json | 4 ++-- scripts/size/scenarios.js | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index 2a42d8a491..8efb90298a 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -16,8 +16,8 @@ "minified": 101638 }, "page: live server components (base + live/GET + action + isPending/latest)": { - "cap": "38.85 KB", - "minified": 117376 + "cap": "38.90 KB", + "minified": 117492 }, "server: floor (getRequestEvent + isServer)": { "cap": "1.29 KB", diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 99c9d6c86f..a08fb425ce 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1443,9 +1443,10 @@ module.exports = [ // A probe beside `latest()` outside any Loading no longer loops on a refetch (F-6, D-23; 2026-10-30, over next @ 7135a77a): 11.07 KB -> 11.10 KB, recorded minified 30,492 -> 30,569 B (+77 B), measured locally (linux) at 11,085 B brotli. `pendingVerdict` remembers the transaction a `latest` read of an unheld flight opened, and a probe in it joins the verdict lane without asking to re-run at its landing (the re-run re-opened it every flush). Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Batch re-record (2026-10-30, over next @ 563b6b0): cap unchanged, recorded minified 30,569 -> 30,596 B (+27 B), measured locally (linux) at 11,092 B brotli. Growth is the 2026-10-30 merges (#4454, #4457, #4461). Recorded minified = measured. // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): 11.10 KB -> 11.12 KB, recorded minified 30,596 -> 30,641 B (+45 B), measured locally (linux) at 11,109 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "11.12 KB", + // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, over next @ 131ceb8c): 11.12 KB -> 11.16 KB, recorded minified 30,641 -> 30,756 B (+115 B), measured locally (linux) at 11,149 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "11.16 KB", // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 28,693 -> 29,086 B (+343 B minified over next), measured locally (linux) at 10,565 B brotli (+135 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 30641, + capMinified: 30756, alias }, { @@ -5611,6 +5612,7 @@ module.exports = [ // 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. // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): 38.83 KB -> 38.85 KB, recorded minified 117,331 -> 117,376 B (+45 B), measured locally (linux) at 38,839 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, over next @ 131ceb8c): 38.85 KB -> 38.90 KB, recorded minified 117,376 -> 117,492 B (+116 B), measured locally (linux) at 38,889 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. 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)"], @@ -5927,11 +5929,12 @@ module.exports = [ // A probe beside `latest()` outside any Loading no longer loops on a refetch (F-6, D-23; 2026-10-30, over next @ 7135a77a): 42.67 KB -> 42.71 KB, recorded minified 125,374 -> 125,450 B (+76 B), measured locally (linux) at 42,693 B brotli. `pendingVerdict` remembers the transaction a `latest` read of an unheld flight opened, and a probe in it joins the verdict lane without asking to re-run at its landing (the re-run re-opened it every flush). Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Batch re-record (2026-10-30, over next @ 563b6b0): cap unchanged, recorded minified 125,450 -> 125,478 B (+28 B), measured locally (linux) at 42,695 B brotli. Growth is the 2026-10-30 merges (#4454, #4457, #4461). Recorded minified = measured. // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): 42.71 KB -> 42.73 KB, recorded minified 125,478 -> 125,518 B (+40 B), measured locally (linux) at 42,715 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "42.73 KB", + // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, over next @ 131ceb8c): 42.73 KB -> 42.81 KB, recorded minified 125,518 -> 125,633 B (+115 B), measured locally (linux) at 42,795 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "42.81 KB", // An Errored heals when the last reader that caught its error is disposed (#4373, #4384, 2026-10-28, re-recorded over next @ cbc0eebd): recorded minified 123,191 -> 123,249 B (+58 B minified over next's record), measured locally (linux) at 41,899 B brotli. Recorded minified = measured; CI re-bases. // Boundary readers keyed by the source they wait on (#4373), with enterLane's tail folded into one else-chain as an offset (-29 B on lane pages) (#4384, 2026-10-28, re-recorded over next @ 1b32ff2e): recorded minified 123,203 -> 123,224 B (+21 B minified over next's record), measured locally (linux) at 41,966 B brotli. Recorded minified = measured; CI re-bases. // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 123,224 -> 123,733 B (+403 B minified over next), measured locally (linux) at 42,119 B brotli (+169 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 125518, + capMinified: 125633, alias: pageAlias }, { @@ -6332,11 +6335,12 @@ module.exports = [ // A probe beside `latest()` outside any Loading no longer loops on a refetch (F-6, D-23; 2026-10-30, over next @ 7135a77a): 48.42 KB -> 48.44 KB, recorded minified 142,631 -> 142,706 B (+75 B), measured locally (linux) at 48,421 B brotli. `pendingVerdict` remembers the transaction a `latest` read of an unheld flight opened, and a probe in it joins the verdict lane without asking to re-run at its landing (the re-run re-opened it every flush). Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Batch re-record (2026-10-30, over next @ 563b6b0): cap unchanged, recorded minified 142,706 -> 142,735 B (+29 B), measured locally (linux) at 48,365 B brotli. Growth is the 2026-10-30 merges (#4454, #4457, #4461). Recorded minified = measured. // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): cap unchanged at 48.44 KB, recorded minified 142,735 -> 142,780 B (+45 B), measured locally (linux) at 48,394 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, over next @ 131ceb8c): cap unchanged at 48.44 KB, recorded minified 142,780 -> 142,896 B (+116 B), measured locally (linux) at 48,424 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: "48.44 KB", // An Errored heals when the last reader that caught its error is disposed (#4373, #4384, 2026-10-28, re-recorded over next @ cbc0eebd): recorded minified 140,451 -> 140,509 B (+58 B minified over next's record), measured locally (linux) at 47,516 B brotli. Recorded minified = measured; CI re-bases. // Boundary readers keyed by the source they wait on (#4373), with enterLane's tail folded into one else-chain as an offset (-29 B on lane pages) (#4384, 2026-10-28, re-recorded over next @ 1b32ff2e): recorded minified 140,463 -> 140,485 B (+22 B minified over next's record), measured locally (linux) at 47,547 B brotli. Recorded minified = measured; CI re-bases. // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 140,485 -> 140,983 B (+405 B minified over next), measured locally (linux) at 47,722 B brotli (+162 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 142780, + capMinified: 142896, alias: pageAlias, conditions: solidConditions, compile: { hydratable: true } From 74f3771b931869c256dc39abc906c0ad6472e50e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 16:59:46 +0000 Subject: [PATCH 10/10] chore(size): re-measure the staged-error marks over next @ b773146 isPending/latest 11.12 -> 11.17 KB (30,771), compiled live 42.75 -> 42.79 KB (125,655), live + router 48.44 -> 48.51 KB (142,918); the live server-components floor keeps its 38.85 KB cap and re-records minified 117,376 -> 117,514 (Size-Exception in the PR body). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- scripts/size/floor-caps.json | 4 ++-- scripts/size/scenarios.js | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index 37245301e0..393492092d 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -16,8 +16,8 @@ "minified": 101638 }, "page: live server components (base + live/GET + action + isPending/latest)": { - "cap": "38.90 KB", - "minified": 117492 + "cap": "38.85 KB", + "minified": 117514 }, "server: floor (getRequestEvent + isServer)": { "cap": "1.29 KB", diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 624bf27001..99347622f1 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1443,10 +1443,10 @@ module.exports = [ // A probe beside `latest()` outside any Loading no longer loops on a refetch (F-6, D-23; 2026-10-30, over next @ 7135a77a): 11.07 KB -> 11.10 KB, recorded minified 30,492 -> 30,569 B (+77 B), measured locally (linux) at 11,085 B brotli. `pendingVerdict` remembers the transaction a `latest` read of an unheld flight opened, and a probe in it joins the verdict lane without asking to re-run at its landing (the re-run re-opened it every flush). Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Batch re-record (2026-10-30, over next @ 563b6b0): cap unchanged, recorded minified 30,569 -> 30,596 B (+27 B), measured locally (linux) at 11,092 B brotli. Growth is the 2026-10-30 merges (#4454, #4457, #4461). Recorded minified = measured. // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): 11.10 KB -> 11.12 KB, recorded minified 30,596 -> 30,641 B (+45 B), measured locally (linux) at 11,109 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, over next @ 131ceb8c): 11.12 KB -> 11.16 KB, recorded minified 30,641 -> 30,756 B (+115 B), measured locally (linux) at 11,149 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "11.16 KB", + // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, re-measured over next @ b773146): 11.12 KB -> 11.17 KB, recorded minified 30,641 -> 30,771 B (+130 B), measured locally (linux) at 11,153 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "11.17 KB", // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 28,693 -> 29,086 B (+343 B minified over next), measured locally (linux) at 10,565 B brotli (+135 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 30756, + capMinified: 30771, alias }, { @@ -5614,7 +5614,7 @@ module.exports = [ // 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. // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): 38.83 KB -> 38.85 KB, recorded minified 117,331 -> 117,376 B (+45 B), measured locally (linux) at 38,839 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, over next @ 131ceb8c): 38.85 KB -> 38.90 KB, recorded minified 117,376 -> 117,492 B (+116 B), measured locally (linux) at 38,889 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, re-measured over next @ b773146): cap unchanged at 38.85 KB, recorded minified 117,376 -> 117,514 B (+138 B), measured locally (linux) at 38,849 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. 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)"], @@ -5932,12 +5932,12 @@ module.exports = [ // Batch re-record (2026-10-30, over next @ 563b6b0): cap unchanged, recorded minified 125,450 -> 125,478 B (+28 B), measured locally (linux) at 42,695 B brotli. Growth is the 2026-10-30 merges (#4454, #4457, #4461). Recorded minified = measured. // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): 42.71 KB -> 42.73 KB, recorded minified 125,478 -> 125,518 B (+40 B), measured locally (linux) at 42,715 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // #4428 fallback memo lazy (+7 B over next @ ab3d2add, which sits +15 over this record; 2026-10-30): 42.73 KB -> 42.75 KB, recorded minified 125,518 -> 125,540 B (+22 B), measured locally (linux) at 42,735 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, over next @ 131ceb8c): 42.73 KB -> 42.81 KB, recorded minified 125,518 -> 125,633 B (+115 B), measured locally (linux) at 42,795 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "42.75 KB", + // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, re-measured over next @ b773146): 42.75 KB -> 42.79 KB, recorded minified 125,540 -> 125,655 B (+115 B), measured locally (linux) at 42,777 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "42.79 KB", // An Errored heals when the last reader that caught its error is disposed (#4373, #4384, 2026-10-28, re-recorded over next @ cbc0eebd): recorded minified 123,191 -> 123,249 B (+58 B minified over next's record), measured locally (linux) at 41,899 B brotli. Recorded minified = measured; CI re-bases. // Boundary readers keyed by the source they wait on (#4373), with enterLane's tail folded into one else-chain as an offset (-29 B on lane pages) (#4384, 2026-10-28, re-recorded over next @ 1b32ff2e): recorded minified 123,203 -> 123,224 B (+21 B minified over next's record), measured locally (linux) at 41,966 B brotli. Recorded minified = measured; CI re-bases. // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 123,224 -> 123,733 B (+403 B minified over next), measured locally (linux) at 42,119 B brotli (+169 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 125540, + capMinified: 125655, alias: pageAlias }, { @@ -6339,12 +6339,12 @@ module.exports = [ // A probe beside `latest()` outside any Loading no longer loops on a refetch (F-6, D-23; 2026-10-30, over next @ 7135a77a): 48.42 KB -> 48.44 KB, recorded minified 142,631 -> 142,706 B (+75 B), measured locally (linux) at 48,421 B brotli. `pendingVerdict` remembers the transaction a `latest` read of an unheld flight opened, and a probe in it joins the verdict lane without asking to re-run at its landing (the re-run re-opened it every flush). Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Batch re-record (2026-10-30, over next @ 563b6b0): cap unchanged, recorded minified 142,706 -> 142,735 B (+29 B), measured locally (linux) at 48,365 B brotli. Growth is the 2026-10-30 merges (#4454, #4457, #4461). Recorded minified = measured. // D-28's verdict-seat reset compares holds, not lane objects: a seated leaf reading a merged transaction's verdict lane stays the hold's verdict reader (`laneRead`) (#4427, #4455, 2026-10-30, over next @ cddaa76): cap unchanged at 48.44 KB, recorded minified 142,735 -> 142,780 B (+45 B), measured locally (linux) at 48,394 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, over next @ 131ceb8c): cap unchanged at 48.44 KB, recorded minified 142,780 -> 142,896 B (+116 B), measured locally (linux) at 48,424 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "48.44 KB", + // A staged error reaches the memos and derived stores that derive from it (#4456, 2026-10-30, re-measured over next @ b773146): 48.44 KB -> 48.51 KB, recorded minified 142,780 -> 142,918 B (+138 B), measured locally (linux) at 48,498 B brotli. The settle walk marks a derivation of a staged error with it, and a derived store's family hears it (D-41), with the verdict engine alone. Correctness; Size-Exception in #4456. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "48.51 KB", // An Errored heals when the last reader that caught its error is disposed (#4373, #4384, 2026-10-28, re-recorded over next @ cbc0eebd): recorded minified 140,451 -> 140,509 B (+58 B minified over next's record), measured locally (linux) at 47,516 B brotli. Recorded minified = measured; CI re-bases. // Boundary readers keyed by the source they wait on (#4373), with enterLane's tail folded into one else-chain as an offset (-29 B on lane pages) (#4384, 2026-10-28, re-recorded over next @ 1b32ff2e): recorded minified 140,463 -> 140,485 B (+22 B minified over next's record), measured locally (linux) at 47,547 B brotli. Recorded minified = measured; CI re-bases. // latest() read inline and through a memo show the same screen (#4284 through a memo; a plain write beside an async memo), 2026-10-29, over next @ 7ebd4a32: recorded minified 140,485 -> 140,983 B (+405 B minified over next), measured locally (linux) at 47,722 B brotli (+162 B over next). A render effect a verdict memo's change re-runs reads as its verdict reader for that pass (`laneDirty`), a pass that read a staging before the frame had a transaction re-derives once as a verdict reader only if the frame parks, `latest()` of a first load in flight in a verdict lane is loading (also through an identity memo, a derived store and `isPending`), a lane's dissolution leaves such a load and the nodes pending on it uninitialized instead of committing `undefined`, a verdict lane born at a seam is judged at it, a pass born in a staged reader stays the frame's on a verdict-memo read (C4, #4010), a plain write beside `latest()` computes once (the seam re-derives a pre-transaction staged reader only if the frame parks), a verdict reader's plain read of a staging in a transaction only a `latest()` opened is the frame's read, so a memo of `latest()` computes once too (D-22; not a pass with no value yet, D-6), and an error stays the display while the memo it came through re-asks, a Loading fallback behind it included (D-39, async-gen 91501 #1831). recompute's CONFIG_VERDICT clear now sits behind the lane engine: -21 B minified on every floor. Correctness; the maintainer: "only flag me if brotli goes more than 20kb" (2026-10-29). Cap unchanged; recorded minified = measured. - capMinified: 142896, + capMinified: 142918, alias: pageAlias, conditions: solidConditions, compile: { hydratable: true }