Repository navigation
fix(signals): a verdict reader born into a hold reads it whichever flush runs it (#4426) - #4454
Conversation
…y transaction's flush (#4426) `read` asked two deciders whether the pass was born into the hold, with two different holds. `frameRead` asked about the node's hold (`bornInto(c, txOf(el))`) and joined the pass, and `stagedScreen` then asked about the flush's transaction (`bornInto(c, flushTransaction)`). For a verdict reader born into T and run by another transaction's flush, the second answer was no, so it was served the committed value. That is the screen the first answer had just ruled it does not have (D-25: the hold's own work, born in its world, reads plainly; D-27 / A29: a node born into a hold has no screen). `stagedScreen` now takes the node. A held node (not a lane's) that `read` or `serve` brings here is one `frameRead` found the reader born into, because `frameRead` served every other one the screen. So it answers "no screen" for it, as it already did when the flush's transaction was that hold. Fixes #4426 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…-x1ore2-4426 # Conflicts: # packages/signals/docs/RULES-INDEX.md # packages/signals/src/core/core.ts
Measured locally (linux) over next @ 7135a77. On page: live server components (a frozen floor): 38,795 B brotli / 117,251 B minified, +24 B minified, so the cap goes 38.76 -> 38.81 KB and floor-caps.json's recorded minified 117,227 -> 117,251. On page: compiled live server components: 42,689 / 125,398, so the cap goes 42.67 -> 42.70 KB and the recorded minified 125,374 -> 125,398. Caps are at measured + 10 B rounded up to 0.01 KB. Every other scenario passes the gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 6658563 The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 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)
|
Coverage Report for CI Build 41057603758Coverage remained the same at 78.1%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 13.17%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
Ledger conflicts resolved to next's entries, then re-measured over next @ 9c4e4a8: frozen floors base SC 33.77 KB (101,638) and live SC 38.83 KB (117,331); compiled base SC 35.72 KB (108,007). Size-Exception in the PR body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
|
The red I checked it on a clean build of Generated by Claude Code |
createStore and the compiled CSR app were over their caps on next after #4454 and #4457 landed together; raise both by the cap rule and re-record the minified of every non-frozen scenario that moved. Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 Co-authored-by: Claude <noreply@anthropic.com>
…ad; seat rows D1''/L1''/V3'' (#4472) * fix(signals): a netted write read in a strict untrack is no staged read; seat rows D1''/L1''/V3'' #4452 made a signal's staging equal to its committed value (writes that netted, D-8) a read of the screen in `read`'s fast block. The reader-seat table still raised Fl on that read (rows D1 / L1 / D7, and V3 for a verdict reader), so the shadow's seat parted from the flags and every later read of the pass reported too ("world after the read", "verdict seat after the read", B2 "born held"): 8 INV-seat-3 classes (x59) in the signals suite, 7 in web, and 60 in the generator (37 / 35 / 43 at 2,000 cases x seeds 3289 / 91501 / 7, every pair). Bisected over every merge 7135a77..131ceb8: 0 before #4452 (66c6f56), 60 at it, none added by #4453, #4458, #4460, #4454, #4457 or #4455. The runtime follows the rule (D-8: a tick whose writes net to the committed value made no proposal), so the table was stale: rows D1''/L1'' (D7'' in a lane) and V3'' serve P and raise nothing. Shadow: 0 classes in signals, web and the generator. With the rows in, the shadow found the path #4452 missed: in dev a labelled `untrack` read (`strictRead`) takes the slow path, where `serve` still called `stagedRead`, so dev held the errored re-run that prod (fast block) does not. `serve` now skips it for a netted signal in dev; prod folds the test away (0 B on every scenario). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): gate serve's netted skip on the strict read, not on dev Only the strict untrack read skips the fast block in dev; a hydration snapshot or a promoted write reaches serve in dev and prod alike and keeps the staged-read mark. 0 B in prod. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --------- Co-authored-by: Claude <noreply@anthropic.com>
…dict reader (E2) (#4473) * fix(signals): a held frame's leaf is the hold's own work, not its verdict reader (E2) A render effect in a frame a held pass built (CONFIG_HELD_FRAME, e.g. the branch `<Show when={y() > 0}>` opens under an action holding `y`) re-run by the hold's next step showed two ways depending on memo wrapping: - inline `latest(y)`: `route` seated it as the hold's verdict reader, so its run went out at the verdict lane's reveal, ahead of its frame; - `createMemo(() => latest(y))` read plainly: `laneRead`'s L7' arm served the memo's staging as the hold's own value and the run waited for the landing (and a later run, with nothing staged, entered the verdict lane through `enterLane`). `[10, 20]` beside `[10]` for one hold; with #4455, the two-hold review shape (`[latest(y), m()]` inline vs a memo of it, or split leaves) the same. A member of the hold's held frame is born in its world: it reads plainly and a probe does not rescue it (D-25, as #4426/#4454 for its reads), and its runs are the frame's, which commits with the hold (D-27's joined pass, A29 ruling A). `route` and `enterLane`'s verdict arm now leave it the frame's when the lane's hold is the one it was born into; every form waits for the landing (the branch is not on screen before it). The reader-seat table carries the same clause (test-only). #4455's pin (`verdict-seat-merged-lane-4427`) asserted the inline leaf running ahead of its held frame (`1/20` at A's step); it now expects the frame's `1/10` until B's landing, and gains the mainline-mount contrast (`<Show when={latest(y) > 0}>`), which still shows `1/20`. Lives in verdict.ts / lanes.ts only: 0 B on every floor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): record the held-frame verdict clause's cost on the lane pages Measured locally (linux) over next @ 131ceb8. signals: + isPending/latest 11,125 B brotli / 30,704 B minified (+63 B), cap 11.12 -> 11.14 KB; page: compiled live server components 42,810 / 125,600 (+82 B), cap 42.73 -> 42.82 KB. Caps at measured + 10 B rounded up to 0.01 KB; recorded minified = measured. page: live server components (frozen floor) +63 B minified, under its brotli cap (38,825 vs 38.85 KB): not raised. Every floor without the verdict engine is 0 B. (app: hydrating (no stores) is over its cap on next as well, unchanged here.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * refactor(signals): drop #4455's held-hold comparison in laneRead; the held-frame clause covers it laneRead's D-28 seat reset goes back to comparing verdict lanes by identity. With the held-frame clause, no shape depends on the hold comparison, and the seat table's #4427 known entry no longer matches anything. -48 B minified on the verdict pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): re-record over next @ d0fc285 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Before: a verdict reader born into a hold T could be run by a flush that belongs to another transaction F. One example is an effect mounted by a content pass that T holds. When that reader read a node T holds, it was served the committed value.
Two deciders disagreed:
frameReadhad just joined the pass to T because the reader has no screen (bornInto(c, txOf(el))).stagedScreenthen asked about the flush's transaction instead (bornInto(c, flushTransaction)), so it handed the reader the screen anyway.In async-gen seed 3289 #911, the effect under
<Show when={s0()}>, mounted fors0 = 3, reads0() === 0.After: a reader born into a hold reads that hold's value of the nodes it holds, whichever flush runs it, and the two deciders agree. This restores D-25 (the hold's own work reads plainly) and D-27 / A29.
How
stagedScreennow takes the node being read. A held node that isn't a lane's only reachesstagedScreenfromreadorservewhenframeReadfound the reader born into it; every other held node was already served the screen. SostagedScreenanswers "no screen" for that node, the same answer it already gave when the flush's transaction was that hold. Lane nodes are excluded becauseframeReadnever asks about them.The cost is one mask compare, on the verdict / folded-lane staging path only. Nothing changes in
recomputeand no effects are added. The #4438 seat-shadowknown #4426entry now gets 0 hits. It is left in place for later cleanup.Size over next @ 9c4e4a8, measured locally (linux), brotli / minified:
signals: + createStoresignals: + isPending/latestpage: base server components(frozen floor)page: live server components(frozen floor)page: compiled base server componentspage: compiled live server componentsThe check sits in
core.js, so every bundle that shipsstagedScreencarries about 20 B minified. That includes+ createStore, compiled CSR and compiled hydrating, as well as the verdict pages. An earlier version of this body said the rest of the dist was byte-identical tonext, and that was wrong. Next was already 13 B minified over the recorded size on the base server-components floor (#4452), so this takes it past the allowance. The brotli on that page (+94 B) is layout of those bytes. The one available reorder came out worse.Size-Exception: the server-component pages carry the D-25 born-into-hold check that fixes #4426 (+20–24 B minified, the brotli is layout); the base page crosses its record only with next's +13 B (#4452); correctness.
How did you test this change?
packages/signals/tests/verdict-born-into-other-flush-4426.test.tsreplays seed 3289 Using a createStore array with createContext #911 at signals level. It fails onnext, where the content's effect reads 0, and passes with the fix.server-functions-adapter-requestfails the same way onnext.known #4426onnext, and nothing with the fix.Fixes #4426
Generated by Claude Code