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/.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/signals/docs/RULES-DERIVED.md b/packages/signals/docs/RULES-DERIVED.md index 3ad13ab09..fe2c79dfe 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) 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. ### 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..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×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×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 e5c43f972..7feb74518 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -1098,6 +1098,13 @@ export function joinPass(c: Computed, el: Signal | Computed): voi ); } +/** `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 * reading a node held by a transaction that is not the flush's. It is served * the committed value — a pending one's too — publishes mainline, and is @@ -1810,7 +1817,12 @@ 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 over a folded lane (D-34), once the frame has a + // transaction, reads the screen too. + if ( + ((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane?.folded)) && + stagedScreen(c as Computed) + ) return el._value as T; stagedRead(c as Computed); return el._pendingValue as T; @@ -2028,7 +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): 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 39072babd..6c1ab8590 100644 --- a/packages/signals/src/core/lanes.ts +++ b/packages/signals/src/core/lanes.ts @@ -553,12 +553,21 @@ 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. + // 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); const status = (el as Computed)._statusFlags; // An errored member's lane value predates its error; the error is the answer (#4106). if (!guess && status & STATUS_ERROR) return NOT_PENDING; if (c === null) return laneValueOf(el); - const l = txOf(el); if (!tracking || c._config & CONFIG_CHILDREN_FORBIDDEN) { // An untracked read inside lane work derives from the lane like a // tracked one, minus the subscription — `untrack` is about @@ -733,10 +742,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. - if (stale(el, question)) { + // 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); + if (GlobalQueue._laneRebase?.(el, v, older) || older) { el._pendingValue = v; el._config |= CONFIG_HELD; - GlobalQueue._laneRebase?.(el, v); schedule(); return v; } diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts index 4b913da2f..7baaffa58 100644 --- a/packages/signals/src/core/scheduler.ts +++ b/packages/signals/src/core/scheduler.ts @@ -246,6 +246,15 @@ 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) — 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; @@ -807,10 +816,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; + /** 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; // 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..8da924be1 100644 --- a/packages/signals/src/core/verdict.ts +++ b/packages/signals/src/core/verdict.ts @@ -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 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 fcadbe4ca..04acf393e 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"; @@ -159,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; @@ -171,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); @@ -319,7 +328,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; @@ -381,6 +391,54 @@ GlobalQueue._laneRebase = (el: Signal, truth: unknown): void => { } }; +/** 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`). */ +function fold(el: Signal, truth: unknown): boolean { + 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); 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)) { + 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 + * 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. */ +function foldedRead(c: Computed | null, el: Signal | Computed): unknown { + 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; +} + 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..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 @@ -370,9 +371,13 @@ 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)); + // 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 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. 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..d12cd0d23 --- /dev/null +++ b/packages/signals/tests/optimistic-keyed-fold.test.ts @@ -0,0 +1,300 @@ +/** + * 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, + createMemo, + createSignal, + flush, + mapArray, + onCleanup, + resolve, + TimeoutError, + untrack, + 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 }); + }); + + 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")]); + 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 }); + }); + } +}); 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); }); 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 b1e189c94..66156628f 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1095,8 +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. - 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 }, { @@ -1405,8 +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. - 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 }, { @@ -2642,8 +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. - 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.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 }, { @@ -2933,6 +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, 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 @@ -3987,8 +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 -> 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 }, { @@ -4144,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 }, { @@ -5408,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)"], @@ -5697,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 }, { @@ -6073,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 }