fix(signals): a fold repeats no unchanged presence to a held node (#3743, supersedes #3744) - #3791
Conversation
🦋 Changeset detectedLatest commit: b32268c The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Size (brotli, eager entry chunk)
Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. Caps in |
Coverage Report for CI Build 37265932281Coverage remained the same at 75.991%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 12.17%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | memo + sync render effect only (reference) |
32 ms | 26.9 ms | +19.26% |
| ⚡ | dbmon shallow full tick |
53.3 ms | 50.6 ms | +5.51% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing port/3744-presence-folds (b32268c) with next (b61eef6)
) Port of #3744 (brenelz) onto L2 (#3774). The fold's presence loop (`notifyFoldTail`) wrote every observed `in` node with `key in neu`; `setSignal` joins a held node's transaction before its equality gate (A34 (1): a write to a held node is a proposal, the same value or another), so a `reconcile()` outside an action that had deleted an observed key — the snapshot leaving it absent too — made the whole mainline tick the action's: an unrelated `a.value` stayed stale until the action settled. Presence is now diffed `old` -> `neu` like the leaves (the fold's contract, #3296: the view the nodes were last told); a live chained `old` (a store proxy) is written unconditionally as before. `notifyFold` shares the tail. `applyAdopt` materializes a nested overlay draft before taking its diff base (`adoptPB` did so after it was taken): through the overlay's prototype a deleted key still read as present, so the presence skip fired on a real deletion, and a reconcile restoring the key never re-proposed on the held leaf, which committed the draft's `undefined` beside a backing that had the key. Two L2 fixes the diff uncovered, pinned by the existing #2719/#3164 and Q-D twin tests and a new leaf-only reader case: a container carrying an arrangement guess is told of an arrangement change whether or not anything subscribes to it (`notifyFoldTail`/`notifyWrites`) — for a guessed container the write is the landing that judges the guess (Q-D, plan sec. 39), and before only the unconditional presence write reached the lane, so a newer question's rows landing beneath an optimistic push published beside it for a reader of leaves alone; and `_laneRebase` no longer re-stages a slot whose row the committed backing already shows by key, nor an unchanged `length` (a spurious identical frame). Signals 4900 / 0 / 3 expected fails (+9 pins: 8 green, one `it.fails` for the open setter-path case — `t.wk` is cumulative, not per-setter), solid 819 / 0, web 1132 / 1411 / 275, 0 failures. Size (br): + createStore 14517 -> 14523, every store family 28770 -> 28765, page base 44017 -> 44028, page live 47656 -> 47668 (minified -97..-100 B each); every scenario within its cap. Co-authored-by: Brenley Dueck <brenleydueck@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
The port of #3744 measures 44,031 B brotli on the base server-components page against next @ 1a3f87f's 44,029 (+2 B, 1 B over the cap) while the same bundle is -97 B minified: brotli layout on the 150 KB bundle, not code. Cap set at measured + 10 B rounded up to 0.01 KB; dated note in scenarios.js beside the page-base notes. Size-Exception: page: base server components 44,029 -> 44,031 B (cap 44.03 -> 44.05 KB) — presence diff in store folds (#3743); source −97 B minified; accepted by the maintainer (2026-10-04) Co-authored-by: Cursor <cursoragent@cursor.com>
840fe3e to
b32268c
Compare
…e base 44.05 -> 44.78, page live 47.67 -> 48.45 Refetched content lands at the transition's commit (the staging, the content token, the two halves of the mount's follow effect, the staged data tables, and on L2 the switch's rebind in the effect half with the frameless host waiter). Measured against `next` @ bde4299 (after #3790/#3791): frames 12,997 -> 13,770 B (+773; +2,262 B minified, frames client +2,255), page base 44,048 -> 44,762 B (+714; +2,265 B minified), page live 47,670 -> 48,436 B (+766; +2,265 B minified). Caps set at head + 10 B. Ledger notes in scenarios.js re-applied from the PR's 1b347c7 against these numbers, after #3743's page-base note. Size-Exception: frames: eager client consumer 12,997 -> 13,770 B (cap 13.00 -> 13.78 KB); page: base server components 44,048 -> 44,762 B (cap 44.05 -> 44.78 KB); page: live server components 47,670 -> 48,436 B (cap 47.67 -> 48.45 KB) — #3759's staging so refetched content lands at the transition's commit; accepted by the maintainer (2026-10-04). Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* frames: refetched content lands at the transition's commit (#3759, ported onto L2) A refetch or single-flight region for a call a boundary is showing is staged in the transport and the call resolves to a content token; the mount lands it in the two halves of the render effect that follows its address accessor. Under the hold model the two halves ride core's seams unchanged: the compute half is the delivering transaction's pass (its `preview` writes — the staged slot args — are held with it and commit in the frame whose landing dissolves the lane's guesses), the effect half is its `land` (the commit replays the staged chunks). Both signals pins (`compute-write-joins-transition`, `settle-folds-queued-writes`) and the optimistic-hold specs were green on untouched L2; the morph-in-transition specs were red (3/3) and are closed by the staging. Composes with e133516 (the switch gate re-arms in the pass): the rebind — display — moves to the effect half so a switch's content waits for the commit too (`frames-morph-in-transition` switch case), while the gate settles on the new address's first write through a frameless waiter registered on the host, so a second switch mid-flight still binds (`call-driven-lifecycle`). No core seam touched. Size caps not raised: frames 12,997 -> 13,770 B (cap 13.00 KB), page base 44,017 -> 44,765, page live 47,656 -> 48,447 — reported for the maintainer's exception. Signals 4898/0/2, web 1138/0 + server 1411/2 skipped + hydrate 275/0. Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * size: Size-Exceptions for #3759 on L2 — frames 13.00 -> 13.78 KB, page base 44.05 -> 44.78, page live 47.67 -> 48.45 Refetched content lands at the transition's commit (the staging, the content token, the two halves of the mount's follow effect, the staged data tables, and on L2 the switch's rebind in the effect half with the frameless host waiter). Measured against `next` @ bde4299 (after #3790/#3791): frames 12,997 -> 13,770 B (+773; +2,262 B minified, frames client +2,255), page base 44,048 -> 44,762 B (+714; +2,265 B minified), page live 47,670 -> 48,436 B (+766; +2,265 B minified). Caps set at head + 10 B. Ledger notes in scenarios.js re-applied from the PR's 1b347c7 against these numbers, after #3743's page-base note. Size-Exception: frames: eager client consumer 12,997 -> 13,770 B (cap 13.00 -> 13.78 KB); page: base server components 44,048 -> 44,762 B (cap 44.05 -> 44.78 KB); page: live server components 47,670 -> 48,436 B (cap 47.67 -> 48.45 KB) — #3759's staging so refetched content lands at the transition's commit; accepted by the maintainer (2026-10-04). Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Port of #3744 (@brenelz) onto
nextafter the L2 hold model (#3774). The PR's tests are taken verbatim (plus three pins the port needed); the fix is re-implemented in L2's fold/notify path. Supersedes #3744.Co-authored-by: Brenley Dueck.Closes #3743
What the issue was, on L2
notifyFoldTail(the fold's presence loop) wrote every observedinnode withkey in neu, unconditionally. L2'ssetSignaljoins a held node's transaction before its equality gate — A34 (1): a write to a held node is a proposal, the same value or another. So areconcile()outside an action that had deleted an observed key — the snapshot leaving it absent too — made the whole mainline tick the action's: an unrelateda.valuewritten on the same tick stayed stale until the action settled (#3743).The fix
old → neulike the leaves (the fold's contract, Solid 2: Same-batch store reset leaves memos and effects holding cancelled values #3296: notify against the view the nodes were last told). A live chainedold(a store proxy, §7b) is written unconditionally as before.notifyFoldshares the tail (notifyFoldTail).applyAdoptmaterializes a nested overlay draft before capturing its diff base (adoptPBdid so only after the base was taken). Through the overlay's prototype a deleted key still read as present, so the presence skip fired on a real deletion. This also fixes a pre-existing value-loop bug: a reconcile restoring the key never re-proposed on the held leaf, which committed the draft'sundefinedbeside a backing that had the key (pinned: "value twin").Two fixes beyond the PR (maintainer rulings)
notifyFoldTailand setter pathnotifyWrites). A container carrying an arrangement guess (CONFIG_OVERRIDE) is told of an arrangement change whether or not anything subscribes to it: for a guessed container the write is the landing that judges the guess (A18; Q-D, plan sec. 39). Before, the unconditional presence write was the one landing that reached the lane; with presence diffed, a newer question's rows landing beneath an optimistic push published beside it for a reader of leaves alone. Forced by the existingcreateOptimisticStorepin "2.0.0-beta.14: Optimistic updates tearing fresh data #2719/2.0.0-rc until doesnt entangle the update resolving it with the action update #3164 holds optimistic rows across a separate source landing" going red after the diff; pinned on both paths by two new leaf-only-reader tests._laneRebasecompares by key, never by raw identity: a slot whose row the committed backing already shows (the same key's new object — its target adopted it) stays the committed backing's, andlengthis re-staged only when the committed length differs. Forced bysignal-store-twins-qd's store case emitting a spurious identical frame after (1). Doc comment says "by key" explicitly.Open, pinned
it.fails"a later mainline setter that does not write the held key publishes on the mainline tick" — the setter-path case from the #3743 thread. Spec (A34 (1)): an explicit repeat of the held key joins; a key the later setter did not write publishes on the mainline tick. Current L2 joins:
t.wk(the written keysnotifyWritesvisits) is retained for the pending fold and cumulative across the batch's setters, so the later setter re-notifies the held node with its unchanged value andsetSignaljoins before the gate. Fix direction: per-setter written keys for the notify; the fold keeps the batch's union. Not fixed here.Spec touches
SPEC-ASYNC-SEMANTICS.md: A34 Mechanism (L2) gains "a store fold's notifications are diffs (#3296, #3743)…"; A18 Mechanism (L2) gains the_laneRebaseby-key clause and "a store landing reaches a guess through the fold's diff… the container's arrangement guess is written by every landing that changes the arrangement — whether or not anything subscribes". A34 "Pinned by" lists the new file.RULES-INDEX.mdregenerated.Public API changes
None.
Tests
New file
tests/store/unchanged-presence-no-hold-3743.test.ts: 9 tests — the PR's 5 verbatim (issue case, contrast, two wide-object overlay cases, chained projection), plus the value twin, the two leaf-only-reader container-guess pins (fold path, setter path), and theit.failsabove.rules-index.mjs --checkgreen)Performance
reconcile-dbmon/listened-paths/write-flooron the prod dist vs anextbuild, back-to-back: no consistent direction. The machine was shared with another build during the window (load 6–12); thenextbaseline itself swung ±25% between pairs and every port-vs-next delta flipped sign across runs. In the quietest pair: dbmon ticks −2…−3% (within ±9–12% rme),all ~12k paths+2%, owned backings +1%. Nothing in the change touches a hot loop (the setter-path addition is one_configbit test on the container gate). CodSpeed is the real number.Size
Brotli vs
next@ 1a3f87f (Rolldown 1.2.11), minified delta where it changed:Page base was 1 B over its frozen cap (brotli layout on the 150 KB bundle; the same bundle is −97 B minified). Raised 44.03 → 44.05 KB (measured + 10 B, rounded up to 0.01 KB) in a separate commit with a dated note in
scenarios.js.Size-Exception: page: base server components 44,029 -> 44,031 B (cap 44.03 -> 44.05 KB) — presence diff in store folds (#3743); source −97 B minified; accepted by the maintainer (2026-10-04)