From 8e020e3960260ae5a4deea3d26403fa986d26a6e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 20:36:38 +0000 Subject: [PATCH 1/8] fix(web): a sweep's slot re-emissions join the sweep's unit Each arg binding of a slot occurrence re-sends the whole record as it sweeps, so the first copy carried the other args' previous values and went out alone, ahead of the holes the same commit moved. The chat's outbox dropped a pending message before the thread showed its row. Inside a sweep the record now joins the sweep's unit like hole and attr records (C13): the pass's last re-emission stands and leaves with them. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015SnZadCVdDSeDzxBUhcMU9 --- .changeset/slot-records-join-sweep.md | 5 +++ packages/web/frames/src/frame-sink.ts | 12 ++++++- .../web/test/server/frame-sweep-ops.spec.tsx | 36 ++++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 .changeset/slot-records-join-sweep.md diff --git a/.changeset/slot-records-join-sweep.md b/.changeset/slot-records-join-sweep.md new file mode 100644 index 000000000..bcdd3c478 --- /dev/null +++ b/.changeset/slot-records-join-sweep.md @@ -0,0 +1,5 @@ +--- +"@solidjs/web": patch +--- + +A live server component's slot record that a commit changes now leaves in the same write as the holes that commit moved, with every arg current. Before, each arg of the occurrence re-sent the record as it updated, so the first copy went out alone with the other args still at their previous values, ahead of the hole. A client fill reading those args could show a state no commit had: the chat's pending message left the outbox before the thread showed its row. diff --git a/packages/web/frames/src/frame-sink.ts b/packages/web/frames/src/frame-sink.ts index b1255528e..ee2ff0c1f 100644 --- a/packages/web/frames/src/frame-sink.ts +++ b/packages/web/frames/src/frame-sink.ts @@ -818,8 +818,18 @@ export function createFrameSink(write, frame, have, hooks) { // Data args are `{$ref}`s into the response's data table; server-content // args are `{$frame}` nested regions. No document-sink counterpart — // slots only exist in frame streams. + // + // A sweep's re-emission joins the sweep's unit (C13), as a hole's does: + // each arg binding of an occurrence re-sends the whole record as it + // sweeps, so the first carries the others' previous values. Sent as it + // came, that record would reach the client alone, ahead of the holes the + // same commit moved — a client fill reading it shows a state no commit + // had (an outbox dropping a message before the hole shows its row). In + // the unit the last re-emission of the pass stands, and it leaves with + // the holes. slot(key, args) { - put(id, { [`slot:${key}`]: { kind: "slot", args } }); + const r = { [`slot:${key}`]: { kind: "slot", args } }; + swept ? Object.assign(swept.r, r) : put(id, r); }, // A nested server-content region (a `{$frame}` slot arg): its html is // the root record of the CHILD frame id — the consumer binds a nested diff --git a/packages/web/test/server/frame-sweep-ops.spec.tsx b/packages/web/test/server/frame-sweep-ops.spec.tsx index 033c2ecd9..b3cc2290b 100644 --- a/packages/web/test/server/frame-sweep-ops.spec.tsx +++ b/packages/web/test/server/frame-sweep-ops.spec.tsx @@ -15,7 +15,7 @@ */ import { describe, expect, it } from "vitest"; import vm from "node:vm"; -import { createMemo } from "solid-js"; +import { createMemo, For } from "solid-js"; import { Loading, renderToStream } from "@solidjs/web"; import { frameTransformDirectResult, @@ -176,4 +176,38 @@ describe("C13 server half — the sink emits one sweep as one unit", () => { ]); expect((Object.values(ops[1].r)[0] as any).value).toBe("a2"); }); + + it("stream face: a slot occurrence whose args the sweep moved leaves in the same batch as the hole, once, with every arg current", async () => { + // The chat's composer: a slot outside the list hole reading the same + // source. Each arg binding re-sends the occurrence as it sweeps; sent as + // they came, the first went out alone with `busy` still the previous + // commit's, ahead of the hole that shows the new row. + const ch = channel(); + const Comp = (props: any) => { + const rows = createMemo(() => ch.iterable); + return ( +
+
    + {row =>
  1. {row}
  2. }
    +
+ 1} /> +
+ ); + }; + const { chunks, until, done } = consume( + renderServerComponent(Comp, { frame: { id: "f" }, live: true }) + ); + ch.push(["a"]); + await until(c => has(c, "")); + const seen = chunks.length; + ch.push(["a", "b"]); + await until(c => records([c], "hole:").length > 0); + ch.end(); + await done; + + const batches = chunks.slice(seen).filter(c => c.r && !has(c, ":complete")); + expect(batches).toHaveLength(1); + expect(Object.keys(batches[0].r).map(key => key.split(":")[0])).toEqual(["slot", "hole"]); + expect(batches[0].r["slot:composer#0"].args.busy).toBe(true); + }, 8000); }); From 98f31fb5df06a44ca7b3a32f33b2a8eb08e6a0d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 21:25:40 +0000 Subject: [PATCH 2/8] fix(signals): a keyed arrangement guess folds a correction instead of voiding it Two overlapping actions pushing onto one keyed optimistic store: when the stream echoed the first write while the second was open, the correction dissolved the lane. For that window the store read torn (a hole at the first row's index, `length` from the guess, a raw target served for a row), and at the commit the confirmed row remounted twice, which destroyed an open editor and its draft in the board example. A keyed arrangement guess is rows by key, so a current truth that differs now folds into the hold beneath it (D-34, amended): the guess stays the lane's value and the screen's until the hold commits, and rows match by key there. Deriving passes outside the lane read the hold's world (each guess's truth, else what it covered) and join the hold, as for any held truth (#3164); `latest()` shows it; `until`/`resolve` keep their postures. Keys born under a folded guess are held with it. Lane work in a frame that already has a transaction reads that frame's stagings as the screen, as a verdict reader does, so a list pass never pairs the truth's length with the guess's rows. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015SnZadCVdDSeDzxBUhcMU9 --- .changeset/keyed-guess-folds-correction.md | 5 + packages/signals/docs/RULES-DERIVED.md | 2 + packages/signals/docs/RULES-INDEX.md | 2 +- packages/signals/src/core/core.ts | 22 +- packages/signals/src/core/lanes.ts | 54 ++++- packages/signals/src/core/scheduler.ts | 15 ++ packages/signals/src/core/verdict.ts | 5 +- packages/signals/src/store/optimistic.ts | 12 + packages/signals/src/store/store.ts | 7 + .../tests/optimistic-keyed-fold.test.ts | 216 ++++++++++++++++++ 10 files changed, 328 insertions(+), 12 deletions(-) create mode 100644 .changeset/keyed-guess-folds-correction.md create mode 100644 packages/signals/tests/optimistic-keyed-fold.test.ts diff --git a/.changeset/keyed-guess-folds-correction.md b/.changeset/keyed-guess-folds-correction.md new file mode 100644 index 000000000..ec7187290 --- /dev/null +++ b/.changeset/keyed-guess-folds-correction.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +A keyed optimistic store's arrangement guess is no longer voided when the source answers with something different while the guess's action is open: the answer folds beneath it until the hold commits, and rows then match by key. Two overlapping actions on one list used to leave the store torn after the first one's echo (a hole at its index, `length` from the guess, a raw target served for a row), and the confirmed row remounted twice at the commit, taking an open editor and its draft with it. Now the screen keeps both guesses whole, nothing remounts, and a row another tab added shows when the write settles. Memos and effects outside the optimistic lane still derive from the answer right away and are held with it, and `latest()` shows it. diff --git a/packages/signals/docs/RULES-DERIVED.md b/packages/signals/docs/RULES-DERIVED.md index 3ad13ab09..529c71c35 100644 --- a/packages/signals/docs/RULES-DERIVED.md +++ b/packages/signals/docs/RULES-DERIVED.md @@ -286,6 +286,8 @@ An override's lifetime is that of the hold it was made over — in the usual sha When an override's own source answers (its own landing, or a sync recompute from an upstream change): an equal answer confirms silently — nothing re-runs; a different answer corrects — derivations re-derive from the truth now as the hold's work (downstream refetches restart at once, no waterfall), the screen and untracked reads keep what the screen showed until the commit, `latest()` answers the truth and `isPending()` reads `true` while they differ, and the correction reveals with the hold. "Keep the override" means keep what _showed_: a guess the screen never displayed — its reveal held on its own downstream flight, including through a shown `Loading` — is void at the correction, and untracked reads see the committed value, as the screen does. A later landing equal to the override restores it as the graph's value. +A keyed store's arrangement guess (rows by key) is not voided by a correction: the truth folds beneath it. Derivations outside the lane re-derive from the truth now and join the hold, `until`/`resolve` keep their postures over it, and `latest()` shows the hold's world whole. Lane passes, untracked reads and the screen keep the guess until the commit, and at the commit rows match by key. So a row the truth returns keeps its identity, a row another tab added shows at the commit, and the first of two overlapping actions confirming does not void the second's guess (2026-10-21). A list pass inside a frame that already has a transaction reads that frame's stagings as the screen, as a verdict reader does, so it never pairs the truth's length with the guess's rows. + **Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#a18-an-override-lives-exactly-as-long-as-its-own-transaction-a-newer-truth-from-the-source-supersedes-it-in-the-graph-immediately-on-screen-at-commit) supersession (#3331, 2026-09-09/10) and (d) before the first commit, "Correction display is what showed" (2026-10-05), lane contract 2, lane ruling 5, [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) (2026-09-09), OS-R39. **Status:** standing. ### D-35. Provenance: an older question's answer never corrects a newer guess; the body's end corrects what nothing authoritative is answering diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 17a7acb44..003c7c6e4 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -61,7 +61,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:213` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… | | A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:249` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×4 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 create-deferred-3710.test.ts×2 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 show-stale-child-in-action-4137.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … | | A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:221` | scheduler.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… | -| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:71` | async.ts×3 constants.ts×1 lanes.ts×7 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… | +| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:71` | async.ts×3 constants.ts×1 lanes.ts×8 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… | | A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:81` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×5 flight-owned-transaction.test.ts×1 lane-authority-twins.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×19 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… | | A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:149` | async.ts×1 constants.ts×1 core.ts×6 scheduler.ts×2 types.ts×1 verdict.ts×4 store.ts×1 | create-deferred-3710.test.ts×2 fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… | | A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:408` | — | question-scoped-pending.test.ts×2 spec-async-semantics.test.ts×3 createOptimisticStore.test.ts×1 | [superseded 2026-07-13 by A24] (superseded) Optimistic writes announce a store-wide pending — (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the… | diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index 4cb92f61f..3d2df2591 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -1082,7 +1082,11 @@ export function ext(el: { _x: NodeExtension | null }): NodeExtension { * something not ready under a loading boundary that has not shown content: * the boundary collects it and shows its fallback, and the pass is the * boundary's, not the tick's (A29's boundary exemption, #3540). */ -export function joinPass(c: Computed, el: Signal | Computed): void { +export function joinPass( + c: Computed, + el: Signal | Computed, + t: Transaction = txOf(el) +): void { c._flags |= REACTIVE_JOINED; // A new memo's first pass is the hold's work (born held) but not the // tick's: a render effect that reads it decides — under a boundary that @@ -1093,10 +1097,7 @@ export function joinPass(c: Computed, el: Signal | Computed): voi (el as Computed)._statusFlags & STATUS_UNINITIALIZED || (c as any)._type !== EFFECT_RENDER ) - joinPassTx( - txOf(el), - (!(c as any)._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c) - ); + joinPassTx(t, (!(c as any)._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c)); } /** A15's stale reader (shared-hole and reveal corollaries): a render effect @@ -1810,7 +1811,11 @@ export function read(el: Signal | Computed): T { if (!c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN) return el._value as T; // (A plain signal is never uninitialized — the carve-out is `serve`'s.) - if (observeFlightHook && c._config & CONFIG_VERDICT && stagedScreen(c as Computed)) + // Lane work, once the frame has a transaction, reads the screen too. + if ( + ((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane !== null)) && + stagedScreen(c as Computed) + ) return el._value as T; stagedRead(c as Computed); return el._pendingValue as T; @@ -2028,7 +2033,10 @@ export function serve(el: Signal | Computed, c: Computed | null): /** A pass read a staging of this flush: it derives from what the flush may * yet hold (REACTIVE_STAGED_READ — the seam decides). Lane work too (plan sec. 28, a - * lane sees the screen plus its own guesses): the staging is the screen if + * lane sees the screen plus its own guesses), before the frame has a + * transaction (after, it reads the screen like a verdict reader, + * `stagedScreen` — a list pass never pairs a held length with the guess's + * rows, D-34): the staging is the screen if * the frame commits — one pass, the common case — and a held write if it * parks, which the seam repairs (`stagedReaders`): the pass re-derives on * the committed world and its lane's runs wait that round, so the held diff --git a/packages/signals/src/core/lanes.ts b/packages/signals/src/core/lanes.ts index 39072babd..10371a317 100644 --- a/packages/signals/src/core/lanes.ts +++ b/packages/signals/src/core/lanes.ts @@ -59,7 +59,7 @@ import { STATUS_UNINITIALIZED } from "./constants.js"; import { attrHooks } from "./attribution-hooks.js"; -import { ext, stagedRead, tracking } from "./core.js"; +import { ext, joinPass, stagedRead, tracking } from "./core.js"; import { NotReadyError } from "./error.js"; import { laneValueOf } from "./truth.js"; import { enqueueSub } from "./heap.js"; @@ -536,6 +536,34 @@ function dissolveLane(l: Transaction, into: Transaction | null, except?: Signal< else l._queues[0].length = l._queues[1].length = 0; } +/** A lane a correction folded under (D-34, `laneWrite`): a deriving pass + * outside the lane composes the hold's world — each guess's truth staged + * beneath it, else the value it covered — and joins the hold, tracked or + * not (`untrack` is about dependencies, not about which world a pass + * derives from). Lane passes and the screen keep the guesses: held truth is + * masked from lane passes only (#3164). NOT_PENDING: not such a read. */ +function foldedRead(c: Computed, el: Signal | Computed): unknown { + if ( + !(el._config & CONFIG_GUESS) || + passLane !== null || + (c as any)._type === EFFECT_RENDER || + c._config & CONFIG_CHILDREN_FORBIDDEN + ) + return NOT_PENDING; + const l = txOf(el); + // The store's own derive reconciles its landings against the arrangement + // the guesses made (store/optimistic.ts): it is the author, not a reader. + if (!l._folded || (el as any)._host?.fam?.node === c) return NOT_PENDING; + joinPass(c, el, resolveTx(l._parent ?? l)); + return foldedValue(el); +} + +/** A guess's value in the hold's world: the truth staged beneath it, else + * the value it covered. */ +export function foldedValue(el: Signal | Computed): unknown { + return el._pendingValue !== NOT_PENDING ? el._pendingValue : covered(el); +} + /** Lanes. A lane's node is read by its reader's kind: an authoritative * reader (`until`) sees the truth — staged beneath a guess, else committed * — and is no lane's; an untracked read sees the lane's latest value (A17: @@ -553,6 +581,10 @@ function dissolveLane(l: Transaction, into: Transaction | null, except?: Signal< * member throws like any. */ export function laneRead(c: Computed | null, el: Signal | Computed): unknown { const guess = el._config & CONFIG_GUESS; + if (c !== null) { + const v = foldedRead(c, el); + if (v !== NOT_PENDING) return v; + } if (c !== null && c._config & CONFIG_AUTHORITATIVE) return GlobalQueue._truth!(el); const status = (el as Computed)._statusFlags; // An errored member's lane value predates its error; the error is the answer (#4106). @@ -733,10 +765,26 @@ function laneWrite(el: Signal | Computed, v: T): T { // question is the one being asked (`question`: a slice's, a derive's // continuation's — the flight's); unstamped writes and mainline are // current. - if (stale(el, question)) { + const keyed = !stale(el, question) && GlobalQueue._keyedArrangement?.(el); + if (keyed || stale(el, question)) { el._pendingValue = v; el._config |= CONFIG_HELD; - GlobalQueue._laneRebase?.(el, v); + if (keyed) { + // A keyed arrangement guess is rows by key: a current truth folds + // into the hold beneath it instead of voiding it (D-34). The guess + // stays the lane's value and the screen's. A confirmation is silent + // (A17), only truth readers are told; a correction re-derives the + // graph outside the lane now (`foldedRead`), the screen at the + // commit, where rows match by key. + const l = txOf(el); + let changed = !el._equals || !el._equals(el._x!._lane as T, v); + if (changed && GlobalQueue._echoes?.(el._x!._lane, v)) changed = false; + if (changed) { + l._folded = true; + insertSubs(el); + } else GlobalQueue._confirmed?.(l); + joinFuture(resolveTx(l._parent ?? l)); + } else GlobalQueue._laneRebase?.(el, v); schedule(); return v; } diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts index d20b2d844..e0fa44daf 100644 --- a/packages/signals/src/core/scheduler.ts +++ b/packages/signals/src/core/scheduler.ts @@ -245,6 +245,10 @@ export interface Transaction { * leaf reading a held lane that has not shown is a stale reader of it * (#3460); one reading a lane the seam has not judged yet is its work. */ _held: boolean; + /** Lane: a current truth that differs folded beneath one of its keyed + * guesses (lanes.ts `laneWrite`, D-34): a deriving pass outside the lane + * reads the hold's world from its guesses (`foldedRead`). */ + _folded: boolean; /** `affects()` marks declared in it (affects.ts): released at the landing. * Not nodes — a mark holds nothing (`blocked` never sees one). */ _marks: Signal[] | null; @@ -269,6 +273,7 @@ export function newTransaction(lane: boolean, parent: Transaction | null = null) _links: null, _shown: false, _held: false, + _folded: false, _marks: null }; if (!lane) transactions.push(t); @@ -321,6 +326,12 @@ export function joinPassTx(t: Transaction, own?: unknown): void { else merge(resolveTx(t), passTx); schedule(); } +/** The hold a lane's guess `n` folds a correction into (`_folded`, lanes.ts + * `laneWrite`), or null. */ +export function foldedHold(n: Signal): Transaction | null { + const l = resolveTx(n._x!._transaction!); + return l._folded ? resolveTx(l._parent ?? l) : null; +} /** The transaction holding a node (CONFIG_HELD set), resolved through * merges; the path is compressed. */ export function txOf(n: Signal | Computed): Transaction { @@ -808,6 +819,10 @@ export class GlobalQueue implements IQueue { * `laneWrite`): the store re-bases an arrangement guess over it * (store/optimistic.ts). */ declare static _laneRebase: ((el: Signal, truth: unknown) => void) | undefined; + /** A landing on a node of a keyed array whose arrangement a lane guesses + * (store/optimistic.ts): the guess is rows by key, not positions, so a + * current truth folds beneath it instead of voiding it (D-34). */ + declare static _keyedArrangement: ((el: Signal) => boolean) | undefined; // Lanes (lanes.ts). declare static _verdictLane: ((t: Transaction) => Transaction) | undefined; // Observe tier (attribution.ts): the guesses of `t`'s lanes (lanes.ts). diff --git a/packages/signals/src/core/verdict.ts b/packages/signals/src/core/verdict.ts index 4f00fe065..9c136a4da 100644 --- a/packages/signals/src/core/verdict.ts +++ b/packages/signals/src/core/verdict.ts @@ -46,7 +46,7 @@ import { import { warnStrictReadUntracked } from "./dev.js"; import { NotReadyError } from "./error.js"; import { link } from "./graph.js"; -import { display, laneValueOf, verdictLane } from "./lanes.js"; +import { display, foldedValue, laneValueOf, verdictLane } from "./lanes.js"; import { enqueueSub } from "./heap.js"; import { flushTransaction, @@ -394,6 +394,9 @@ function verdictValue(el: Signal | Computed, c: Computed | null): (!el._equals || !el._equals(el._x!._lane as any, el._pendingValue as any)))) ) probeFound = true; + // A lane a correction folded under (D-34): `latest()` shows the hold's + // world whole — the truth beneath each guess, else what it covered. + if (latestActive && l._folded) return foldedValue(el); return el._x!._lane; } if (probing && flying) probeFound = true; diff --git a/packages/signals/src/store/optimistic.ts b/packages/signals/src/store/optimistic.ts index fcadbe4ca..9cd39d645 100644 --- a/packages/signals/src/store/optimistic.ts +++ b/packages/signals/src/store/optimistic.ts @@ -381,6 +381,18 @@ GlobalQueue._laneRebase = (el: Signal, truth: unknown): void => { } }; +GlobalQueue._keyedArrangement = (el: Signal): boolean => { + const t = (el as any)._host as StoreTarget | undefined; + if (t === undefined || (t.fam?.key ?? null) === null) return false; + const k = t.k; + return ( + k !== null && + (k._config & CONFIG_OVERRIDE) !== 0 && + k._x!._lane instanceof LaneView && + Array.isArray(k._x!._lane.rows) + ); +}; + function sameRow(a: unknown, b: unknown, keyFn: (item: any) => any): boolean { if (a === b) return true; if (!isWrappable(a) || !isWrappable(b)) return false; diff --git a/packages/signals/src/store/store.ts b/packages/signals/src/store/store.ts index 142f77529..5ee229f14 100644 --- a/packages/signals/src/store/store.ts +++ b/packages/signals/src/store/store.ts @@ -84,6 +84,7 @@ import { flushTransaction, GlobalQueue, globalQueue, + foldedHold, holdNode, insertSubs, joinFuture, @@ -373,6 +374,12 @@ function bornStaged(target: StoreTarget, node: Signal, staged: any, key: Pr // container carrying a guess is a lane's: the flush's park decides.) if ((k._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD && heldKeyChanged(target, key)) holdNode(node, txOf(k)); + // A guess a correction folded under (D-34): the truth beneath it is the + // hold's, and so is a key it changed. + else if (k._config & CONFIG_OVERRIDE && k._config & CONFIG_HELD && heldKeyChanged(target, key)) { + const t = foldedHold(k); + if (t !== null) holdNode(node, t); + } // A28 (4): a batch written inside a creation-time pass is promoted — its // late-materialized leaf with it. if (k._config & CONFIG_PROMOTED) notePromotedWrite(node); diff --git a/packages/signals/tests/optimistic-keyed-fold.test.ts b/packages/signals/tests/optimistic-keyed-fold.test.ts new file mode 100644 index 000000000..3d1b5d60f --- /dev/null +++ b/packages/signals/tests/optimistic-keyed-fold.test.ts @@ -0,0 +1,216 @@ +/** + * A keyed store's arrangement guess is rows by key: a current truth that + * differs folds beneath it instead of voiding it (D-34, 2026-10-21). + * + * The case that needed it: two overlapping actions push onto one optimistic + * store, and the stream echoes the first write while the second is still + * open. Voiding the lane on that correction left the store torn for the + * window (index 1 a hole, `length` the guess's, a raw target served for a + * row) and remounted the first row twice at the commit — in a board, an open + * title editor and its draft went with it. Folded, the screen keeps the + * guesses until the hold commits, and at the commit rows match by key: no + * row remounts, and the second action's guess survives the first's echo. + */ +import { describe, expect, it } from "vitest"; +import { + action, + createOptimisticStore, + createRenderEffect, + createRoot, + createSignal, + flush, + mapArray, + onCleanup, + TimeoutError, + until +} from "../src/index.js"; + +type Row = { id: string; title: string }; +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); +const row = (id: string, title = id): Row => ({ id, title }); + +/** A keyed optimistic list over a signal source (a live stream's latest + * snapshot), mapped by `mapArray` and rendered. Rows are tagged with an + * identity counter so a replacement shows as a new `#n`. */ +function setup(initial: Row[], nested = false) { + const [source, setSource] = createSignal(initial); + const frames: string[] = []; + const mapped: (Row | undefined)[] = []; + let mounts = 0; + let unmounts = 0; + let get!: () => Row[]; + let set!: (fn: (d: Row[]) => void) => void; + createRoot(() => { + if (nested) { + const [s, ss] = createOptimisticStore<{ cards: Row[] }>(() => ({ cards: source() }), { + cards: [] + }); + get = () => s.cards; + set = fn => ss(d => fn(d.cards)); + } else { + const [s, ss] = createOptimisticStore(() => source(), []); + get = () => s; + set = fn => ss(d => fn(d)); + } + const rows = mapArray(get, (r: Row) => { + mapped.push(r); + mounts++; + onCleanup(() => unmounts++); + return r; + }); + createRenderEffect( + () => + rows() + .map(r => r.id) + .join(","), + v => void frames.push(v) + ); + }); + flush(); + const ids = new WeakMap(); + let n = 0; + const tag = (o: object) => (ids.has(o) ? ids.get(o)! : (ids.set(o, ++n), n)); + return { + setSource, + frames, + mapped, + get rows() { + return get(); + }, + set, + snap: () => Array.from(get(), r => (r ? `${r.id}#${tag(r)}` : "HOLE")).join(","), + counts: () => ({ mounts, unmounts }) + }; +} + +const push = (h: ReturnType, id: string, timeout?: number) => + action(function* () { + h.set(d => void d.push(row(id))); + yield sleep(5); + yield until(() => h.rows.some(r => r.id === id), timeout ? { timeout } : undefined); + })(); + +describe("keyed arrangement guess: a correction folds beneath it (D-34)", () => { + for (const nested of [false, true]) { + it(`two overlapping pushes, the first echoed alone: the store stays whole and no row remounts${nested ? " (nested)" : ""}`, async () => { + const h = setup([row("a")], nested); + const p1 = push(h, "b"); + flush(); + await sleep(2); + const p2 = push(h, "c"); + flush(); + await sleep(10); + expect(h.snap()).toBe("a#1,b#2,c#3"); + + h.setSource([row("a"), row("b")]); + flush(); + // The window: the second guess is still in force. Every read sees the + // guessed arrangement whole — a third write computing a position reads + // this too. + expect(h.rows.length).toBe(3); + expect(Object.keys(h.rows)).toEqual(["0", "1", "2"]); + expect(h.snap()).toBe("a#1,b#2,c#3"); + await p1; + + h.setSource([row("a"), row("b"), row("c")]); + flush(); + await p2; + flush(); + await sleep(5); + expect(h.snap()).toBe("a#1,b#2,c#3"); + expect(h.frames).toEqual(["a", "a,b", "a,b,c"]); + expect(h.counts()).toEqual({ mounts: 3, unmounts: 0 }); + }); + } + + it("another tab's row lands while a push is open: the screen keeps the guess until the settle, then shows both, the pushed row keeping its identity", async () => { + const h = setup([row("a")]); + const p = push(h, "b"); + flush(); + await sleep(10); + h.setSource([row("a"), row("z")]); + flush(); + expect(h.snap()).toBe("a#1,b#2"); + expect(h.frames.at(-1)).toBe("a,b"); + await sleep(5); + h.setSource([row("a"), row("z"), row("b")]); + flush(); + await p; + flush(); + await sleep(5); + expect(h.snap()).toBe("a#1,z#3,b#2"); + expect(h.frames).toEqual(["a", "a,b", "a,z,b"]); + expect(h.counts()).toEqual({ mounts: 3, unmounts: 0 }); + }); + + it("overlapping pushes with another tab's change in the window: nothing remounts, the change shows with the settle", async () => { + const h = setup([row("a")]); + const p1 = push(h, "b"); + flush(); + await sleep(2); + const p2 = push(h, "c"); + flush(); + await sleep(10); + h.setSource([row("a"), row("b")]); + flush(); + await p1; + await sleep(5); + h.setSource([row("a", "A!"), row("b"), row("z")]); + flush(); + expect(h.snap()).toBe("a#1,b#2,c#3"); + await sleep(5); + h.setSource([row("a", "A!"), row("b"), row("z"), row("c")]); + flush(); + await p2; + flush(); + await sleep(5); + expect(h.snap()).toBe("a#1,b#2,z#4,c#3"); + expect(h.rows[0].title).toBe("A!"); + expect(h.counts()).toEqual({ mounts: 4, unmounts: 0 }); + }); + + it("the second push never confirmed: it times out and reverts; the first's row keeps its identity", async () => { + const h = setup([row("a")]); + const p1 = push(h, "b", 1000); + flush(); + await sleep(2); + const p2 = push(h, "c", 60).catch(e => e); + flush(); + await sleep(10); + h.setSource([row("a"), row("b")]); + flush(); + await p1; + expect(await p2).toBeInstanceOf(TimeoutError); + flush(); + await sleep(5); + expect(h.snap()).toBe("a#1,b#2"); + expect(h.counts()).toEqual({ mounts: 3, unmounts: 1 }); + }); + + for (const where of ["in the action", "on mainline"]) { + it(`a truth longer than a reorder guess (${where}): a list pass never pairs the truth's length with the guess's rows`, async () => { + const h = setup([row("a"), row("b")]); + let release!: () => void; + const gate = new Promise(r => (release = r)); + const truth = [row("b"), row("a"), row("s")]; + const p = action(function* () { + h.set(d => void d.reverse()); + yield gate; + if (where === "in the action") h.setSource(truth); + })(); + flush(); + if (where === "on mainline") { + h.setSource(truth); + flush(); + } + release(); + await p; + flush(); + await sleep(1); + flush(); + expect(h.mapped.every(r => r !== undefined)).toBe(true); + expect(h.frames).toEqual(["a,b", "b,a", "b,a,s"]); + expect(h.counts()).toEqual({ mounts: 3, unmounts: 0 }); + }); + } +}); From e5a7819b05f27296ef0374ea65c2428cb752c8e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 21:46:47 +0000 Subject: [PATCH 3/8] perf(signals): the keyed fold pays only where optimistic stores are used The fold of a correction under a keyed arrangement guess moves into store/optimistic.ts behind GlobalQueue hooks (`fold`, `foldedRead`, `foldedHold`), so the lane engine and plain stores carry only the calls. The hooks and `Transaction.folded` are not `_`-prefixed: a new mangled name shifts every later one in the dist and costs the floors bytes. The lane staged-screen read is gated on the lane engine, so the core floor is unchanged. Caps raised with ledger entries, pending the maintainer's acceptance: + createStore (+35 B minified), + isPending/latest (+142 B), hydrating + every store primitive family (+694 B: it carries the fold itself), and CSR (+0 B minified; brotli layout noise from the shifted mangled names on a scenario next already had at the allowance). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015SnZadCVdDSeDzxBUhcMU9 --- packages/signals/docs/RULES-INDEX.md | 2 +- packages/signals/src/core/core.ts | 18 ++++-- packages/signals/src/core/lanes.ts | 57 +++--------------- packages/signals/src/core/scheduler.ts | 33 ++++++----- packages/signals/src/core/verdict.ts | 4 +- packages/signals/src/store/optimistic.ts | 73 ++++++++++++++++++++---- packages/signals/src/store/store.ts | 15 ++--- scripts/size/scenarios.js | 20 ++++--- 8 files changed, 124 insertions(+), 98 deletions(-) diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 003c7c6e4..4dcc33c21 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -61,7 +61,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:213` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… | | A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:249` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×4 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 create-deferred-3710.test.ts×2 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 show-stale-child-in-action-4137.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … | | A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:221` | scheduler.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… | -| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:71` | async.ts×3 constants.ts×1 lanes.ts×8 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… | +| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:71` | async.ts×3 constants.ts×1 lanes.ts×7 scheduler.ts×2 verdict.ts×1 map.ts×1 optimistic.ts×1 store.ts×3 | create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… | | A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:81` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×5 flight-owned-transaction.test.ts×1 lane-authority-twins.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×19 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… | | A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:149` | async.ts×1 constants.ts×1 core.ts×6 scheduler.ts×2 types.ts×1 verdict.ts×4 store.ts×1 | create-deferred-3710.test.ts×2 fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… | | A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:408` | — | question-scoped-pending.test.ts×2 spec-async-semantics.test.ts×3 createOptimisticStore.test.ts×1 | [superseded 2026-07-13 by A24] (superseded) Optimistic writes announce a store-wide pending — (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the… | diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index 3d2df2591..e73637cab 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -1082,11 +1082,7 @@ export function ext(el: { _x: NodeExtension | null }): NodeExtension { * something not ready under a loading boundary that has not shown content: * the boundary collects it and shows its fallback, and the pass is the * boundary's, not the tick's (A29's boundary exemption, #3540). */ -export function joinPass( - c: Computed, - el: Signal | Computed, - t: Transaction = txOf(el) -): void { +export function joinPass(c: Computed, el: Signal | Computed): void { c._flags |= REACTIVE_JOINED; // A new memo's first pass is the hold's work (born held) but not the // tick's: a render effect that reads it decides — under a boundary that @@ -1097,7 +1093,17 @@ export function joinPass( (el as Computed)._statusFlags & STATUS_UNINITIALIZED || (c as any)._type !== EFFECT_RENDER ) - joinPassTx(t, (!(c as any)._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c)); + joinPassTx( + txOf(el), + (!(c as any)._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c) + ); +} + +/** `joinPass` into the hold `t` (a folded guess's: store/optimistic.ts + * `foldedRead`, D-34) — a reader is never a render effect there. */ +export function joinPassAt(c: Computed, t: Transaction): void { + c._flags |= REACTIVE_JOINED; + joinPassTx(t, (!(c as any)._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c)); } /** A15's stale reader (shared-hole and reveal corollaries): a render effect diff --git a/packages/signals/src/core/lanes.ts b/packages/signals/src/core/lanes.ts index 10371a317..df31b7faf 100644 --- a/packages/signals/src/core/lanes.ts +++ b/packages/signals/src/core/lanes.ts @@ -59,7 +59,7 @@ import { STATUS_UNINITIALIZED } from "./constants.js"; import { attrHooks } from "./attribution-hooks.js"; -import { ext, joinPass, stagedRead, tracking } from "./core.js"; +import { ext, stagedRead, tracking } from "./core.js"; import { NotReadyError } from "./error.js"; import { laneValueOf } from "./truth.js"; import { enqueueSub } from "./heap.js"; @@ -536,34 +536,6 @@ function dissolveLane(l: Transaction, into: Transaction | null, except?: Signal< else l._queues[0].length = l._queues[1].length = 0; } -/** A lane a correction folded under (D-34, `laneWrite`): a deriving pass - * outside the lane composes the hold's world — each guess's truth staged - * beneath it, else the value it covered — and joins the hold, tracked or - * not (`untrack` is about dependencies, not about which world a pass - * derives from). Lane passes and the screen keep the guesses: held truth is - * masked from lane passes only (#3164). NOT_PENDING: not such a read. */ -function foldedRead(c: Computed, el: Signal | Computed): unknown { - if ( - !(el._config & CONFIG_GUESS) || - passLane !== null || - (c as any)._type === EFFECT_RENDER || - c._config & CONFIG_CHILDREN_FORBIDDEN - ) - return NOT_PENDING; - const l = txOf(el); - // The store's own derive reconciles its landings against the arrangement - // the guesses made (store/optimistic.ts): it is the author, not a reader. - if (!l._folded || (el as any)._host?.fam?.node === c) return NOT_PENDING; - joinPass(c, el, resolveTx(l._parent ?? l)); - return foldedValue(el); -} - -/** A guess's value in the hold's world: the truth staged beneath it, else - * the value it covered. */ -export function foldedValue(el: Signal | Computed): unknown { - return el._pendingValue !== NOT_PENDING ? el._pendingValue : covered(el); -} - /** Lanes. A lane's node is read by its reader's kind: an authoritative * reader (`until`) sees the truth — staged beneath a guess, else committed * — and is no lane's; an untracked read sees the lane's latest value (A17: @@ -581,8 +553,9 @@ export function foldedValue(el: Signal | Computed): unknown { * member throws like any. */ export function laneRead(c: Computed | null, el: Signal | Computed): unknown { const guess = el._config & CONFIG_GUESS; - if (c !== null) { - const v = foldedRead(c, el); + // A lane a correction folded under (D-34): store/optimistic.ts reads it. + if (c !== null && txOf(el).folded) { + const v = GlobalQueue.foldedRead!(c, el); if (v !== NOT_PENDING) return v; } if (c !== null && c._config & CONFIG_AUTHORITATIVE) return GlobalQueue._truth!(el); @@ -765,26 +738,12 @@ function laneWrite(el: Signal | Computed, v: T): T { // question is the one being asked (`question`: a slice's, a derive's // continuation's — the flight's); unstamped writes and mainline are // current. - const keyed = !stale(el, question) && GlobalQueue._keyedArrangement?.(el); - if (keyed || stale(el, question)) { + const older = stale(el, question); + // A keyed arrangement guess folds a current truth instead (D-34). + if (older || GlobalQueue.fold?.(el, v)) { el._pendingValue = v; el._config |= CONFIG_HELD; - if (keyed) { - // A keyed arrangement guess is rows by key: a current truth folds - // into the hold beneath it instead of voiding it (D-34). The guess - // stays the lane's value and the screen's. A confirmation is silent - // (A17), only truth readers are told; a correction re-derives the - // graph outside the lane now (`foldedRead`), the screen at the - // commit, where rows match by key. - const l = txOf(el); - let changed = !el._equals || !el._equals(el._x!._lane as T, v); - if (changed && GlobalQueue._echoes?.(el._x!._lane, v)) changed = false; - if (changed) { - l._folded = true; - insertSubs(el); - } else GlobalQueue._confirmed?.(l); - joinFuture(resolveTx(l._parent ?? l)); - } else GlobalQueue._laneRebase?.(el, v); + if (older) GlobalQueue._laneRebase?.(el, v); schedule(); return v; } diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts index e0fa44daf..88c4f54d2 100644 --- a/packages/signals/src/core/scheduler.ts +++ b/packages/signals/src/core/scheduler.ts @@ -247,8 +247,9 @@ export interface Transaction { _held: boolean; /** Lane: a current truth that differs folded beneath one of its keyed * guesses (lanes.ts `laneWrite`, D-34): a deriving pass outside the lane - * reads the hold's world from its guesses (`foldedRead`). */ - _folded: boolean; + * reads the hold's world from its guesses (store/optimistic.ts + * `foldedRead`). Unset until then. */ + folded?: boolean; /** `affects()` marks declared in it (affects.ts): released at the landing. * Not nodes — a mark holds nothing (`blocked` never sees one). */ _marks: Signal[] | null; @@ -273,7 +274,6 @@ export function newTransaction(lane: boolean, parent: Transaction | null = null) _links: null, _shown: false, _held: false, - _folded: false, _marks: null }; if (!lane) transactions.push(t); @@ -326,12 +326,6 @@ export function joinPassTx(t: Transaction, own?: unknown): void { else merge(resolveTx(t), passTx); schedule(); } -/** The hold a lane's guess `n` folds a correction into (`_folded`, lanes.ts - * `laneWrite`), or null. */ -export function foldedHold(n: Signal): Transaction | null { - const l = resolveTx(n._x!._transaction!); - return l._folded ? resolveTx(l._parent ?? l) : null; -} /** The transaction holding a node (CONFIG_HELD set), resolved through * merges; the path is compressed. */ export function txOf(n: Signal | Computed): Transaction { @@ -819,10 +813,23 @@ export class GlobalQueue implements IQueue { * `laneWrite`): the store re-bases an arrangement guess over it * (store/optimistic.ts). */ declare static _laneRebase: ((el: Signal, truth: unknown) => void) | undefined; - /** A landing on a node of a keyed array whose arrangement a lane guesses - * (store/optimistic.ts): the guess is rows by key, not positions, so a - * current truth folds beneath it instead of voiding it (D-34). */ - declare static _keyedArrangement: ((el: Signal) => boolean) | undefined; + // The fold's hooks and `Transaction.folded` (D-34) are not `_`-prefixed: + // a new mangled name shifts every later one (scripts/mangle-props.mjs), + // which costs the floors bytes for a path only optimistic stores reach. + /** A current truth landing on a guess (lanes.ts `laneWrite`): a keyed + * arrangement guess is rows by key, not positions, so the store folds the + * truth into the hold beneath it instead of voiding it (D-34) — true — + * and the lane stages it beneath. */ + declare static fold: ((el: Signal, truth: unknown) => boolean) | undefined; + /** A read of a folded lane's guess (`folded`): a deriving pass outside + * the lane — or `latest()`, `c` null — is served the hold's world; + * NOT_PENDING otherwise (store/optimistic.ts). */ + declare static foldedRead: + | ((c: Computed | null, el: Signal | Computed) => unknown) + | undefined; + /** The hold a container guess `k` folded a correction into, or null + * (store/optimistic.ts): a key born under it is held there. */ + declare static foldedHold: ((k: Signal) => Transaction | null) | undefined; // Lanes (lanes.ts). declare static _verdictLane: ((t: Transaction) => Transaction) | undefined; // Observe tier (attribution.ts): the guesses of `t`'s lanes (lanes.ts). diff --git a/packages/signals/src/core/verdict.ts b/packages/signals/src/core/verdict.ts index 9c136a4da..402adae57 100644 --- a/packages/signals/src/core/verdict.ts +++ b/packages/signals/src/core/verdict.ts @@ -46,7 +46,7 @@ import { import { warnStrictReadUntracked } from "./dev.js"; import { NotReadyError } from "./error.js"; import { link } from "./graph.js"; -import { display, foldedValue, laneValueOf, verdictLane } from "./lanes.js"; +import { display, laneValueOf, verdictLane } from "./lanes.js"; import { enqueueSub } from "./heap.js"; import { flushTransaction, @@ -396,7 +396,7 @@ function verdictValue(el: Signal | Computed, c: Computed | null): probeFound = true; // A lane a correction folded under (D-34): `latest()` shows the hold's // world whole — the truth beneath each guess, else what it covered. - if (latestActive && l._folded) return foldedValue(el); + if (latestActive && l.folded) return GlobalQueue.foldedRead!(null, el); return el._x!._lane; } if (probing && flying) probeFound = true; diff --git a/packages/signals/src/store/optimistic.ts b/packages/signals/src/store/optimistic.ts index 9cd39d645..cea3f4683 100644 --- a/packages/signals/src/store/optimistic.ts +++ b/packages/signals/src/store/optimistic.ts @@ -25,19 +25,26 @@ */ import { CONFIG_AUTO_DISPOSE, + CONFIG_CHILDREN_FORBIDDEN, + CONFIG_GUESS, CONFIG_OVERRIDE, + CONFIG_SLOT_NODE, + EFFECT_RENDER, NOT_PENDING, STATUS_PENDING } from "../core/constants.js"; -import { computed, ext, isEqual, read as readNode, untrack } from "../core/core.js"; +import { computed, ext, isEqual, joinPassAt, read as readNode, untrack } from "../core/core.js"; import { laneValueOf, optimisticWrite, pendingGuessOf } from "../core/lanes.js"; import { getOwner } from "../core/owner.js"; import { GlobalQueue, insertSubs, + joinFuture, laneDirty, list, notifyEpoch, + passLane, + resolveTx, schedule, txOf } from "../core/scheduler.js"; @@ -381,16 +388,62 @@ GlobalQueue._laneRebase = (el: Signal, truth: unknown): void => { } }; -GlobalQueue._keyedArrangement = (el: Signal): boolean => { +/** D-34 for a keyed arrangement guess: rows by key, not positions. A + * current truth that differs folds into the hold beneath it instead of + * voiding the lane: the guess stays the lane's value and the screen's until + * the hold commits, where rows match by key, and the graph outside the lane + * re-derives from the truth now (`foldedRead`). A confirmation is silent + * (A17): only truth readers are told. True: the lane stages the truth + * beneath (`laneWrite`). */ +GlobalQueue.fold = (el: Signal, truth: unknown): boolean => { const t = (el as any)._host as StoreTarget | undefined; - if (t === undefined || (t.fam?.key ?? null) === null) return false; - const k = t.k; - return ( - k !== null && - (k._config & CONFIG_OVERRIDE) !== 0 && - k._x!._lane instanceof LaneView && - Array.isArray(k._x!._lane.rows) - ); + const k = t?.k; + if ( + k == null || + t!.fam?.key == null || + !(k._config & CONFIG_OVERRIDE) || + !(k._x!._lane instanceof LaneView) || + !Array.isArray(k._x!._lane.rows) + ) + return false; + const l = txOf(el); + const guess = el._x!._lane; + if ((!el._equals || !el._equals(guess, truth)) && !GlobalQueue._echoes!(guess, truth)) { + l.folded = true; + insertSubs(el); + } else GlobalQueue._confirmed?.(l); + joinFuture(resolveTx(l._parent ?? l)); + return true; +}; + +/** A folded lane's guess read by a deriving pass outside the lane, tracked + * or not (`untrack` is about dependencies, not about which world a pass + * derives from), or by `latest()` (`c` null): the hold's world — the truth + * staged beneath the guess, else the value it covered — and the pass joins + * the hold. Lane passes and the screen keep the guess: held truth is masked + * from lane passes only (#3164). The store's own derive reconciles its + * landings against the arrangement the guesses made: it is the author, not + * a reader. */ +GlobalQueue.foldedRead = (c, el) => { + if (c !== null) { + if ( + !(el._config & CONFIG_GUESS) || + passLane !== null || + (c as any)._type === EFFECT_RENDER || + c._config & CONFIG_CHILDREN_FORBIDDEN || + (el as any)._host?.fam?.node === c + ) + return NOT_PENDING; + const l = txOf(el); + joinPassAt(c, resolveTx(l._parent ?? l)); + } + if (el._pendingValue !== NOT_PENDING) return el._pendingValue; + return el._config & CONFIG_SLOT_NODE ? GlobalQueue._slotCovered!(el) : el._value; +}; + +GlobalQueue.foldedHold = k => { + const l = txOf(k); + return l.folded ? resolveTx(l._parent ?? l) : null; }; function sameRow(a: unknown, b: unknown, keyFn: (item: any) => any): boolean { diff --git a/packages/signals/src/store/store.ts b/packages/signals/src/store/store.ts index 5ee229f14..5f5005cee 100644 --- a/packages/signals/src/store/store.ts +++ b/packages/signals/src/store/store.ts @@ -84,7 +84,6 @@ import { flushTransaction, GlobalQueue, globalQueue, - foldedHold, holdNode, insertSubs, joinFuture, @@ -371,14 +370,12 @@ function bornStaged(target: StoreTarget, node: Signal, staged: any, key: Pr if (k === null) return; // a node-less staging holds nothing (`stageOn`) // Held by the container's transaction only for a key the HELD staging // changed — a mainline layer's change above it stages mainline. (A - // container carrying a guess is a lane's: the flush's park decides.) - if ((k._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD && heldKeyChanged(target, key)) - holdNode(node, txOf(k)); - // A guess a correction folded under (D-34): the truth beneath it is the - // hold's, and so is a key it changed. - else if (k._config & CONFIG_OVERRIDE && k._config & CONFIG_HELD && heldKeyChanged(target, key)) { - const t = foldedHold(k); - if (t !== null) holdNode(node, t); + // container carrying a guess is a lane's: the flush's park decides — + // unless a correction folded under the guess, D-34: the key is that + // correction's hold's.) + if (k._config & CONFIG_HELD && heldKeyChanged(target, key)) { + const t = k._config & CONFIG_OVERRIDE ? GlobalQueue.foldedHold?.(k) : txOf(k); + if (t) holdNode(node, t); } // A28 (4): a batch written inside a creation-time pass is promoted — its // late-materialized leaf with it. diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 3794728de..0376ad4fe 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1093,8 +1093,9 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): 14.90 KB -> 14.94 KB, recorded minified 45,089 -> 45,115 B (+26 B), measured in CI at 14,927 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 14.94 -> 14.95 KB, recorded minified 45,115 -> 45,175 B, measured locally (linux) at 14,932 B brotli / 45,175 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): cap 14.95 KB unchanged, recorded minified 45,175 -> 45,145 B (-30 B), measured in CI at 14,914 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - limit: "14.95 KB", - capMinified: 45145, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 14.95 -> 15.01 KB, recorded minified 45,145 -> 45,201 B, measured locally (linux) at 14,993 B brotli / 45,201 B minified (+35 B minified over next): a key born under a container guess that a correction folded under is held in that correction's hold. The fold itself lives in store/optimistic.ts behind GlobalQueue hooks, so a plain store carries only the birth check. Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "15.01 KB", + capMinified: 45201, alias }, { @@ -1402,8 +1403,9 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): 10.12 KB -> 10.14 KB, recorded minified 28,356 -> 28,383 B (+27 B), measured in CI at 10,121 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 10.14 -> 10.16 KB, recorded minified 28,383 -> 28,443 B, measured locally (linux) at 10,144 B brotli / 28,443 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 10.16 KB -> 10.19 KB, recorded minified 28,443 -> 28,463 B (+20 B), measured in CI at 10,173 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - limit: "10.19 KB", - capMinified: 28463, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 10.19 -> 10.25 KB, recorded minified 28,463 -> 28,644 B, measured locally (linux) at 10,239 B brotli / 28,644 B minified (+142 B minified over next): the lane engine asks the optimistic store to fold a current truth under a keyed guess (`laneWrite`), serves a folded lane's guesses to deriving passes outside the lane (`laneRead`) and to `latest()` (`verdictValue`) through GlobalQueue hooks, and lane work reads a frame's stagings as the screen once the frame has a transaction. Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "10.25 KB", + capMinified: 28644, alias }, { @@ -2635,8 +2637,9 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): cap 30.30 KB unchanged, recorded minified 94,999 -> 95,025 B (+26 B), measured in CI at 30,267 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 30.30 -> 30.32 KB, recorded minified 95,025 -> 95,085 B, measured locally (linux) at 30,308 B brotli / 95,085 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 30.32 KB -> 30.41 KB, recorded minified 95,085 -> 95,109 B (+24 B), measured in CI at 30,394 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - limit: "30.41 KB", - capMinified: 95109, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 30.41 -> 30.55 KB, recorded minified 95,109 -> 95,842 B, measured locally (linux) at 30,537 B brotli / 95,842 B minified (+694 B minified over next): this scenario carries createOptimisticStore, so it pays for the fold itself (store/optimistic.ts `fold`, `foldedRead`, `foldedHold`) beside the lane and store hooks above. Correctness: two overlapping writes on one keyed store no longer tear the store or remount the confirmed row. CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "30.55 KB", + capMinified: 95842, alias }, { @@ -2925,8 +2928,9 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): cap 13.24 KB unchanged, recorded minified 37,060 -> 37,087 B (+27 B), measured in CI at 13,217 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 13.24 -> 13.28 KB, recorded minified 37,087 -> 37,147 B, measured locally (linux) at 13,264 B brotli / 37,147 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): cap 13.28 KB unchanged, recorded minified 37,147 -> 37,113 B (-34 B), measured in CI at 13,249 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - limit: "13.28 KB", - capMinified: 37113, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 13.28 -> 13.30 KB, recorded minified 37,113 -> 37,134 B, measured locally (linux) at 13,290 B brotli / 37,134 B minified (+0 B minified over next): no code of this change reaches this scenario; the dist's mangled property names shift with it and brotli lands +45 B, while next already sat 21 B past the recorded minified (1 B past the allowance). Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "13.30 KB", + capMinified: 37134, alias }, { From f143b9c214a3d57472f62928656368cf5b280fb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 22:43:51 +0000 Subject: [PATCH 4/8] fix(signals): golf the keyed fold; void an unshown keyed guess; pin until's posture - read(): a lane pass reads the frame's stagings as the screen only over a folded lane, gated on the lane engine (floors +0 B minified). - One store hook for the re-base and the fold (_laneRebase), and a folded lane carries the store's reader (Transaction.folded): fewer unmangled names in core. - D-34: a keyed guess the screen never showed is void at a correction, as any guess's; the fold applies to a shown one only. - laneRead: the fold's arm stays before the authoritative one so an until outside the lane joins the hold (D-31); pinned by a new test. - Size caps re-recorded on the golfed numbers. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015SnZadCVdDSeDzxBUhcMU9 --- packages/signals/docs/RULES-DERIVED.md | 2 +- packages/signals/src/core/core.ts | 13 +-- packages/signals/src/core/lanes.ts | 16 ++-- packages/signals/src/core/scheduler.ts | 39 ++++----- packages/signals/src/core/verdict.ts | 2 +- packages/signals/src/store/optimistic.ts | 29 +++---- packages/signals/src/store/store.ts | 5 +- .../tests/optimistic-keyed-fold.test.ts | 84 +++++++++++++++++++ scripts/size/scenarios.js | 21 ++--- 9 files changed, 145 insertions(+), 66 deletions(-) diff --git a/packages/signals/docs/RULES-DERIVED.md b/packages/signals/docs/RULES-DERIVED.md index 529c71c35..fe2c79dfe 100644 --- a/packages/signals/docs/RULES-DERIVED.md +++ b/packages/signals/docs/RULES-DERIVED.md @@ -286,7 +286,7 @@ An override's lifetime is that of the hold it was made over — in the usual sha When an override's own source answers (its own landing, or a sync recompute from an upstream change): an equal answer confirms silently — nothing re-runs; a different answer corrects — derivations re-derive from the truth now as the hold's work (downstream refetches restart at once, no waterfall), the screen and untracked reads keep what the screen showed until the commit, `latest()` answers the truth and `isPending()` reads `true` while they differ, and the correction reveals with the hold. "Keep the override" means keep what _showed_: a guess the screen never displayed — its reveal held on its own downstream flight, including through a shown `Loading` — is void at the correction, and untracked reads see the committed value, as the screen does. A later landing equal to the override restores it as the graph's value. -A keyed store's arrangement guess (rows by key) is not voided by a correction: the truth folds beneath it. Derivations outside the lane re-derive from the truth now and join the hold, `until`/`resolve` keep their postures over it, and `latest()` shows the hold's world whole. Lane passes, untracked reads and the screen keep the guess until the commit, and at the commit rows match by key. So a row the truth returns keeps its identity, a row another tab added shows at the commit, and the first of two overlapping actions confirming does not void the second's guess (2026-10-21). A list pass inside a frame that already has a transaction reads that frame's stagings as the screen, as a verdict reader does, so it never pairs the truth's length with the guess's rows. +A keyed store's arrangement guess (rows by key) that the screen showed is not voided by a correction: the truth folds beneath it (one the screen never showed is void, as above). Derivations outside the lane re-derive from the truth now and join the hold, `until`/`resolve` keep their postures over it (an `until` outside the lane reads the truth and joins the hold, D-31), and `latest()` shows the hold's world whole. Lane passes, untracked reads and the screen keep the guess until the commit, and at the commit rows match by key. So a row the truth returns keeps its identity, a row another tab added shows at the commit, and the first of two overlapping actions confirming does not void the second's guess (2026-10-21). A folded lane's list pass inside a frame that already has a transaction reads that frame's stagings as the screen, as a verdict reader does, so it never pairs the truth's length with the guess's rows. **Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#a18-an-override-lives-exactly-as-long-as-its-own-transaction-a-newer-truth-from-the-source-supersedes-it-in-the-graph-immediately-on-screen-at-commit) supersession (#3331, 2026-09-09/10) and (d) before the first commit, "Correction display is what showed" (2026-10-05), lane contract 2, lane ruling 5, [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) (2026-09-09), OS-R39. **Status:** standing. diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index e73637cab..8438198af 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -1817,9 +1817,10 @@ export function read(el: Signal | Computed): T { if (!c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN) return el._value as T; // (A plain signal is never uninitialized — the carve-out is `serve`'s.) - // Lane work, once the frame has a transaction, reads the screen too. + // Lane work over a folded lane (D-34), once the frame has a + // transaction, reads the screen too. if ( - ((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane !== null)) && + ((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane?.folded)) && stagedScreen(c as Computed) ) return el._value as T; @@ -2039,10 +2040,10 @@ export function serve(el: Signal | Computed, c: Computed | null): /** A pass read a staging of this flush: it derives from what the flush may * yet hold (REACTIVE_STAGED_READ — the seam decides). Lane work too (plan sec. 28, a - * lane sees the screen plus its own guesses), before the frame has a - * transaction (after, it reads the screen like a verdict reader, - * `stagedScreen` — a list pass never pairs a held length with the guess's - * rows, D-34): the staging is the screen if + * lane sees the screen plus its own guesses) — a folded lane's only before + * the frame has a transaction (after, it reads the screen like a verdict + * reader, `stagedScreen` — a list pass never pairs a held length with the + * guess's rows, D-34): the staging is the screen if * the frame commits — one pass, the common case — and a held write if it * parks, which the seam repairs (`stagedReaders`): the pass re-derives on * the committed world and its lane's runs wait that round, so the held diff --git a/packages/signals/src/core/lanes.ts b/packages/signals/src/core/lanes.ts index df31b7faf..6c1ab8590 100644 --- a/packages/signals/src/core/lanes.ts +++ b/packages/signals/src/core/lanes.ts @@ -553,9 +553,14 @@ function dissolveLane(l: Transaction, into: Transaction | null, except?: Signal< * member throws like any. */ export function laneRead(c: Computed | null, el: Signal | Computed): unknown { const guess = el._config & CONFIG_GUESS; + const l = txOf(el); // A lane a correction folded under (D-34): store/optimistic.ts reads it. - if (c !== null && txOf(el).folded) { - const v = GlobalQueue.foldedRead!(c, el); + // Before the authoritative arm: an `until` outside the lane reads the + // same truth there, and joins the hold it folded into — its posture over + // a foreign hold (D-31), which the truth read alone would not take. + const folded = c !== null && l.folded; + if (folded) { + const v = folded(c, el); if (v !== NOT_PENDING) return v; } if (c !== null && c._config & CONFIG_AUTHORITATIVE) return GlobalQueue._truth!(el); @@ -563,7 +568,6 @@ export function laneRead(c: Computed | null, el: Signal | Computed(el: Signal | Computed, v: T): T { // question is the one being asked (`question`: a slice's, a derive's // continuation's — the flight's); unstamped writes and mainline are // current. + // A store re-bases a keyed arrangement guess over an older truth, and + // folds a current one beneath it (D-34): true. const older = stale(el, question); - // A keyed arrangement guess folds a current truth instead (D-34). - if (older || GlobalQueue.fold?.(el, v)) { + if (GlobalQueue._laneRebase?.(el, v, older) || older) { el._pendingValue = v; el._config |= CONFIG_HELD; - if (older) GlobalQueue._laneRebase?.(el, v); schedule(); return v; } diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts index 88c4f54d2..c4a3f4bb4 100644 --- a/packages/signals/src/core/scheduler.ts +++ b/packages/signals/src/core/scheduler.ts @@ -246,10 +246,14 @@ export interface Transaction { * (#3460); one reading a lane the seam has not judged yet is its work. */ _held: boolean; /** Lane: a current truth that differs folded beneath one of its keyed - * guesses (lanes.ts `laneWrite`, D-34): a deriving pass outside the lane - * reads the hold's world from its guesses (store/optimistic.ts - * `foldedRead`). Unset until then. */ - folded?: boolean; + * guesses (lanes.ts `laneWrite`, D-34) — the store's reader of it: a + * deriving pass outside the lane, or `latest()` (`c` null), is served + * the hold's world from its guesses; NOT_PENDING otherwise + * (store/optimistic.ts `foldedRead`). Unset until then; not + * `_`-prefixed: a new mangled name shifts every later one + * (scripts/mangle-props.mjs), which costs the floors bytes for a path + * only optimistic stores reach. */ + folded?: (c: Computed | null, el: Signal | Computed) => unknown; /** `affects()` marks declared in it (affects.ts): released at the landing. * Not nodes — a mark holds nothing (`blocked` never sees one). */ _marks: Signal[] | null; @@ -809,27 +813,14 @@ export class GlobalQueue implements IQueue { declare static _storeWrappable: ((v: unknown) => boolean) | undefined; declare static _mark: ((node: Signal | Computed) => void) | undefined; declare static _witnessMark: (() => void) | undefined; - /** An older question's truth was held beneath a guess (lanes.ts - * `laneWrite`): the store re-bases an arrangement guess over it - * (store/optimistic.ts). */ - declare static _laneRebase: ((el: Signal, truth: unknown) => void) | undefined; - // The fold's hooks and `Transaction.folded` (D-34) are not `_`-prefixed: - // a new mangled name shifts every later one (scripts/mangle-props.mjs), - // which costs the floors bytes for a path only optimistic stores reach. - /** A current truth landing on a guess (lanes.ts `laneWrite`): a keyed - * arrangement guess is rows by key, not positions, so the store folds the - * truth into the hold beneath it instead of voiding it (D-34) — true — - * and the lane stages it beneath. */ - declare static fold: ((el: Signal, truth: unknown) => boolean) | undefined; - /** A read of a folded lane's guess (`folded`): a deriving pass outside - * the lane — or `latest()`, `c` null — is served the hold's world; - * NOT_PENDING otherwise (store/optimistic.ts). */ - declare static foldedRead: - | ((c: Computed | null, el: Signal | Computed) => unknown) + /** A truth landing on a guess (lanes.ts `laneWrite`), `older` when it + * answers an older question: the store re-bases an arrangement guess over + * it; a current one differing from a shown keyed arrangement guess folds + * beneath it instead of voiding it (D-34) — true: the lane stages it + * beneath (store/optimistic.ts). */ + declare static _laneRebase: + | ((el: Signal, truth: unknown, older: boolean) => boolean | void) | undefined; - /** The hold a container guess `k` folded a correction into, or null - * (store/optimistic.ts): a key born under it is held there. */ - declare static foldedHold: ((k: Signal) => Transaction | null) | undefined; // Lanes (lanes.ts). declare static _verdictLane: ((t: Transaction) => Transaction) | undefined; // Observe tier (attribution.ts): the guesses of `t`'s lanes (lanes.ts). diff --git a/packages/signals/src/core/verdict.ts b/packages/signals/src/core/verdict.ts index 402adae57..8da924be1 100644 --- a/packages/signals/src/core/verdict.ts +++ b/packages/signals/src/core/verdict.ts @@ -396,7 +396,7 @@ function verdictValue(el: Signal | Computed, c: Computed | null): probeFound = true; // A lane a correction folded under (D-34): `latest()` shows the hold's // world whole — the truth beneath each guess, else what it covered. - if (latestActive && l.folded) return GlobalQueue.foldedRead!(null, el); + if (latestActive && l.folded) return l.folded(null, el); return el._x!._lane; } if (probing && flying) probeFound = true; diff --git a/packages/signals/src/store/optimistic.ts b/packages/signals/src/store/optimistic.ts index cea3f4683..5522b5ecf 100644 --- a/packages/signals/src/store/optimistic.ts +++ b/packages/signals/src/store/optimistic.ts @@ -326,7 +326,8 @@ function reconcileDraft(draft: any, incoming: any, keyFn: KeyFn | null): void { * committed length differs, and as the `LaneView`'s base for structural * reads. Unkeyed families compose * positionally — the guess stands as written (the documented residue). */ -GlobalQueue._laneRebase = (el: Signal, truth: unknown): void => { +GlobalQueue._laneRebase = (el: Signal, truth: unknown, older: boolean) => { + if (!older) return fold(el, truth); const t = (el as any)._host as StoreTarget | undefined; if (t === undefined || (el as any)._key !== $TRACK) return; const view = el._x!._lane; @@ -395,26 +396,27 @@ GlobalQueue._laneRebase = (el: Signal, truth: unknown): void => { * re-derives from the truth now (`foldedRead`). A confirmation is silent * (A17): only truth readers are told. True: the lane stages the truth * beneath (`laneWrite`). */ -GlobalQueue.fold = (el: Signal, truth: unknown): boolean => { +function fold(el: Signal, truth: unknown): boolean { const t = (el as any)._host as StoreTarget | undefined; const k = t?.k; + const l = txOf(el); + // A guess the screen never showed is void at the correction (D-34). if ( - k == null || + !l._shown || + !(k?._config! & CONFIG_OVERRIDE) || t!.fam?.key == null || - !(k._config & CONFIG_OVERRIDE) || - !(k._x!._lane instanceof LaneView) || - !Array.isArray(k._x!._lane.rows) + !(k!._x!._lane instanceof LaneView) || + !Array.isArray(k!._x!._lane.rows) ) return false; - const l = txOf(el); const guess = el._x!._lane; if ((!el._equals || !el._equals(guess, truth)) && !GlobalQueue._echoes!(guess, truth)) { - l.folded = true; + l.folded = foldedRead; insertSubs(el); } else GlobalQueue._confirmed?.(l); joinFuture(resolveTx(l._parent ?? l)); return true; -}; +} /** A folded lane's guess read by a deriving pass outside the lane, tracked * or not (`untrack` is about dependencies, not about which world a pass @@ -424,7 +426,7 @@ GlobalQueue.fold = (el: Signal, truth: unknown): boolean => { * from lane passes only (#3164). The store's own derive reconciles its * landings against the arrangement the guesses made: it is the author, not * a reader. */ -GlobalQueue.foldedRead = (c, el) => { +function foldedRead(c: Computed | null, el: Signal | Computed): unknown { if (c !== null) { if ( !(el._config & CONFIG_GUESS) || @@ -439,12 +441,7 @@ GlobalQueue.foldedRead = (c, el) => { } if (el._pendingValue !== NOT_PENDING) return el._pendingValue; return el._config & CONFIG_SLOT_NODE ? GlobalQueue._slotCovered!(el) : el._value; -}; - -GlobalQueue.foldedHold = k => { - const l = txOf(k); - return l.folded ? resolveTx(l._parent ?? l) : null; -}; +} function sameRow(a: unknown, b: unknown, keyFn: (item: any) => any): boolean { if (a === b) return true; diff --git a/packages/signals/src/store/store.ts b/packages/signals/src/store/store.ts index 5f5005cee..9d5a8d03b 100644 --- a/packages/signals/src/store/store.ts +++ b/packages/signals/src/store/store.ts @@ -91,6 +91,7 @@ import { passLane, queuePendingNode, schedule, + resolveTx, txOf, type Transaction, installStoreHooks @@ -374,8 +375,8 @@ function bornStaged(target: StoreTarget, node: Signal, staged: any, key: Pr // unless a correction folded under the guess, D-34: the key is that // correction's hold's.) if (k._config & CONFIG_HELD && heldKeyChanged(target, key)) { - const t = k._config & CONFIG_OVERRIDE ? GlobalQueue.foldedHold?.(k) : txOf(k); - if (t) holdNode(node, t); + const l = txOf(k); + if (!(k._config & CONFIG_OVERRIDE) || l.folded) holdNode(node, resolveTx(l._parent ?? l)); } // A28 (4): a batch written inside a creation-time pass is promoted — its // late-materialized leaf with it. diff --git a/packages/signals/tests/optimistic-keyed-fold.test.ts b/packages/signals/tests/optimistic-keyed-fold.test.ts index 3d1b5d60f..d12cd0d23 100644 --- a/packages/signals/tests/optimistic-keyed-fold.test.ts +++ b/packages/signals/tests/optimistic-keyed-fold.test.ts @@ -17,11 +17,14 @@ import { createOptimisticStore, createRenderEffect, createRoot, + createMemo, createSignal, flush, mapArray, onCleanup, + resolve, TimeoutError, + untrack, until } from "../src/index.js"; @@ -187,6 +190,87 @@ describe("keyed arrangement guess: a correction folds beneath it (D-34)", () => expect(h.counts()).toEqual({ mounts: 3, unmounts: 1 }); }); + it("a mainline `until` over a folded lane keeps its posture: it reads the truth, joins the hold and delivers at its commit (D-31)", async () => { + const h = setup([row("a")]); + const p1 = push(h, "b"); + flush(); + await sleep(2); + const p2 = push(h, "c"); + flush(); + await sleep(10); + h.setSource([row("a"), row("b")]); + flush(); + const log: string[] = []; + // The truth confirms `b`; a mainline wait over the hold delivers at its + // commit, never in the window (the truth read alone would). + void until(() => h.rows.some(r => r.id === "b")).then(() => log.push("b")); + // `c` is a guess the truth has not confirmed: invisible to `until`. + void until(() => h.rows.some(r => r.id === "c")).then(() => log.push("c")); + void resolve(() => h.rows.length).then(n => log.push(`resolve ${n}`)); + await p1; + await sleep(5); + expect(log).toEqual([]); + h.setSource([row("a"), row("b"), row("c")]); + flush(); + await p2; + flush(); + await sleep(5); + expect(log.sort()).toEqual(["b", "c", "resolve 3"]); + }); + + it("a keyed guess the screen never showed is void at a correction, as any guess's (D-34)", async () => { + const [source, setSource] = createSignal([row("a")]); + let s!: Row[]; + let set!: (fn: (d: Row[]) => void) => void; + const frames: string[] = []; + createRoot(() => { + [s, set] = createOptimisticStore(() => source(), []); + // The lane's reveal waits on its own downstream flight. + const n = createMemo(async () => { + const v = s.length; + await sleep(20); + return v; + }); + const rows = mapArray( + () => s, + (r: Row) => r + ); + createRenderEffect( + () => + rows() + .map(r => r.id) + .join(",") + + "|" + + n(), + v => void frames.push(v) + ); + }); + flush(); + await sleep(30); + flush(); + const p = action(function* () { + set(d => void d.push(row("b"))); + yield sleep(5); + yield until(() => s.some(r => r.id === "b")); + })(); + flush(); + await sleep(5); + { + setSource([row("a"), row("z")]); + flush(); + // Never shown, the guess is void: untracked reads see the committed + // value, as the screen does. + expect(untrack(() => s.map(r => r.id).join(","))).toBe("a"); + } + await sleep(5); + setSource([row("a"), row("z"), row("b")]); + flush(); + await p; + await sleep(40); + flush(); + expect(frames).toEqual(["a|1", "a,z,b|3"]); + }); + for (const where of ["in the action", "on mainline"]) { it(`a truth longer than a reorder guess (${where}): a list pass never pairs the truth's length with the guess's rows`, async () => { const h = setup([row("a"), row("b")]); diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 0376ad4fe..d6b173f2c 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1093,9 +1093,9 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): 14.90 KB -> 14.94 KB, recorded minified 45,089 -> 45,115 B (+26 B), measured in CI at 14,927 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 14.94 -> 14.95 KB, recorded minified 45,115 -> 45,175 B, measured locally (linux) at 14,932 B brotli / 45,175 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): cap 14.95 KB unchanged, recorded minified 45,175 -> 45,145 B (-30 B), measured in CI at 14,914 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 14.95 -> 15.01 KB, recorded minified 45,145 -> 45,201 B, measured locally (linux) at 14,993 B brotli / 45,201 B minified (+35 B minified over next): a key born under a container guess that a correction folded under is held in that correction's hold. The fold itself lives in store/optimistic.ts behind GlobalQueue hooks, so a plain store carries only the birth check. Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "15.01 KB", - capMinified: 45201, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 14.95 -> 14.98 KB, recorded minified 45,145 -> 45,210 B, measured locally (linux) at 14,967 B brotli / 45,210 B minified (+44 B minified over next): a key born under a container guess that a correction folded under is held in that correction's hold. The fold itself lives in store/optimistic.ts behind the lane's `_laneRebase` hook, so a plain store carries only the birth check. Golfed at the maintainer's request (was +35 B with a hook here, +142 B core). Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "14.98 KB", + capMinified: 45210, alias }, { @@ -1403,9 +1403,9 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): 10.12 KB -> 10.14 KB, recorded minified 28,356 -> 28,383 B (+27 B), measured in CI at 10,121 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 10.14 -> 10.16 KB, recorded minified 28,383 -> 28,443 B, measured locally (linux) at 10,144 B brotli / 28,443 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 10.16 KB -> 10.19 KB, recorded minified 28,443 -> 28,463 B (+20 B), measured in CI at 10,173 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 10.19 -> 10.25 KB, recorded minified 28,463 -> 28,644 B, measured locally (linux) at 10,239 B brotli / 28,644 B minified (+142 B minified over next): the lane engine asks the optimistic store to fold a current truth under a keyed guess (`laneWrite`), serves a folded lane's guesses to deriving passes outside the lane (`laneRead`) and to `latest()` (`verdictValue`) through GlobalQueue hooks, and lane work reads a frame's stagings as the screen once the frame has a transaction. Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 10.19 -> 10.25 KB, recorded minified 28,463 -> 28,612 B, measured locally (linux) at 10,234 B brotli / 28,612 B minified (+110 B minified over next, golfed from +142 B at the maintainer's request): `laneWrite` hands a current truth on a guess to the store's `_laneRebase` hook (one hook for the re-base and the fold), and a folded lane carries the store's reader of it (`Transaction.folded`), which `laneRead`, `latest()` (`verdictValue`) and a folded lane's list pass in `read()` use. The floors carry none of it (`read()`'s check is gated on the lane engine). Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: "10.25 KB", - capMinified: 28644, + capMinified: 28612, alias }, { @@ -2637,9 +2637,9 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): cap 30.30 KB unchanged, recorded minified 94,999 -> 95,025 B (+26 B), measured in CI at 30,267 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 30.30 -> 30.32 KB, recorded minified 95,025 -> 95,085 B, measured locally (linux) at 30,308 B brotli / 95,085 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 30.32 KB -> 30.41 KB, recorded minified 95,085 -> 95,109 B (+24 B), measured in CI at 30,394 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 30.41 -> 30.55 KB, recorded minified 95,109 -> 95,842 B, measured locally (linux) at 30,537 B brotli / 95,842 B minified (+694 B minified over next): this scenario carries createOptimisticStore, so it pays for the fold itself (store/optimistic.ts `fold`, `foldedRead`, `foldedHold`) beside the lane and store hooks above. Correctness: two overlapping writes on one keyed store no longer tear the store or remount the confirmed row. CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "30.55 KB", - capMinified: 95842, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 30.41 -> 30.62 KB, recorded minified 95,109 -> 95,775 B, measured locally (linux) at 30,605 B brotli / 95,775 B minified (+627 B minified over next, golfed from +694 B at the maintainer's request): this scenario carries createOptimisticStore, so it pays for the fold itself (store/optimistic.ts `fold`, `foldedRead`, core `joinPassAt`) beside the lane and store changes above. Correctness: two overlapping writes on one keyed store no longer tear the store or remount the confirmed row. CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "30.62 KB", + capMinified: 95775, alias }, { @@ -3979,8 +3979,9 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 26.10 KB (unchanged), recorded minified 81,112 -> 81,172 B, measured locally (linux) at 26,087 B brotli / 81,172 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 26.10 KB -> 26.11 KB, recorded minified 81,172 -> 81,138 B (-34 B), measured in CI at 26,092 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Re-recorded on next @ 0222d684 (2026-10-21, accepted by the maintainer): 26.11 KB -> 26.14 KB, recorded minified 81,138 -> 81,159 B (+21 B), measured in CI at 26,123 B brotli. #4188 (reaction arms captured before tracking reads), #4207 (#4203, unread pending memo under a verdict mount) and #4176 (deferred releases restored after a throwing cleanup) each passed alone and together put next 1 B past the 20 B minified allowance. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "26.14 KB", - capMinified: 81159, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 26.14 -> 26.18 KB, recorded minified 81,159 -> 81,203 B, measured locally (linux) at 26,169 B brotli / 81,203 B minified (+44 B minified over next): the app's store pays the birth check under a folded guess (see `signals: + createStore`); brotli also moves with the dist's mangled names, which this change reorders. Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "26.18 KB", + capMinified: 81203, alias }, { From 00ce0acccb445041f266d70657a8695aa3646d44 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 00:46:22 +0000 Subject: [PATCH 5/8] chore(size): record the maintainer's acceptance of the keyed-fold cap raises Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015SnZadCVdDSeDzxBUhcMU9 --- scripts/size/scenarios.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index d6b173f2c..da3f2ac16 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1093,7 +1093,7 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): 14.90 KB -> 14.94 KB, recorded minified 45,089 -> 45,115 B (+26 B), measured in CI at 14,927 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 14.94 -> 14.95 KB, recorded minified 45,115 -> 45,175 B, measured locally (linux) at 14,932 B brotli / 45,175 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): cap 14.95 KB unchanged, recorded minified 45,175 -> 45,145 B (-30 B), measured in CI at 14,914 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 14.95 -> 14.98 KB, recorded minified 45,145 -> 45,210 B, measured locally (linux) at 14,967 B brotli / 45,210 B minified (+44 B minified over next): a key born under a container guess that a correction folded under is held in that correction's hold. The fold itself lives in store/optimistic.ts behind the lane's `_laneRebase` hook, so a plain store carries only the birth check. Golfed at the maintainer's request (was +35 B with a hook here, +142 B core). Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 14.95 -> 14.98 KB, recorded minified 45,145 -> 45,210 B, measured locally (linux) at 14,967 B brotli / 45,210 B minified (+44 B minified over next): a key born under a container guess that a correction folded under is held in that correction's hold. The fold itself lives in store/optimistic.ts behind the lane's `_laneRebase` hook, so a plain store carries only the birth check. Golfed at the maintainer's request (was +35 B with a hook here, +142 B core). Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: "14.98 KB", capMinified: 45210, alias @@ -1403,7 +1403,7 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): 10.12 KB -> 10.14 KB, recorded minified 28,356 -> 28,383 B (+27 B), measured in CI at 10,121 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 10.14 -> 10.16 KB, recorded minified 28,383 -> 28,443 B, measured locally (linux) at 10,144 B brotli / 28,443 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 10.16 KB -> 10.19 KB, recorded minified 28,443 -> 28,463 B (+20 B), measured in CI at 10,173 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 10.19 -> 10.25 KB, recorded minified 28,463 -> 28,612 B, measured locally (linux) at 10,234 B brotli / 28,612 B minified (+110 B minified over next, golfed from +142 B at the maintainer's request): `laneWrite` hands a current truth on a guess to the store's `_laneRebase` hook (one hook for the re-base and the fold), and a folded lane carries the store's reader of it (`Transaction.folded`), which `laneRead`, `latest()` (`verdictValue`) and a folded lane's list pass in `read()` use. The floors carry none of it (`read()`'s check is gated on the lane engine). Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 10.19 -> 10.25 KB, recorded minified 28,463 -> 28,612 B, measured locally (linux) at 10,234 B brotli / 28,612 B minified (+110 B minified over next, golfed from +142 B at the maintainer's request): `laneWrite` hands a current truth on a guess to the store's `_laneRebase` hook (one hook for the re-base and the fold), and a folded lane carries the store's reader of it (`Transaction.folded`), which `laneRead`, `latest()` (`verdictValue`) and a folded lane's list pass in `read()` use. The floors carry none of it (`read()`'s check is gated on the lane engine). Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: "10.25 KB", capMinified: 28612, alias @@ -2637,7 +2637,7 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): cap 30.30 KB unchanged, recorded minified 94,999 -> 95,025 B (+26 B), measured in CI at 30,267 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 30.30 -> 30.32 KB, recorded minified 95,025 -> 95,085 B, measured locally (linux) at 30,308 B brotli / 95,085 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 30.32 KB -> 30.41 KB, recorded minified 95,085 -> 95,109 B (+24 B), measured in CI at 30,394 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 30.41 -> 30.62 KB, recorded minified 95,109 -> 95,775 B, measured locally (linux) at 30,605 B brotli / 95,775 B minified (+627 B minified over next, golfed from +694 B at the maintainer's request): this scenario carries createOptimisticStore, so it pays for the fold itself (store/optimistic.ts `fold`, `foldedRead`, core `joinPassAt`) beside the lane and store changes above. Correctness: two overlapping writes on one keyed store no longer tear the store or remount the confirmed row. CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 30.41 -> 30.62 KB, recorded minified 95,109 -> 95,775 B, measured locally (linux) at 30,605 B brotli / 95,775 B minified (+627 B minified over next, golfed from +694 B at the maintainer's request): this scenario carries createOptimisticStore, so it pays for the fold itself (store/optimistic.ts `fold`, `foldedRead`, core `joinPassAt`) beside the lane and store changes above. Correctness: two overlapping writes on one keyed store no longer tear the store or remount the confirmed row. CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: "30.62 KB", capMinified: 95775, alias @@ -2928,7 +2928,7 @@ module.exports = [ // Every comparator call guarded, default comparators on a fast path (#4111, 2026-10-20, over next @ 6371b910): cap 13.24 KB unchanged, recorded minified 37,060 -> 37,087 B (+27 B), measured in CI at 13,217 B brotli. `isSame` is the one guarded comparator call (a NotReady read is a change, any other throw goes to the node's boundary, A3), with `isEqual` on a fast path so the propagation benches keep next's speed. Size-Exception accepted by the maintainer (2026-10-19). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 13.24 -> 13.28 KB, recorded minified 37,087 -> 37,147 B, measured locally (linux) at 13,264 B brotli / 37,147 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): cap 13.28 KB unchanged, recorded minified 37,147 -> 37,113 B (-34 B), measured in CI at 13,249 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 13.28 -> 13.30 KB, recorded minified 37,113 -> 37,134 B, measured locally (linux) at 13,290 B brotli / 37,134 B minified (+0 B minified over next): no code of this change reaches this scenario; the dist's mangled property names shift with it and brotli lands +45 B, while next already sat 21 B past the recorded minified (1 B past the allowance). Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 13.28 -> 13.30 KB, recorded minified 37,113 -> 37,134 B, measured locally (linux) at 13,290 B brotli / 37,134 B minified (+0 B minified over next): no code of this change reaches this scenario; the dist's mangled property names shift with it and brotli lands +45 B, while next already sat 21 B past the recorded minified (1 B past the allowance). Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: "13.30 KB", capMinified: 37134, alias @@ -3979,7 +3979,7 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 26.10 KB (unchanged), recorded minified 81,112 -> 81,172 B, measured locally (linux) at 26,087 B brotli / 81,172 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 26.10 KB -> 26.11 KB, recorded minified 81,172 -> 81,138 B (-34 B), measured in CI at 26,092 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Re-recorded on next @ 0222d684 (2026-10-21, accepted by the maintainer): 26.11 KB -> 26.14 KB, recorded minified 81,138 -> 81,159 B (+21 B), measured in CI at 26,123 B brotli. #4188 (reaction arms captured before tracking reads), #4207 (#4203, unread pending memo under a verdict mount) and #4176 (deferred releases restored after a throwing cleanup) each passed alone and together put next 1 B past the 20 B minified allowance. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, pending the maintainer's acceptance): 26.14 -> 26.18 KB, recorded minified 81,159 -> 81,203 B, measured locally (linux) at 26,169 B brotli / 81,203 B minified (+44 B minified over next): the app's store pays the birth check under a folded guess (see `signals: + createStore`); brotli also moves with the dist's mangled names, which this change reorders. Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 26.14 -> 26.18 KB, recorded minified 81,159 -> 81,203 B, measured locally (linux) at 26,169 B brotli / 81,203 B minified (+44 B minified over next): the app's store pays the birth check under a folded guess (see `signals: + createStore`); brotli also moves with the dist's mangled names, which this change reorders. Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. limit: "26.18 KB", capMinified: 81203, alias From 102f72324cba1f87dca1a57f8178e99f9506bd01 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 00:54:31 +0000 Subject: [PATCH 6/8] chore(size): re-measure the keyed-fold entries over next @ db8241ce Raises the live server components frozen floor (Size-Exception in the PR). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015SnZadCVdDSeDzxBUhcMU9 --- scripts/size/floor-caps.json | 4 ++-- scripts/size/scenarios.js | 38 ++++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index 4d4107a88..384f6f597 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -16,8 +16,8 @@ "minified": 101526 }, "page: live server components (base + live/GET + action + isPending/latest)": { - "cap": "37.53 KB", - "minified": 115011 + "cap": "37.64 KB", + "minified": 115136 }, "server: floor (getRequestEvent + isServer)": { "cap": "1.34 KB", diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 3f8d4515f..6c51e250f 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1095,9 +1095,9 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 14.94 -> 14.95 KB, recorded minified 45,115 -> 45,175 B, measured locally (linux) at 14,932 B brotli / 45,175 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): cap 14.95 KB unchanged, recorded minified 45,175 -> 45,145 B (-30 B), measured in CI at 14,914 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 14.95 KB -> 15.01 KB, recorded minified 45,145 -> 45,286 B (+141 B), measured in CI at 14,997 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 14.95 -> 14.98 KB, recorded minified 45,145 -> 45,210 B, measured locally (linux) at 14,967 B brotli / 45,210 B minified (+44 B minified over next): a key born under a container guess that a correction folded under is held in that correction's hold. The fold itself lives in store/optimistic.ts behind the lane's `_laneRebase` hook, so a plain store carries only the birth check. Golfed at the maintainer's request (was +35 B with a hook here, +142 B core). Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "15.01 KB", - capMinified: 45286, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): 15.01 -> 15.04 KB, recorded minified 45,286 -> 45,330 B (+44 B), measured locally (linux) at 15,030 B brotli: a key born under a container guess that a correction folded under is held in that correction's hold; the fold itself lives in store/optimistic.ts behind the lane's `_laneRebase` hook. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "15.04 KB", + capMinified: 45330, alias }, { @@ -1406,9 +1406,9 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 10.14 -> 10.16 KB, recorded minified 28,383 -> 28,443 B, measured locally (linux) at 10,144 B brotli / 28,443 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 10.16 KB -> 10.19 KB, recorded minified 28,443 -> 28,463 B (+20 B), measured in CI at 10,173 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 10.19 KB -> 10.25 KB, recorded minified 28,463 -> 28,625 B (+162 B), measured in CI at 10,236 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 10.19 -> 10.25 KB, recorded minified 28,463 -> 28,612 B, measured locally (linux) at 10,234 B brotli / 28,612 B minified (+110 B minified over next, golfed from +142 B at the maintainer's request): `laneWrite` hands a current truth on a guess to the store's `_laneRebase` hook (one hook for the re-base and the fold), and a folded lane carries the store's reader of it (`Transaction.folded`), which `laneRead`, `latest()` (`verdictValue`) and a folded lane's list pass in `read()` use. The floors carry none of it (`read()`'s check is gated on the lane engine). Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "10.25 KB", - capMinified: 28625, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): 10.25 -> 10.30 KB, recorded minified 28,625 -> 28,735 B (+110 B, golfed from +142 B at the maintainer's request), measured locally (linux) at 10,281 B brotli: `laneWrite` hands a current truth on a guess to the store's `_laneRebase` hook (one hook for the re-base and the fold), and a folded lane carries the store's reader of it (`Transaction.folded`), which `laneRead`, `latest()` (`verdictValue`) and a folded lane's list pass in `read()` use. The floors carry none of it. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "10.30 KB", + capMinified: 28735, alias }, { @@ -2644,9 +2644,9 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 30.30 -> 30.32 KB, recorded minified 95,025 -> 95,085 B, measured locally (linux) at 30,308 B brotli / 95,085 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 30.32 KB -> 30.41 KB, recorded minified 95,085 -> 95,109 B (+24 B), measured in CI at 30,394 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 30.41 KB -> 30.42 KB, recorded minified 95,109 -> 95,268 B (+159 B), measured in CI at 30,403 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 30.41 -> 30.62 KB, recorded minified 95,109 -> 95,775 B, measured locally (linux) at 30,605 B brotli / 95,775 B minified (+627 B minified over next, golfed from +694 B at the maintainer's request): this scenario carries createOptimisticStore, so it pays for the fold itself (store/optimistic.ts `fold`, `foldedRead`, core `joinPassAt`) beside the lane and store changes above. Correctness: two overlapping writes on one keyed store no longer tear the store or remount the confirmed row. CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "30.42 KB", - capMinified: 95268, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): 30.42 -> 30.68 KB, recorded minified 95,268 -> 95,896 B (+628 B, golfed from +694 B at the maintainer's request), measured locally (linux) at 30,661 B brotli: this scenario carries createOptimisticStore, so it pays for the fold itself (store/optimistic.ts `fold`, `foldedRead`, core `joinPassAt`). Correctness: two overlapping writes on one keyed store no longer tear the store or remount the confirmed row. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "30.68 KB", + capMinified: 95896, alias }, { @@ -2936,7 +2936,7 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 13.24 -> 13.28 KB, recorded minified 37,087 -> 37,147 B, measured locally (linux) at 13,264 B brotli / 37,147 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): cap 13.28 KB unchanged, recorded minified 37,147 -> 37,113 B (-34 B), measured in CI at 13,249 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 13.28 KB -> 13.33 KB, recorded minified 37,113 -> 37,252 B (+139 B), measured in CI at 13,312 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 13.28 -> 13.30 KB, recorded minified 37,113 -> 37,134 B, measured locally (linux) at 13,290 B brotli / 37,134 B minified (+0 B minified over next): no code of this change reaches this scenario; the dist's mangled property names shift with it and brotli lands +45 B, while next already sat 21 B past the recorded minified (1 B past the allowance). Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): cap 13.33 KB and recorded minified 37,252 B unchanged: +0 B minified here, measured locally (linux) at 13,316 B brotli. limit: "13.33 KB", capMinified: 37252, alias @@ -3991,7 +3991,7 @@ module.exports = [ // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 26.10 KB -> 26.11 KB, recorded minified 81,172 -> 81,138 B (-34 B), measured in CI at 26,092 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Re-recorded on next @ 0222d684 (2026-10-21, accepted by the maintainer): 26.11 KB -> 26.14 KB, recorded minified 81,138 -> 81,159 B (+21 B), measured in CI at 26,123 B brotli. #4188 (reaction arms captured before tracking reads), #4207 (#4203, unread pending memo under a verdict mount) and #4176 (deferred releases restored after a throwing cleanup) each passed alone and together put next 1 B past the 20 B minified allowance. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 26.14 KB -> 26.23 KB, recorded minified 81,159 -> 81,283 B (+124 B), measured in CI at 26,214 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, over next @ a2a5b6f8, accepted by the maintainer 2026-10-22): 26.14 -> 26.18 KB, recorded minified 81,159 -> 81,203 B, measured locally (linux) at 26,169 B brotli / 81,203 B minified (+44 B minified over next): the app's store pays the birth check under a folded guess (see `signals: + createStore`); brotli also moves with the dist's mangled names, which this change reorders. Correctness; CI re-bases. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): cap 26.23 KB unchanged, recorded minified 81,283 B (+44 B: the app's store pays the birth check under a folded guess), measured locally (linux) at 26,216 B brotli, under the cap. limit: "26.23 KB", capMinified: 81283, alias @@ -4149,8 +4149,9 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 32.13 KB (unchanged), recorded minified 101,677 -> 101,737 B, measured locally (linux) at 32,091 B brotli / 101,737 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 32.13 KB -> 32.20 KB, recorded minified 101,737 -> 101,703 B (-34 B), measured in CI at 32,182 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 32.20 KB (unchanged), recorded minified 101,703 -> 101,848 B (+145 B), measured in CI at 32,174 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - limit: "32.20 KB", - capMinified: 101848, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): 32.20 -> 32.23 KB, recorded minified 101,848 -> 101,892 B (+44 B: the app's store pays the birth check under a folded guess), measured locally (linux) at 32,215 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "32.23 KB", + capMinified: 101892, alias }, { @@ -5413,6 +5414,7 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 37.46 -> 37.48 KB, recorded minified 114,922 -> 114,982 B, measured locally (linux) at 37,467 B brotli / 114,982 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 37.48 KB -> 37.53 KB, recorded minified 114,982 -> 115,031 B (+49 B), measured in CI at 37,516 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 37.53 KB (unchanged), recorded minified 115,031 -> 115,011 B (-20 B), measured in CI at 37,512 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, re-measured over next @ db8241ce): frozen floor 37.53 -> 37.64 KB, recorded minified 115,011 -> 115,136 B (+125 B: +110 B the lane engine's fold hooks, as `signals: + isPending/latest`, the rest brotli/mangle layout), measured locally (linux) at 37,627 B brotli. Size-Exception: the maintainer accepted the keyed-fold size cost (2026-10-22). 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)"], @@ -5702,8 +5704,9 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 41.31 KB (unchanged), recorded minified 122,907 -> 122,967 B, measured locally (linux) at 41,300 B brotli / 122,967 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 41.31 KB -> 41.42 KB, recorded minified 122,967 -> 123,015 B (+48 B), measured in CI at 41,410 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 41.42 KB (unchanged), recorded minified 123,015 -> 123,010 B (-5 B), measured in CI at 41,403 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - limit: "41.42 KB", - capMinified: 123010, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): 41.42 -> 41.47 KB, recorded minified 123,010 -> 123,120 B (+110 B: the page carries the lane engine's fold hooks, as `signals: + isPending/latest`), measured locally (linux) at 41,452 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "41.47 KB", + capMinified: 123120, alias: pageAlias }, { @@ -6078,8 +6081,9 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 46.90 -> 46.91 KB, recorded minified 140,198 -> 140,258 B, measured locally (linux) at 46,896 B brotli / 140,258 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): cap 46.91 KB unchanged, recorded minified 140,258 -> 140,296 B (+38 B), measured in CI at 46,884 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 46.91 KB -> 46.96 KB, recorded minified 140,296 -> 140,292 B (-4 B), measured in CI at 46,942 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - limit: "46.96 KB", - capMinified: 140292, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): 46.96 -> 47.01 KB, recorded minified 140,292 -> 140,402 B (+110 B: the page carries the lane engine's fold hooks, as `signals: + isPending/latest`), measured locally (linux) at 46,993 B brotli. Correctness. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "47.01 KB", + capMinified: 140402, alias: pageAlias, conditions: solidConditions, compile: { hydratable: true } From 285189f2d46b644f2611ecff08b2a6ebb89ad796 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 00:58:35 +0000 Subject: [PATCH 7/8] chore(size): record compiled CSR's minified at CI's 81,327 B The keyed fold's +44 B was left above the recorded minified, which leaves next 44 B past it and only 20 B of allowance for later PRs. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 --- scripts/size/scenarios.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 6c51e250f..a877c36aa 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -3991,9 +3991,9 @@ module.exports = [ // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 26.10 KB -> 26.11 KB, recorded minified 81,172 -> 81,138 B (-34 B), measured in CI at 26,092 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Re-recorded on next @ 0222d684 (2026-10-21, accepted by the maintainer): 26.11 KB -> 26.14 KB, recorded minified 81,138 -> 81,159 B (+21 B), measured in CI at 26,123 B brotli. #4188 (reaction arms captured before tracking reads), #4207 (#4203, unread pending memo under a verdict mount) and #4176 (deferred releases restored after a throwing cleanup) each passed alone and together put next 1 B past the 20 B minified allowance. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 26.14 KB -> 26.23 KB, recorded minified 81,159 -> 81,283 B (+124 B), measured in CI at 26,214 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): cap 26.23 KB unchanged, recorded minified 81,283 B (+44 B: the app's store pays the birth check under a folded guess), measured locally (linux) at 26,216 B brotli, under the cap. + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): cap 26.23 KB unchanged, recorded minified 81,283 -> 81,327 B (+44 B: the app's store pays the birth check under a folded guess), measured locally (linux) at 26,216 B brotli, under the cap. limit: "26.23 KB", - capMinified: 81283, + capMinified: 81327, alias }, { From 3cd7d4ea9f5f4070555bff452d94b7504078c958 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 01:18:38 +0000 Subject: [PATCH 8/8] perf(signals): recognize a keyed arrangement guess by its view's removed-keys set fold's guard was re-deriving "keyed container with a LaneView of rows" on every landing; the setter already knows, and now always gives a keyed array's view a removed-keys set (empty or not). -59 B minified where createOptimisticStore is used. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015SnZadCVdDSeDzxBUhcMU9 --- packages/signals/src/store/optimistic.ts | 20 ++++++++------------ scripts/size/scenarios.js | 6 +++--- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/signals/src/store/optimistic.ts b/packages/signals/src/store/optimistic.ts index 5522b5ecf..04acf393e 100644 --- a/packages/signals/src/store/optimistic.ts +++ b/packages/signals/src/store/optimistic.ts @@ -166,10 +166,12 @@ export function notifyOptimisticWrites( if (structural) { const rows = isArr ? (pb as any[]).map(unwrapValue) : shallowKeys(pb); // The rows the setter removed from the view it saw, by key: a later - // re-base over another action's truth must not bring them back. + // re-base over another action's truth must not bring them back. A set + // (empty or not) marks a keyed arrangement guess (`fold`). let removed: Set | null = null; const keyFn = t.fam!.key; if (isArr && keyFn !== null && keyFn !== undefined) { + removed = new Set(); const kept = new Set(); for (let i = 0; i < rows.length; i++) { const k = isWrappable(rows[i]) ? keyFn(rows[i]) : undefined; @@ -178,7 +180,7 @@ export function notifyOptimisticWrites( for (let i = 0; i < (old as any[]).length; i++) { const r = unwrapValue((old as any[])[i]); const k = isWrappable(r) ? keyFn(r) : undefined; - if (k !== undefined && !kept.has(k)) (removed ??= new Set()).add(k); + if (k !== undefined && !kept.has(k)) removed.add(k); } } optimisticWrite(getContainerNode(t), new LaneView(rows, base, removed) as any); @@ -397,17 +399,11 @@ GlobalQueue._laneRebase = (el: Signal, truth: unknown, older: boolean) => { * (A17): only truth readers are told. True: the lane stages the truth * beneath (`laneWrite`). */ function fold(el: Signal, truth: unknown): boolean { - const t = (el as any)._host as StoreTarget | undefined; - const k = t?.k; + const k = ((el as any)._host as StoreTarget | undefined)?.k; const l = txOf(el); - // A guess the screen never showed is void at the correction (D-34). - if ( - !l._shown || - !(k?._config! & CONFIG_OVERRIDE) || - t!.fam?.key == null || - !(k!._x!._lane instanceof LaneView) || - !Array.isArray(k!._x!._lane.rows) - ) + // A guess the screen never showed is void at the correction (D-34); a + // keyed arrangement guess's view carries its removed-keys set. + if (!l._shown || !(k?._config! & CONFIG_OVERRIDE) || !(k!._x!._lane as LaneView)?.removed) return false; const guess = el._x!._lane; if ((!el._equals || !el._equals(guess, truth)) && !GlobalQueue._echoes!(guess, truth)) { diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index a877c36aa..66156628f 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -2644,9 +2644,9 @@ module.exports = [ // Writable memo write clears its error without a torn read (#4110, 2026-10-20, over next @ 7014aca4, accepted by the maintainer): 30.30 -> 30.32 KB, recorded minified 95,025 -> 95,085 B, measured locally (linux) at 30,308 B brotli / 95,085 B minified (+60 B minified over the recorded size), which includes #4163's +9 B merged without re-recording: a write to an errored writable memo recovers it at commit and keeps the error shown until then, and clears an error a pass staged under the same hold. Correctness; matches CI's numbers. // Verdict-mount lane provenance for nested first passes (#4166, 2026-10-20, over next @ 5a4b2e85): 30.32 KB -> 30.41 KB, recorded minified 95,085 -> 95,109 B (+24 B), measured in CI at 30,394 B brotli. A module-local laneRoot carries the mount's lane through nested first passes, gated on the lane engine, so bundles without it shrink. Size-Exception accepted by the maintainer (2026-10-20). Cap at measured + 10 B rounded up to 0.01 KB, never lowered here; recorded minified = measured. // Throwing cleanups no longer abort teardown (#4213, 2026-10-21, over next @ a2a5b6f8): 30.41 KB -> 30.42 KB, recorded minified 95,109 -> 95,268 B (+159 B), measured in CI at 30,403 B brotli. Every onCleanup and effect cleanup runs when one throws; the first error is rethrown by a root's dispose() or at the end of the flush. Size-Exception accepted by the maintainer (2026-10-21). Cap at measured + 10 B rounded up to 0.01 KB, never lowered; recorded minified = measured. - // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): 30.42 -> 30.68 KB, recorded minified 95,268 -> 95,896 B (+628 B, golfed from +694 B at the maintainer's request), measured locally (linux) at 30,661 B brotli: this scenario carries createOptimisticStore, so it pays for the fold itself (store/optimistic.ts `fold`, `foldedRead`, core `joinPassAt`). Correctness: two overlapping writes on one keyed store no longer tear the store or remount the confirmed row. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. - limit: "30.68 KB", - capMinified: 95896, + // Keyed optimistic guess folds a correction (D-34, #4210, 2026-10-21, accepted by the maintainer 2026-10-22, re-measured over next @ db8241ce): 30.42 -> 30.66 KB, recorded minified 95,268 -> 95,837 B (+569 B, golfed from +694 B at the maintainer's request), measured locally (linux) at 30,642 B brotli: this scenario carries createOptimisticStore, so it pays for the fold itself (store/optimistic.ts `fold`, `foldedRead`, core `joinPassAt`); a keyed arrangement guess is recognized by its view's removed-keys set. Correctness: two overlapping writes on one keyed store no longer tear the store or remount the confirmed row. Cap at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. + limit: "30.66 KB", + capMinified: 95837, alias }, {