diff --git a/.changeset/rearm-boundary-scope-u9.md b/.changeset/rearm-boundary-scope-u9.md new file mode 100644 index 000000000..59b0f810f --- /dev/null +++ b/.changeset/rearm-boundary-scope-u9.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +A Loading boundary re-armed by `on` owns the content under it that reads a hold another change made (D-14, D-16, U-9). Before, that content joined the hold, so no fallback showed and the boundary reported pending, and a held tree under a re-armed boundary could redraw forever. Now the fallback shows with the re-arming change and the content reveals at the hold's commit, which no longer waits for it. A re-arm whose own change joins that hold still shows no fallback (the frame waits on the same data), and `LOADING_ON_OUTSIDE_HOLD` is reported only for a flight the change itself started. diff --git a/packages/signals/docs/OPEN-QUESTIONS.md b/packages/signals/docs/OPEN-QUESTIONS.md index a30c29dc0..2577d845c 100644 --- a/packages/signals/docs/OPEN-QUESTIONS.md +++ b/packages/signals/docs/OPEN-QUESTIONS.md @@ -10,16 +10,16 @@ Three sections were drafted. **Q** stays while a ruled screen is still written o Each is pinned `it.fails`; the ruled screen is in `SCENARIOS.md`. Confirming here fixes the derived rule; the build order is yours. -| | ruling | pin | the deviation today | size note | -| ------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **U-1** | D-2 — a new async memo's answer does not show beside the old value (#3800) | `issue-3800-repro.test.ts` | built (#3977); the last shape, a memo created inside a fresh boundary, built 2026-10-14: caught and staged with the hold, which does not wait for it (`issue-3800-repro.test.ts:195`) | −22 B minified with `_bornIn` dropped (column 2); the last shape −4 B minified on the floors, −50 B on the boundary apps (boundary walks test truthiness) | -| **U-2** | withdrawn with the direction rule (F-2). The hold waits for a mount's first load a render effect consumes outside a boundary | `direction-rule-probe.test.ts` ×3 | flipped to wait (#3977) | nothing to build. The +270 B mechanism would implement the rejected screen | -| **U-3** | D-13 — a boundary mounted by a pass that joined the hold appears at the hold's commit | `loading-fallback-in-flush-3540.test.ts:299`, `:910` (flight, flat and nested); outside-reader form in `fresh-boundary-born-held-reader.test.ts` | built (2026-10-14): a born-held first pass is not caught when the flush has joined the hold and the pass is its own, not a verdict lane's mount (`_fresh`, boundaries.ts); the pins are plain `it` | −4 B minified on the floors, −33 B minified on the boundary apps (`hiddenBy` installed as `_hidden` directly) | -| **U-4** | D-14 — an outer `on`-reset boundary does not wait for a flight its nested `on`-reset boundary catches (A33, B5) | `fuzz-findings-l2.test.ts`, fuzzer finding F1 | built (2026-10-13): what an `on` re-arm collects is that boundary's alone (`arm`, boundaries.ts) | +17 B brotli / +61 B minified on the CSR scenario, within its cap (a `record` helper paid for the sweep) | -| **U-5** | D-9 — a landing does not reveal a gate its stale reader re-derives onto a new flight (A15 reveal corollary) | `fuzz-findings-l2.test.ts`, fuzzer finding F2 | it reveals | not measured | -| **U-6** | D-26 — a stale reader of a held re-guess shows the revealed guess, not the unrevealed one (A17, #3460) | `fuzz-findings-l2.test.ts`, fuzzer finding F5b | shows `1,2` where `1,1` is ruled | needs a per-node slot for the revealed lane value | -| **U-7** | D-30 — a store row first read after an adoption derives nothing from the hold (#3706) | `adoption-unchanged-key-read-3706.test.ts:278` | built (#3986): the row is paired at that read by the family's key, as the adoption would have paired it; the pin is a plain `it` | not measured | -| **U-8** | D-8 — a later mainline setter that does not write the held key publishes on the mainline tick (#3743) | `store/unchanged-presence-no-hold-3743.test.ts:491` | built (2026-10-13): a setter's written keys are its own set, the batch's earlier chained behind (`touch`, store.ts); the notify visits this setter's keys, the fold keeps the batch's union | −41 B brotli / −14 B minified on `+ createStore` | -| **U-9** | D-14 — a held tree under a re-armed boundary re-derives without a redraw loop | `loading-fallback-in-flush-3540.test.ts:1020` | a redraw loop | Q-2 ruled 2026-10-09 (D-16); the loop is still the deviation | +| | ruling | pin | the deviation today | size note | +| ------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **U-1** | D-2 — a new async memo's answer does not show beside the old value (#3800) | `issue-3800-repro.test.ts` | built (#3977); the last shape, a memo created inside a fresh boundary, built 2026-10-14: caught and staged with the hold, which does not wait for it (`issue-3800-repro.test.ts:195`) | −22 B minified with `_bornIn` dropped (column 2); the last shape −4 B minified on the floors, −50 B on the boundary apps (boundary walks test truthiness) | +| **U-2** | withdrawn with the direction rule (F-2). The hold waits for a mount's first load a render effect consumes outside a boundary | `direction-rule-probe.test.ts` ×3 | flipped to wait (#3977) | nothing to build. The +270 B mechanism would implement the rejected screen | +| **U-3** | D-13 — a boundary mounted by a pass that joined the hold appears at the hold's commit | `loading-fallback-in-flush-3540.test.ts:299`, `:910` (flight, flat and nested); outside-reader form in `fresh-boundary-born-held-reader.test.ts` | built (2026-10-14): a born-held first pass is not caught when the flush has joined the hold and the pass is its own, not a verdict lane's mount (`_fresh`, boundaries.ts); the pins are plain `it` | −4 B minified on the floors, −33 B minified on the boundary apps (`hiddenBy` installed as `_hidden` directly) | +| **U-4** | D-14 — an outer `on`-reset boundary does not wait for a flight its nested `on`-reset boundary catches (A33, B5) | `fuzz-findings-l2.test.ts`, fuzzer finding F1 | built (2026-10-13): what an `on` re-arm collects is that boundary's alone (`arm`, boundaries.ts) | +17 B brotli / +61 B minified on the CSR scenario, within its cap (a `record` helper paid for the sweep) | +| **U-5** | D-9 — a landing does not reveal a gate its stale reader re-derives onto a new flight (A15 reveal corollary) | `fuzz-findings-l2.test.ts`, fuzzer finding F2 | it reveals | not measured | +| **U-6** | D-26 — a stale reader of a held re-guess shows the revealed guess, not the unrevealed one (A17, #3460) | `fuzz-findings-l2.test.ts`, fuzzer finding F5b | shows `1,2` where `1,1` is ruled | needs a per-node slot for the revealed lane value | +| **U-7** | D-30 — a store row first read after an adoption derives nothing from the hold (#3706) | `adoption-unchanged-key-read-3706.test.ts:278` | built (#3986): the row is paired at that read by the family's key, as the adoption would have paired it; the pin is a plain `it` | not measured | +| **U-8** | D-8 — a later mainline setter that does not write the held key publishes on the mainline tick (#3743) | `store/unchanged-presence-no-hold-3743.test.ts:491` | built (2026-10-13): a setter's written keys are its own set, the batch's earlier chained behind (`touch`, store.ts); the notify visits this setter's keys, the fold keeps the batch's union | −41 B brotli / −14 B minified on `+ createStore` | +| **U-9** | D-14 — a held tree under a re-armed boundary re-derives without a redraw loop | `loading-fallback-in-flush-3540.test.ts:1020` (the loop), `:520` (re-armed held mounts), `:859` (committed content under a re-armed boundary); `loading-on-frame-following-3540.test.ts` §3b, §3c | built (2026-10-14): a re-arm owns what an earlier hold staged under it (`arm` records it, `heldUnder`), and a pass under it that reads a hold is the boundary's, not the tick's (`_fresh`): the fallback shows now, the content at that hold's commit, and the hold does not wait for it. A re-arm whose own flush joins that hold folds back into it (D-16): the seam drops a fallback staged into the very hold its readers wait on. The pins are plain `it` | 0 B minified on the floors (optional calls golfed in the core); +417 B minified on the boundary apps, caps raised (accepted 2026-10-14) | **Answer format that is enough:** `U-1, U-3–U-9 stand, U-6 after 2.0` — or the line you prefer. U-2 is withdrawn with the direction rule's time half (once drafted as F-8; F-2). Q-1 through Q-5 are ruled and carried by the rules. C-1 is D-12. C-2 is D-7. C-3 is D-32. diff --git a/packages/signals/docs/RULES-DERIVED.md b/packages/signals/docs/RULES-DERIVED.md index 978bd9b45..7a6b09ec7 100644 --- a/packages/signals/docs/RULES-DERIVED.md +++ b/packages/signals/docs/RULES-DERIVED.md @@ -120,7 +120,7 @@ Whether a fallback is seen depends on whether the boundary is on screen. A fresh A flight whose only observers are render effects behind a fallback holds no change — a memo under the boundary that no render effect reads was never an observer at all, and one rendered by an effect outside the boundary is that effect's read (F-3). In both orders: a render effect created under a fallback never counts, and a reader that counted while the boundary showed content stops counting the moment `on` sends it back to the fallback. Writes the hold kept for those readers alone commit at once (`Boundary: Loading... | Sum: 2` publishes as one frame); a reader outside the boundary still holds (`Sum` waits until `details` lands). The wait does not vanish; it moves onto the boundary: after the reset the fallback stays until every reader under it has settled — including readers whose flights started before the reset — then one coherent reveal. -**Collapses:** [A33](./SPEC-ASYNC-SEMANTICS.md#a33-a-fallback-caught-flight-holds-no-transaction-a-loading-reset-moves-the-hold-onto-the-boundary) (#3375, #3459), §23.1 item 1, recovery 2.2. **Status:** standing. The nested form — an outer `on`-reset boundary not waiting for a flight its nested `on`-reset boundary catches — is built (U-4, 2026-10-13: a reader a nested collecting boundary holds is that boundary's alone; `tests/fuzz-findings-l2.test.ts`, fuzzer finding F1). +**Collapses:** [A33](./SPEC-ASYNC-SEMANTICS.md#a33-a-fallback-caught-flight-holds-no-transaction-a-loading-reset-moves-the-hold-onto-the-boundary) (#3375, #3459), §23.1 item 1, recovery 2.2. **Status:** standing. The nested form — an outer `on`-reset boundary not waiting for a flight its nested `on`-reset boundary catches — is built (U-4, 2026-10-13: a reader a nested collecting boundary holds is that boundary's alone; `tests/fuzz-findings-l2.test.ts`, fuzzer finding F1). The re-arm over a hold is built (U-9, 2026-10-14): content an earlier hold staged under the boundary, or that reads a hold after the re-arm, is the boundary's — the fallback shows now and the content reveals at that hold's commit, which does not wait for it; no redraw loop (`tests/loading-fallback-in-flush-3540.test.ts`, the re-armed shapes; `tests/loading-on-frame-following-3540.test.ts` §3b, §3c). ### D-15. `on` is a dependency list; the swap lands with the frame of the change that caused it diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 397b31e68..f4c6f8e7c 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -55,7 +55,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:173` | — | createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… | | A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:181` | projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… | | A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:189` | core.ts×3 verdict.ts×2 | createMemo.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 verdict-contract.test.ts×2 | [ruled] `[isPending(x), x()]` is atomic within one scope — `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. | -| A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:103` | — | latest-isPending-consistency.test.ts×1 loading-fallback-in-flush-3540.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… | +| A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:103` | — | latest-isPending-consistency.test.ts×1 loading-fallback-in-flush-3540.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… | | A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:197` | — | createOptimistic.test.ts×2 spec-async-semantics.test.ts×1 | [ruled, amended in place] Resting optimistic nodes report pending like a plain memo — A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optim… | | A13 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:205` | — | spec-async-semantics.test.ts×7 | [ruled 2026-07-06 (promoted from B1)] Resting optimistic ≡ plain async memo at every checkpoint — (was B1) A resting optimistic node (no active override) is observationally identical to a plain async … | | 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… | diff --git a/packages/signals/src/boundaries.ts b/packages/signals/src/boundaries.ts index 9e97c0447..5c6dcf612 100644 --- a/packages/signals/src/boundaries.ts +++ b/packages/signals/src/boundaries.ts @@ -48,7 +48,6 @@ import { EFFECT_RENDER, EFFECT_TRACKED, EFFECT_USER, - NOT_PENDING, REACTIVE_DISPOSED, REACTIVE_LANE_READ, REACTIVE_ZOMBIE, @@ -64,10 +63,12 @@ import { link } from "./core/graph.js"; import { enqueueSub } from "./core/heap.js"; import { cleanup, createOwner, dispose, getOwner } from "./core/owner.js"; import { + clock, flushTransaction, GlobalQueue, globalQueue, haltReactivity, + heldUnder, joinFuture, passLane, passTx, @@ -75,6 +76,7 @@ import { schedule, setPassLane, txOf, + unhold, type Transaction } from "./core/scheduler.js"; import { attrHooks } from "./core/attribution-hooks.js"; @@ -199,6 +201,8 @@ function caught(b: Boundary, node: Computed, error: unknown): void { function flip(b: Boundary): void { b._initialized = false; b._lane = b._armLane; + const o = b._output!; + if (!b._lane && o._config & CONFIG_HELD && txOf(o) !== flushTransaction) unhold(o); if (b._lane !== null && b._output !== null) GlobalQueue._laneStage!(b._output, b._lane, true, false); } @@ -249,6 +253,13 @@ function catchStatus( return false; } +/** The nearest loading boundary that would catch `node` was re-armed this + * flush. */ +function armedOver(node: Computed): boolean | undefined { + for (let b: Boundary | null | undefined = boundaryOf(node); b; b = b._parent) + if (b._type & STATUS_PENDING && isCollecting(b)) return b._armed; +} + /** The nearest boundary on `r`'s chain showing its fallback, if any * (GlobalQueue._hidden: `r` is behind a fallback). */ function hiddenBy(r: Computed): Boundary | undefined { @@ -306,7 +317,10 @@ export function prune(b: Boundary, pass: boolean): number { for (const r of b._readers) { if (r._flags & REACTIVE_DISPOSED) b._readers.delete(r); else if (!(r._statusFlags & mask)) { - if (r !== b._tree && (r._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD) { + if ( + (r !== b._tree || b._armed) && + (r._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD + ) { // Landed, held: the content is that transaction's — the output's // pass enters it and the reveal lands with the reader's run. The // seam, past its park decision, waits for the commit instead. (A @@ -351,6 +365,18 @@ function boundarySeam(): void { // A parked frame's boundary (zombie) keeps its state for its revival. if (flags & REACTIVE_ZOMBIE) continue; prune(b, false); + const o = b._output!; + if ( + b._armed && + !b._initialized && + o._config & CONFIG_HELD && + [...b._readers].every( + r => !(r._statusFlags & STATUS_PENDING) && r._config & CONFIG_HELD && txOf(r) === txOf(o) + ) + ) { + b._readers.clear(); + b._initialized = true; + } if (__DEV__ && b._armed && !b._initialized && !b._ahead) reportOutsideHold(b); b._armed = false; if (b._readers.size === 0) { @@ -387,6 +413,9 @@ function reportOutsideHold(b: Boundary): void { const sources = r._x?._pendingSources; if (sources === undefined) continue; for (const source of sources) { + // Only the flight the change itself started (D-16): an outside reader + // of an earlier write's flight holds nothing the change waits on. + if (source._time !== clock) continue; for (let s = source._subs; s !== null; s = s._nextSub) { if (!outside(s._sub, true)) continue; const name = (source as any)._name as string | undefined; @@ -459,8 +488,13 @@ function arm(b: Boundary): void { b._armLane = lane; if (__DEV__) b._ahead = lane !== null; collecting.add(b); + const n = b._readers.size; + heldUnder(b._owner, flushTransaction, r => record(b, r)); for (const r of b._readers) { - if (r._statusFlags & STATUS_PENDING && !(r._flags & REACTIVE_DISPOSED)) { + if ( + b._readers.size !== n || + (r._statusFlags & STATUS_PENDING && !(r._flags & REACTIVE_DISPOSED)) + ) { // What it collects was forwarded through it to the boundaries above // too (`catchStatus` records a reader in every boundary on the way). // It is this boundary's alone now (B5, the nearest catcher): an outer @@ -821,7 +855,8 @@ export function createErrorBoundary( // boundary. An app without one pays the three null checks and nothing else. GlobalQueue._catch = catchStatus; GlobalQueue._fresh = (node, held) => - !held && node._statusFlags & STATUS_UNINITIALIZED && catchStatus(node, STATUS_PENDING); + !held && + (node._statusFlags & STATUS_UNINITIALIZED ? catchStatus(node, STATUS_PENDING) : armedOver(node)); GlobalQueue._hidden = hiddenBy; GlobalQueue._boundarySeam = boundarySeam; GlobalQueue._heldRun = heldRun; diff --git a/packages/signals/src/core/async.ts b/packages/signals/src/core/async.ts index 3ac53a1e9..9ba0eb187 100644 --- a/packages/signals/src/core/async.ts +++ b/packages/signals/src/core/async.ts @@ -907,7 +907,8 @@ export function propagateStatus( if ( passLane === null && (sub._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD && - globalQueue._running + globalQueue._running && + !GlobalQueue._fresh?.(sub, pendingSource._time === clock) ) joinFuture(txOf(sub)); return; diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index dc755f8e0..8bc1ccb82 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -325,7 +325,8 @@ export function recompute(el: Computed, create: boolean = false): void { // a sync write (#3460). The pass is the lane's if it reads the lane's // world (`read`), and has left it otherwise. if (tx._lane) { - } else if (isEffect !== EFFECT_RENDER && !(el._config & CONFIG_VERDICT)) joinPassTx(tx); + } else if (isEffect !== EFFECT_RENDER && !(el._config & CONFIG_VERDICT)) + joinPassTx(tx, GlobalQueue._fresh?.(el, el._statusFlags & STATUS_UNINITIALIZED)); else if ( tx !== flushTransaction && !(el._statusFlags & STATUS_UNINITIALIZED || el._config & CONFIG_HELD_FRAME) diff --git a/packages/signals/src/core/effect.ts b/packages/signals/src/core/effect.ts index ac534d855..e519d9ede 100644 --- a/packages/signals/src/core/effect.ts +++ b/packages/signals/src/core/effect.ts @@ -159,7 +159,7 @@ function runEffect(node: Effect, type: number): void { // hidden subtree. The synchronous first render on creation is not a queued // run and goes through. `_modified` stays set; the boundary re-queues the // run (`release`). - if (GlobalQueue._heldRun && GlobalQueue._heldRun(node)) return; + if (GlobalQueue._heldRun?.(node)) return; // Error arm (#2840), user effects only: a compute-phase error that is still // the node's settled state at effect time runs the bundle's error handler in // this same imperative, writable scope. Unwrap the StatusError used for @@ -262,7 +262,7 @@ export function trackedEffect(fn: () => void | (() => void), options?: NodeOptio // pass, but several passes in one flush each enqueue `_run` into the same // user queue, and this gate is what collapses them into one run. if (!node._modified || node._flags & REACTIVE_DISPOSED) return; - if (GlobalQueue._heldRun && GlobalQueue._heldRun(node)) return; + if (GlobalQueue._heldRun?.(node)) return; if (__DEV__) setTrackedQueueCallback(true); try { node._modified = false; diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts index af6d0907f..5357c5d3c 100644 --- a/packages/signals/src/core/scheduler.ts +++ b/packages/signals/src/core/scheduler.ts @@ -256,10 +256,7 @@ export function blocked(t: Transaction): boolean { judge ??= t; // An action still running in it holds it open (action.ts). Its lanes // (lanes.ts): a lane under `t` blocks it while blocked itself. - const r = - t._open !== 0 || - blockedBy(t._nodes, t) || - (!!GlobalQueue._lanesBlocked && GlobalQueue._lanesBlocked(t)); + const r = t._open !== 0 || blockedBy(t._nodes, t) || !!GlobalQueue._lanesBlocked?.(t); judge = prev; return r; } @@ -354,10 +351,7 @@ function blockedBy(nodes: Signal[], owner: Transaction, own = false): boole * everything under it, so a reader there is not on screen and holds * nothing — boundaries.ts). */ function onScreen(r: Computed, t: Transaction): boolean { - return ( - !(r._flags & REACTIVE_ZOMBIE && removalStagedBy(r, t)) && - !(GlobalQueue._hidden && GlobalQueue._hidden(r)) - ); + return !(r._flags & REACTIVE_ZOMBIE && removalStagedBy(r, t)) && !GlobalQueue._hidden?.(r); } /** A15 (#3463): a reader whose removal is staged — a zombie, parked by a * pass awaiting its commit — is still on screen and live for every hold, @@ -367,10 +361,9 @@ function onScreen(r: Computed, t: Transaction): boolean { * the nearest non-zombie ancestor. */ function removalStagedBy(r: Computed, t: Transaction): boolean { let o: Owner | null = r; - while (o !== null && (o as any)._flags & REACTIVE_ZOMBIE) o = o._parent; + while ((o as any)?._flags & REACTIVE_ZOMBIE) o = o!._parent; return ( - o !== null && - (o._config & CONFIG_HELD) !== 0 && + !!((o?._config as number) & CONFIG_HELD) && (o as any)._x?._transaction != null && txOf(o as any) === resolveTx(t) ); @@ -437,6 +430,26 @@ function holdFrame(owner: Owner, t: Transaction): void { } } +/** The nodes under `owner` a transaction other than `t` holds (boundaries.ts: + * what an earlier hold staged under a re-armed boundary). */ +export function heldUnder( + owner: Owner, + t: Transaction | null, + fn: (n: Computed) => void +): void { + for (let c = owner._firstChild; c !== null; c = c._nextSibling) { + if ((c._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD && txOf(c as any) !== t) + fn(c as Computed); + heldUnder(c, t, fn); + } +} + +/** `n` leaves the hold it was staged in: its next pass publishes mainline. */ +export function unhold(n: Signal): void { + n._x!._transaction = null; + n._config &= ~CONFIG_HELD; +} + export const dirtyQueue: Heap = { _heap: new Array(2000).fill(undefined), _marked: false, @@ -956,7 +969,7 @@ export class GlobalQueue implements IQueue { * first: a fallback catches what is not ready under it, and the root * never hears of it. */ notify(node: Computed, mask: number, flags: number, error?: any): boolean { - if (GlobalQueue._catch && GlobalQueue._catch(node, flags & mask, error)) return true; + if (GlobalQueue._catch?.(node, flags & mask, error)) return true; if (mask & STATUS_PENDING) { if (flags & STATUS_PENDING) { if (__DEV__ && _enforceLoadingBoundary) _hitUnhandledAsync = true; diff --git a/packages/signals/tests/loading-fallback-in-flush-3540.test.ts b/packages/signals/tests/loading-fallback-in-flush-3540.test.ts index bf96df22d..b1ee03a85 100644 --- a/packages/signals/tests/loading-fallback-in-flush-3540.test.ts +++ b/packages/signals/tests/loading-fallback-in-flush-3540.test.ts @@ -10,9 +10,10 @@ * publishes, and the content appears at the hold's commit. Outside such a * boundary, A15 and the direction rule are unchanged. * - * Re-arm (`on`) under the boundary scope is deferred to a separate change, - * pending its ruling: those shapes keep next's behavior and are pinned - * `it.fails` with the ruled display. + * Re-arm (`on`) under the boundary scope (U-9, D-14/D-16): a boundary + * re-armed by a change that does not join the hold owns its content — what + * reads the hold under it waits behind the fallback, and the hold does not + * wait for it. * * Each case is traced on the pre-L2 core (41fdf9696) too; the notes say * where it differs and the ruling that explains it. Sources: `held` — the @@ -438,10 +439,10 @@ describe("#3540: the catcher is whoever `catchStatus` would catch at — at any ], // A re-armed ancestor: the content is pending and its fallback shows // now — nothing else holds the re-arming frame (#3575). In flight, as - // pre-L2. Held (the ruled display, pinned `it.fails`: re-arm under the - // boundary scope is deferred): pre-L2 shows the committed value - // (`open fresh 0`, a tear under L2's A29); `next` holds the whole mount - // for the commit. + // pre-L2. Held (U-9): the mount's pass reads the hold as the boundary's, + // not the tick's, so the fallback shows now and the content at the + // commit; pre-L2 shows the committed value (`open fresh 0`, a tear under + // L2's A29). "rearmed outer held memo": [ "outer outer fallback | o1 i1", "open fresh 1 · outer outer · x 1 | o0 i0", @@ -514,91 +515,85 @@ describe("#3540: the catcher is whoever `catchStatus` would catch at — at any for (const shape of ["unrevealed", "rearmed outer", "revealed", "rearmed", "plain"] as Shape[]) for (const source of ["held", "flight"] as Source[]) for (const kind of ["memo", "bound"] as Content[]) - // Re-arm over a held write: content bound by a render effect is caught - // (a boundary that has not shown content keeps a held reader, #3540); - // content through a memo is still deferred to the re-arm change. - (source === "held" && shape.startsWith("rearmed") && kind === "memo" ? it.fails : it)( - `${shape}, ${source}, content ${kind}`, - async () => { - const log: string[] = []; - const [open, setOpen] = createSignal(false); - let resolveGate: (() => void) | undefined; - let w!: ReturnType; - let outerOwner: object | undefined; - let innerOwner!: object; - let outer: (() => unknown) | undefined; - let inner!: () => unknown; - const innerFn = () => { - innerOwner = getOwner()!; - createRenderEffect( - () => (open() ? content(kind, w.read, log, "fresh")() : false), - v => { - log.push(`open ${v}`); + it(`${shape}, ${source}, content ${kind}`, async () => { + const log: string[] = []; + const [open, setOpen] = createSignal(false); + let resolveGate: (() => void) | undefined; + let w!: ReturnType; + let outerOwner: object | undefined; + let innerOwner!: object; + let outer: (() => unknown) | undefined; + let inner!: () => unknown; + const innerFn = () => { + innerOwner = getOwner()!; + createRenderEffect( + () => (open() ? content(kind, w.read, log, "fresh")() : false), + v => { + log.push(`open ${v}`); + } + ); + return "inner"; + }; + const showInner = (on?: () => unknown) => { + inner = Loading(innerFn, "inner fallback", on); + createRenderEffect(inner, v => { + log.push(`inner ${v}`); + }); + }; + createRoot(() => { + w = world(source, log); + if (shape === "rearmed" || shape === "plain") + return showInner(shape === "rearmed" ? open : undefined); + outer = Loading( + () => { + outerOwner = getOwner()!; + showInner(); + if (shape === "unrevealed") { + const gate = createMemo( + () => new Promise(r => (resolveGate = () => r("gate"))) + ); + createRenderEffect(gate, v => { + log.push(`gate ${v}`); + }); } - ); - return "inner"; - }; - const showInner = (on?: () => unknown) => { - inner = Loading(innerFn, "inner fallback", on); - createRenderEffect(inner, v => { - log.push(`inner ${v}`); - }); - }; - createRoot(() => { - w = world(source, log); - if (shape === "rearmed" || shape === "plain") - return showInner(shape === "rearmed" ? open : undefined); - outer = Loading( - () => { - outerOwner = getOwner()!; - showInner(); - if (shape === "unrevealed") { - const gate = createMemo( - () => new Promise(r => (resolveGate = () => r("gate"))) - ); - createRenderEffect(gate, v => { - log.push(`gate ${v}`); - }); - } - return "outer"; - }, - "outer fallback", - shape === "rearmed outer" ? open : undefined - ); - createRenderEffect(outer, v => { - log.push(`outer ${v}`); - }); + return "outer"; + }, + "outer fallback", + shape === "rearmed outer" ? open : undefined + ); + createRenderEffect(outer, v => { + log.push(`outer ${v}`); }); - flush(); - await w.settle(); - const end = w.begin(); - log.length = 0; - const steps = [ - () => (setOpen(true), flush()), - end, - w.settle, - async () => { - resolveGate?.(); - await tick(); - } - ]; - const readers = (): [string, number | undefined][] => - outer - ? [ - ["o", readersOf(outerOwner)], - ["i", readersOf(innerOwner)] - ] - : [["i", readersOf(innerOwner)]]; - const pending = () => [ - ...(outer && isPending(() => outer!()) ? ["o"] : []), - ...(isPending(() => inner()) ? ["i"] : []) - ]; - const out = await trace(steps, log, readers, pending); - // The gate step reports the log only. - out[3] = out[3].split(" | ")[0]; - expect(out).toEqual(expected[`${shape} ${source} ${kind}`]); - } - ); + }); + flush(); + await w.settle(); + const end = w.begin(); + log.length = 0; + const steps = [ + () => (setOpen(true), flush()), + end, + w.settle, + async () => { + resolveGate?.(); + await tick(); + } + ]; + const readers = (): [string, number | undefined][] => + outer + ? [ + ["o", readersOf(outerOwner)], + ["i", readersOf(innerOwner)] + ] + : [["i", readersOf(innerOwner)]]; + const pending = () => [ + ...(outer && isPending(() => outer!()) ? ["o"] : []), + ...(isPending(() => inner()) ? ["i"] : []) + ]; + const out = await trace(steps, log, readers, pending); + // The gate step reports the log only. + out[3] = out[3].split(" | ")[0]; + expect(out).toEqual(expected[`${shape} ${source} ${kind}`]); + }); /** * A revealed outer Loading; a Show under it mounts, in the flush, a fresh @@ -800,22 +795,23 @@ describe("#3540: the boundary scope — committed content, held mounts, no catch // Committed content under a revealed boundary, re-run by a plain write // to `key` while the hold is up. With `on: key` the boundary is re-armed // and owns its subtree: the content that reads the hold waits behind the - // fallback, and the hold does not wait for it (the ruled display, pinned - // `it.fails`: re-arm under the boundary scope is deferred). Pre-L2 and - // `next` show no fallback: the content joins the hold (`next` reports the - // boundary pending). Without `on`: A15, the content joins the hold — as - // pre-L2 and next. (`memo`: + // fallback, and the hold does not wait for it (U-9; pre-L2 showed no + // fallback: the content joined the hold). Held, the re-armed content's + // derivation is staged in the hold, so `latest` serves it (A11, as for a + // fresh boundary's content above); in flight the boundary waits on every + // reader it caught or collected. Without `on`: A15, the content joins the + // hold — as pre-L2 and next. (`memo`: // the boundary's own render reads `m`, so its re-run creates a fresh // memo — no value until it loads; `bound`: the binding re-runs over the // committed memo, pending with its committed value.) const committed: Record = { "held rearmed memo": [ - "view fallback | readers=1 view=false m=false latest=throws NotReadyError", + "view fallback | readers=1 view=false m=false latest=c1 1", "view c1 1 · x 1 | readers=0 view=false m=false latest=c1 1", " | readers=0 view=false m=false latest=c1 1" ], "held rearmed bound": [ - "view fallback | readers=1 view=false m=true latest=c0 0", + "view fallback | readers=1 view=false m=true latest=c1 1", "text c1 1 · view p · x 1 | readers=0 view=false m=false latest=c1 1", " | readers=0 view=false m=false latest=c1 1" ], @@ -832,12 +828,12 @@ describe("#3540: the boundary scope — committed content, held mounts, no catch " | readers=0 view=false m=false latest=c1 1" ], "flight rearmed memo": [ - "view fallback | readers=1 view=false m=true latest=c0 0", + "view fallback | readers=3 view=false m=true latest=c0 0", "holder 1 · view c1 1 | readers=0 view=false m=false latest=c1 1", " | readers=0 view=false m=false latest=c1 1" ], "flight rearmed bound": [ - "view fallback | readers=1 view=false m=true latest=c0 0", + "view fallback | readers=2 view=false m=true latest=c0 0", "holder 1 · text c1 1 · view p | readers=0 view=false m=false latest=c1 1", " | readers=0 view=false m=false latest=c1 1" ], @@ -855,51 +851,47 @@ describe("#3540: the boundary scope — committed content, held mounts, no catch for (const source of ["held", "flight"] as Source[]) for (const withOn of [true, false]) for (const kind of ["memo", "bound"] as Content[]) - // Re-arm over committed content: deferred to the re-arm change, pending its ruling. - (withOn ? it.fails : it)( - `${source}, committed content ${withOn ? "under a re-armed boundary" : "under a plain boundary"}, ${kind}`, - async () => { - const log: string[] = []; - const [key, setKey] = createSignal(0); - let w!: ReturnType; - let m!: () => string; - let view!: () => unknown; - let owner: object | undefined; - createRoot(() => { - w = world(source, log); - view = Loading( - () => { - owner = getOwner()!; - m = createMemo(() => `c${key()} ${w.read()}`); - if (kind === "memo") return m(); - createRenderEffect(m, v => { - log.push(`text ${v}`); - }); - return "p"; - }, - "fallback", - withOn ? key : undefined - ); - createRenderEffect(view, v => { - log.push(`view ${v}`); - }); - }); - flush(); - await w.settle(); - const out = await steps( - log, - w, - () => setKey(1), - () => ({ - readers: readersOf(owner), - view: verdict(() => view(), "pending"), - m: verdict(() => m(), "pending"), - latest: verdict(() => m(), "latest") - }) + it(`${source}, committed content ${withOn ? "under a re-armed boundary" : "under a plain boundary"}, ${kind}`, async () => { + const log: string[] = []; + const [key, setKey] = createSignal(0); + let w!: ReturnType; + let m!: () => string; + let view!: () => unknown; + let owner: object | undefined; + createRoot(() => { + w = world(source, log); + view = Loading( + () => { + owner = getOwner()!; + m = createMemo(() => `c${key()} ${w.read()}`); + if (kind === "memo") return m(); + createRenderEffect(m, v => { + log.push(`text ${v}`); + }); + return "p"; + }, + "fallback", + withOn ? key : undefined ); - expect(out).toEqual(committed[`${source} ${withOn ? "rearmed" : "plain"} ${kind}`]); - } - ); + createRenderEffect(view, v => { + log.push(`view ${v}`); + }); + }); + flush(); + await w.settle(); + const out = await steps( + log, + w, + () => setKey(1), + () => ({ + readers: readersOf(owner), + view: verdict(() => view(), "pending"), + m: verdict(() => m(), "pending"), + latest: verdict(() => m(), "latest") + }) + ); + expect(out).toEqual(committed[`${source} ${withOn ? "rearmed" : "plain"} ${kind}`]); + }); // A boundary mounted as part of the hold — ``'s memo // creates it, at top level or in a revealed boundary: it appears at the @@ -1017,9 +1009,7 @@ describe("#3540: the boundary scope — semantic fuzzer findings (rev 19, seed 9 // of it is a read of content the boundary owns, so the tree leaves the // transaction and re-derives (pending under the boundary) rather than // being recorded as a settled reader — which redrew the boundary forever. - // Re-arm under the boundary scope is deferred, pending its ruling: next's - // display here is the held content, no fallback. - it.fails("a held tree under a re-armed boundary re-derives — no redraw loop", async () => { + it("a held tree under a re-armed boundary re-derives — no redraw loop", async () => { const [s1, setS1] = createSignal(0); const [s2, setS2] = createSignal(0); const [visible, setShow] = createSignal(true); diff --git a/packages/signals/tests/loading-on-frame-following-3540.test.ts b/packages/signals/tests/loading-on-frame-following-3540.test.ts index e190332a5..524cf8e14 100644 --- a/packages/signals/tests/loading-on-frame-following-3540.test.ts +++ b/packages/signals/tests/loading-on-frame-following-3540.test.ts @@ -377,44 +377,102 @@ describe("3. an outside hold on the SAME source: the frame waits, the fallback c // The same source read outside, but its flight comes from an earlier write // the `on` change is not part of: nothing holds the change's frame, the // re-armed boundary owns its content (A29's boundary scope, 2026-10-06), so -// the fallback shows now — and nothing is reported. Re-arm under the -// boundary scope is deferred to a separate change, pending its ruling. +// the fallback shows now — and nothing is reported (U-9). describe("3b. an outside reader of a flight the `on` change did not start: the fallback shows now, not reported", () => { for (const outside of ["effect", "loading"]) - test.fails( - `outside ${outside === "effect" ? "render effect" : "revealed Loading"}`, - async () => { + test(`outside ${outside === "effect" ? "render effect" : "revealed Loading"}`, async () => { + const d = captureWarnings(); + const [x, setX] = createSignal(0); + const [key, setKey] = createSignal(0); + const pending: (() => void)[] = []; + const log: string[] = []; + let dispose!: () => void; + createRoot(dispose_ => { + dispose = dispose_; + const data = createMemo( + async () => { + const v = x(); + await new Promise(r => pending.push(r)); + return v; + }, + { name: "data" } + ); + const read = + outside === "effect" + ? data + : untrack(() => + createLoadingBoundary( + () => `holder ${data()}`, + () => "holder fallback" + ) + ); + createRenderEffect(read, v => { + log.push(`outside ${v}`); + }); + const view = untrack(() => + createLoadingBoundary( + () => `c${key()} ${data()}`, + () => "fallback", + { on: key } + ) + ); + createRenderEffect(view, v => { + log.push(`view ${v}`); + }); + }); + flush(); + const settle = async () => { + while (pending.length) pending.shift()!(); + for (let i = 0; i < 8; i++) await microtask(); + flush(); + }; + await settle(); + log.length = 0; + setX(1); + flush(); + expect(log).toEqual([]); + setKey(1); + flush(); + expect(log).toEqual(["view fallback"]); + await settle(); + expect(log.slice(1).sort()).toEqual([ + outside === "effect" ? "outside 1" : "outside holder 1", + "view c1 1" + ]); + expect(d.codes()).toEqual([]); + d.stop(); + dispose(); + }); +}); + +// Content an earlier action holds with no flight (a write it staged): nothing +// under the boundary is pending, but the re-armed boundary owns its content — +// it leaves the hold, the fallback shows now, and the content reveals at the +// action's commit. A re-arm inside the action is the action's frame: no +// fallback, the content lands with the commit (U-9). +describe("3c. content an earlier action holds by a staged write: the re-arm shows the fallback now", () => { + for (const content of ["direct", "memo", "bound"] as const) + for (const inside of [false, true]) + test(`${content} content, re-armed ${inside ? "inside" : "after"} the action`, async () => { const d = captureWarnings(); const [x, setX] = createSignal(0); const [key, setKey] = createSignal(0); - const pending: (() => void)[] = []; const log: string[] = []; + let done!: () => void; let dispose!: () => void; createRoot(dispose_ => { dispose = dispose_; - const data = createMemo( - async () => { - const v = x(); - await new Promise(r => pending.push(r)); - return v; - }, - { name: "data" } - ); - const read = - outside === "effect" - ? data - : untrack(() => - createLoadingBoundary( - () => `holder ${data()}`, - () => "holder fallback" - ) - ); - createRenderEffect(read, v => { - log.push(`outside ${v}`); - }); const view = untrack(() => createLoadingBoundary( - () => `c${key()} ${data()}`, + () => { + if (content === "direct") return () => `c ${x()}`; + const m = createMemo(() => `c ${x()}`); + if (content === "memo") return m; + createRenderEffect(m, v => { + log.push(`bind ${v}`); + }); + return "

"; + }, () => "fallback", { on: key } ) @@ -424,102 +482,32 @@ describe("3b. an outside reader of a flight the `on` change did not start: the f }); }); flush(); - const settle = async () => { - while (pending.length) pending.shift()!(); - for (let i = 0; i < 8; i++) await microtask(); - flush(); - }; - await settle(); + const shows = (v: number) => + content === "bound" ? [`bind c ${v}`, "view

"] : [`view c ${v}`]; + expect(log).toEqual(shows(0)); log.length = 0; - setX(1); + action(function* () { + setX(1); + if (inside) setKey(1); + yield new Promise(r => (done = r)); + })(); flush(); expect(log).toEqual([]); - setKey(1); + if (!inside) { + setKey(1); + flush(); + expect(log).toEqual(["view fallback"]); + } + done(); + for (let i = 0; i < 8; i++) await microtask(); flush(); - expect(log).toEqual(["view fallback"]); - await settle(); - expect(log.slice(1).sort()).toEqual([ - outside === "effect" ? "outside 1" : "outside holder 1", - "view c1 1" - ]); + expect(log).toEqual( + inside ? (content === "bound" ? ["bind c 1"] : shows(1)) : ["view fallback", ...shows(1)] + ); expect(d.codes()).toEqual([]); d.stop(); dispose(); - } - ); -}); - -// Content an earlier action holds with no flight (a write it staged): nothing -// under the boundary is pending, but the re-armed boundary owns its content — -// it leaves the hold, the fallback shows now, and the content reveals at the -// action's commit. A re-arm inside the action is the action's frame: no -// fallback, the content lands with the commit. The re-arm after the action -// is pinned: re-arm under the boundary scope is deferred, pending its ruling. -describe("3c. content an earlier action holds by a staged write: the re-arm shows the fallback now", () => { - for (const content of ["direct", "memo", "bound"] as const) - for (const inside of [false, true]) - (inside ? test : test.fails)( - `${content} content, re-armed ${inside ? "inside" : "after"} the action`, - async () => { - const d = captureWarnings(); - const [x, setX] = createSignal(0); - const [key, setKey] = createSignal(0); - const log: string[] = []; - let done!: () => void; - let dispose!: () => void; - createRoot(dispose_ => { - dispose = dispose_; - const view = untrack(() => - createLoadingBoundary( - () => { - if (content === "direct") return () => `c ${x()}`; - const m = createMemo(() => `c ${x()}`); - if (content === "memo") return m; - createRenderEffect(m, v => { - log.push(`bind ${v}`); - }); - return "

"; - }, - () => "fallback", - { on: key } - ) - ); - createRenderEffect(view, v => { - log.push(`view ${v}`); - }); - }); - flush(); - const shows = (v: number) => - content === "bound" ? [`bind c ${v}`, "view

"] : [`view c ${v}`]; - expect(log).toEqual(shows(0)); - log.length = 0; - action(function* () { - setX(1); - if (inside) setKey(1); - yield new Promise(r => (done = r)); - })(); - flush(); - expect(log).toEqual([]); - if (!inside) { - setKey(1); - flush(); - expect(log).toEqual(["view fallback"]); - } - done(); - for (let i = 0; i < 8; i++) await microtask(); - flush(); - expect(log).toEqual( - inside - ? content === "bound" - ? ["bind c 1"] - : shows(1) - : ["view fallback", ...shows(1)] - ); - expect(d.codes()).toEqual([]); - d.stop(); - dispose(); - } - ); + }); }); describe("4. `on: () => latest(id)`: the display-ahead read shows the fallback now, beside the held frame", () => { diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index 71827e81e..b7403c7ae 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -8,8 +8,8 @@ "minified": 27945 }, "app: hydrating (no stores) with Show/For/Loading/Errored/lazy": { - "cap": "17.93 KB", - "minified": 52998 + "cap": "18.09 KB", + "minified": 53350 }, "page: base server components (hydrating + dynamic + frames + sf reference)": { "cap": "34.334 KB", diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 93e6893ba..a2a400173 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -1910,6 +1910,15 @@ module.exports = [ // Size-Exception (#3884, 2026-10-08): 17.91 -> 17.93 KB, measured at 17,922 B // brotli / 52,998 B minified by CI (Size run 37873447222). Cap set to that // brotli size, rounded up to 0.01 KB. + // Size-Exception (a re-armed boundary owns content another hold staged, + // D-14/D-16/U-9, #4059, 2026-10-14): 17.93 -> 18.09 KB, CI measured + // 18,074 B against `next` @ c818604's 17,887 (+187 B; 144 B over the + // cap; +417 B minified) — the signals boundaries: `arm` records what an + // earlier hold staged under a re-armed boundary (`heldUnder`), `_fresh` + // keeps a pass under it the boundary's, and the seam folds a re-arm + // whose own flush joins that hold back into it. Cap set at measured + + // 10 B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-14 + // (floor-caps.json; the floors unchanged, 0 B minified). limit: floorCaps["app: hydrating (no stores) with Show/For/Loading/Errored/lazy"], capMinified: floorMinified["app: hydrating (no stores) with Show/For/Loading/Errored/lazy"], alias @@ -2415,8 +2424,17 @@ module.exports = [ // that base. Cap set at measured + 10 B rounded up to 0.01 KB, recorded // minified at the head's bytes. Accepted by the maintainer 2026-10-14 // (the floors unchanged, 0 B). - limit: "29.57 KB", - capMinified: 93380, + // Size-Exception (a re-armed boundary owns content another hold staged, + // D-14/D-16/U-9, #4059, 2026-10-14): 29.57 -> 29.65 KB, CI measured + // 29,631 B against `next` @ c818604's 29,537 (+94 B; 61 B over the + // cap; +417 B minified) — the signals boundaries: `arm` records what an + // earlier hold staged under a re-armed boundary (`heldUnder`), `_fresh` + // keeps a pass under it the boundary's, and the seam folds a re-arm + // whose own flush joins that hold back into it. Cap set at measured + + // 10 B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-14 + // (the floors unchanged, 0 B minified). + limit: "29.65 KB", + capMinified: 93714, alias }, { @@ -2678,8 +2696,17 @@ module.exports = [ // them their transaction instead of the committed screen. Cap set at // measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer // 2026-10-13. - limit: "13.07 KB", - capMinified: 37021, + // Size-Exception (a re-armed boundary owns content another hold staged, + // D-14/D-16/U-9, #4059, 2026-10-14): 13.07 -> 13.20 KB, CI measured + // 13,181 B against `next` @ c818604's 13,041 (+140 B; 111 B over the + // cap; +416 B minified) — the signals boundaries: `arm` records what an + // earlier hold staged under a re-armed boundary (`heldUnder`), `_fresh` + // keeps a pass under it the boundary's, and the seam folds a re-arm + // whose own flush joins that hold back into it. Cap set at measured + + // 10 B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-14 + // (the floors unchanged, 0 B minified). + limit: "13.20 KB", + capMinified: 37415, alias }, { @@ -2978,8 +3005,17 @@ module.exports = [ // them their transaction instead of the committed screen. Cap set at // measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer // 2026-10-13. - limit: "14.64 KB", - capMinified: 41549, + // Size-Exception (a re-armed boundary owns content another hold staged, + // D-14/D-16/U-9, #4059, 2026-10-14): 14.64 -> 14.79 KB, CI measured + // 14,778 B against `next` @ c818604's 14,639 (+139 B; 138 B over the + // cap; +416 B minified) — the signals boundaries: `arm` records what an + // earlier hold staged under a re-armed boundary (`heldUnder`), `_fresh` + // keeps a pass under it the boundary's, and the seam folds a re-arm + // whose own flush joins that hold back into it. Cap set at measured + + // 10 B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-14 + // (the floors unchanged, 0 B minified). + limit: "14.79 KB", + capMinified: 41943, alias: observeAlias }, { @@ -3437,8 +3473,17 @@ module.exports = [ // artifact byte-identical; the observe tier scenario above did not move. // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the // maintainer 2026-10-13. - limit: "29.24 KB", - capMinified: 87899, + // Size-Exception (a re-armed boundary owns content another hold staged, + // D-14/D-16/U-9, #4059, 2026-10-14): 29.24 -> 29.38 KB, CI measured + // 29,370 B against `next` @ c818604's 29,232 (+138 B; 130 B over the + // cap; +417 B minified) — the signals boundaries: `arm` records what an + // earlier hold staged under a re-armed boundary (`heldUnder`), `_fresh` + // keeps a pass under it the boundary's, and the seam folds a re-arm + // whose own flush joins that hold back into it. Cap set at measured + + // 10 B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-14 + // (the floors unchanged, 0 B minified). + limit: "29.38 KB", + capMinified: 88330, alias: observeAlias }, // Compiled-template scenarios (2026-10-05): the four `app:` fixtures above @@ -3596,8 +3641,17 @@ module.exports = [ // boundaries above it; a `record` helper folds the four reader-record // sites to pay for most of it. Cap set at measured + 10 B rounded up to // 0.01 KB. Accepted by the maintainer 2026-10-13 (the floors unchanged). - limit: "25.79 KB", - capMinified: 80723, + // Size-Exception (a re-armed boundary owns content another hold staged, + // D-14/D-16/U-9, #4059, 2026-10-14): 25.79 -> 25.92 KB, CI measured + // 25,904 B against `next` @ c818604's 25,802 (+102 B; 114 B over the + // cap; +417 B minified) — the signals boundaries: `arm` records what an + // earlier hold staged under a re-armed boundary (`heldUnder`), `_fresh` + // keeps a pass under it the boundary's, and the seam folds a re-arm + // whose own flush joins that hold back into it. Cap set at measured + + // 10 B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-14 + // (the floors unchanged, 0 B minified). + limit: "25.92 KB", + capMinified: 81041, alias }, { @@ -3709,8 +3763,17 @@ module.exports = [ // promise built from the trace's never-settling Promise stub. Cap set at // measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer // 2026-10-13. - limit: "31.64 KB", - capMinified: 100961, + // Size-Exception (a re-armed boundary owns content another hold staged, + // D-14/D-16/U-9, #4059, 2026-10-14): 31.64 -> 31.78 KB, CI measured + // 31,765 B against `next` @ c818604's 31,694 (+71 B; 125 B over the + // cap; +418 B minified) — the signals boundaries: `arm` records what an + // earlier hold staged under a re-armed boundary (`heldUnder`), `_fresh` + // keeps a pass under it the boundary's, and the seam folds a re-arm + // whose own flush joins that hold back into it. Cap set at measured + + // 10 B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-14 + // (the floors unchanged, 0 B minified). + limit: "31.78 KB", + capMinified: 101338, alias }, {