Repository navigation
fix(signals): a frame node re-run outside its hold re-derives the readers of the staging it replaces (#4378) - #4390
Conversation
…ders of the staging it replaces (#4378) A reader recreated in the flush that clears an upstream error, under Errored > Loading, waits on the async memo's flight, so the frame holds and the error boundary's output stages its content with it. The rejection then re-runs the output outside that transaction's flush: it publishes mainline (#3404), back to the fallback it had committed. That is no change, so nothing re-derived its reader, and the landing applied the reader's held run of the superseded content: an empty hole with nothing in flight. The mainline re-pass now marks its subscribers when it supersedes a staging, so readers of that staging re-derive on the value it publishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 091c602 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)
|
…the floors don't pay The insertSubs in recompute's mainline-publish arm cost +16 B minified on every scenario, the frozen floors and hello world included. The case is a boundary's output: a redraw re-derives it outside the hold that staged its content, and it publishes the committed fallback back unchanged. The redraw now marks the output's readers when the output carries a staging, so they re-derive with it. boundaries.ts is tree-shaken from builds without a boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Coverage Report for CI Build 40785797795Coverage remained the same at 78.516%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Merging this PR will improve performance by 12.42%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
…aging Reading _pendingValue in boundaries.js moved the shared mangled names and every floor with them. CONFIG_HELD on _config, which boundaries.js reads already, marks the same staging. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…0cde2c Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Every frame node up the chain (the boundaries' trees and outputs) drops its staging there, most of them re-run from the heap rather than through redraw, so the generator's E8 case still failed with the redraw version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
|
CodSpeed flags
Generated by Claude Code |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
… a verdict memo read After #4390 the E9 memo shape showed an empty screen. A nested <Show>'s condition memo is created inside a pass that read the action's staging; it read the verdict memo with no staged read of its own, so enterLane moved it into the verdict lane (inline, markVerdictReader's creator walk keeps it in the frame). Its branch then became lane work; the fresh <Loading> staged its fallback, the seam's fold-back redrew it outside the hold, and #4390's re-derive published the errored tree's undefined committed value. enterLane now checks the creator chain the same way, through a shared creatorReads helper (core.ts) that markVerdictReader also uses. Golfed so the live server-components floor stays under its recorded 115,217 B (115,213 B). Ledger re-recorded over next @ 1b32ff2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…'s (E9) (#4394) * fix(signals): a verdict memo read after a staged read stays the frame's (E9) A memo of a verdict (`createMemo(() => latest(s))`, F-6) read by a pass that had already read the action's staging moved that pass into the holder's verdict lane (`laneRead` -> `enterLane`), and the lane showed it at once, with the staging it had read. Inline, `route` keeps such a pass the frame's. The async-app generator's E9 pair found the difference (seed 3289): `<Errored>` showed the branch the action's write recovers while the action was open. `enterLane` now keeps a derivation in that position the frame's, as `route` does inline (D-25), and queues a leaf (a render effect) for the seam's re-derive on the committed world, so `[s(), L()]` shows `v:0 l:1` as inline does (D-26). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * Re-record #4394's size ledger over next @ 178605c Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): a pass born inside a staged reader stays the frame's on a verdict memo read After #4390 the E9 memo shape showed an empty screen. A nested <Show>'s condition memo is created inside a pass that read the action's staging; it read the verdict memo with no staged read of its own, so enterLane moved it into the verdict lane (inline, markVerdictReader's creator walk keeps it in the frame). Its branch then became lane work; the fresh <Loading> staged its fallback, the seam's fold-back redrew it outside the hold, and #4390's re-derive published the errored tree's undefined committed value. enterLane now checks the creator chain the same way, through a shared creatorReads helper (core.ts) that markVerdictReader also uses. Golfed so the live server-components floor stays under its recorded 115,217 B (115,213 B). Ledger re-recorded over next @ 1b32ff2. 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
Fixes #4378
Before: under
<Errored>›<Loading>›<Show when={epoch()} keyed>›<span>{m1()}</span>, a write that clears an upstream error (m0stops throwing, som1asks for a value that rejects), batched withsetEpoch(2), recreates the<span>reader in the same flush. The screen ended at<span></span>with nothing in flight. WithoutsetEpoch(2), the kept reader ends at[ERR], which matches a fresh render. This was the async-app generator's E8 pair (birth seat).After: both end at
[ERR].Root cause
The recreated reader waits on
m1's new flight, so the flush parks. The transaction holds the frame, and the error boundary's output stages"content"with it (its committed value is the fallback). Whenm1rejects, the reader errors and the boundary catches it and re-derives the output. That pass runs outside the transaction's flush, sorecompute's head publishes it mainline (#3404): the held staging is dropped (_pendingValue = NOT_PENDING) and the pass returns the fallback. The fallback is equal to the committed value, sovalueChangedis false and no subscriber is notified. The output's reader (the insert's render effect) had run its pass in the parked flush on the staged"content", and that run was stashed with the transaction. At the landing the stale run applies"content": the empty hole.The generator's current E8 case (
Erroredinside the recreated tree, under the rootLoadingandErrored) has the same cause one level up. The boundary trees and outputs above hold the frame's staging as well. Each one re-runs from the heap, outside the hold, back to its committed[E], and has nothing new to tell the next level up.How
recompute(core.ts), in the "published mainline, not held" arm for a frame reader: when the pass replaces a staging (_pendingValue !== NOT_PENDING), it marks its subscribers (insertSubs). The readers of the replaced staging then re-derive on the value the pass publishes, even when that value is the committed one and there is no change to notify them by. Without a staging, nothing extra runs.No new exports, effects or computations.
I also tried two boundary-local versions (
boundaries.tsredraw), so the floors wouldn't pay. Both cover the issue's shape but not the generator's E8 case, where the staging is replaced at boundary trees and outputs that re-run from the heap, not throughredraw. The arm inrecomputeis the one place every frame node drops a staging.How did you test this change?
packages/signals/tests/recreated-reader-held-boundary-4378.test.ts: the issue's tree as signals boundaries (Errored › Loading › keyed child creating the reader), with the reader kept and recreated. Onnext, "recreated in that flush" fails with the screen atspan(empty slot). With the fix both pass.packages/web/test/recreated-reader-held-boundary-4378.spec.tsx: the issue's JSX repro, kept and recreated, plus the generator's current E8 case pinned throughrunScenario. Onnext, the recreated repro and the E8 pin fail with<span></span>. With the fix all 3 pass.ASYNC_GEN=1 ASYNC_GEN_PAIRS=E8,E9 ASYNC_GEN_CASES=1000:equivalence/E8: error vs contentgoes from 1 to 0. The other findings show up the same way without this change.turbo build --forceafter mergingnext@ b68942c (next has since gained only docs commits), then:c03-hydration-done-counts-holds"…has claimed by hydration end" pin, which flakes onnexttoo.server-functions-adapter-request("keeps a rebuild failure's own error…") fails here as it does onnext.turbo typecheck test-types: clean.scripts/size, local linux, head vsorigin/next@ b68942c, both measured from clean builds in this worktree):gate.mjspasses, andcheck-floor-caps.mjs origin/nextreports no cap raised.The fix sits in
recompute, which every build carries, so every client scenario grows by +15 or +16 B minified, the frozen floors and hello world included. Hello world's brotli goes from 9,933 to 9,962 B. No frozen floor's cap or recorded minified moves: each still measures under its recorded minified. Every scenario that grew gets a ledger line inscripts/size/scenarios.js. Onlysignals: + isPending/latestmeasures above its recorded minified, and itscapMinifiedgoes from 28,680 to 28,693. No caps change.🤖 Generated with Claude Code
https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2