diff --git a/.changeset/staged-error-reaches-derivations.md b/.changeset/staged-error-reaches-derivations.md new file mode 100644 index 0000000000..42c070c2eb --- /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 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 cc5de18b55..bf8b254895 100644 --- a/packages/signals/src/core/async.ts +++ b/packages/signals/src/core/async.ts @@ -214,12 +214,39 @@ 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), 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>(); @@ -318,7 +345,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 +1039,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(""); +}); 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([]); + }); diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index 5ac244dc26..393492092d 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -17,7 +17,7 @@ }, "page: live server components (base + live/GET + action + isPending/latest)": { "cap": "38.85 KB", - "minified": 117376 + "minified": 117514 }, "server: floor (getRequestEvent + isServer)": { "cap": "1.29 KB", diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 74f04bc62a..99347622f1 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, 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: 30641, + capMinified: 30771, alias }, { @@ -5613,6 +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, 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)"], @@ -5930,11 +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. - 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 }, { @@ -6336,11 +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. - 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: 142780, + capMinified: 142918, alias: pageAlias, conditions: solidConditions, compile: { hydratable: true }