Repository navigation
fix(signals): an Errored boundary recovers instead of looping when its memo is re-asked (#4403, #4404, #4405) - #4406
Conversation
…s memo is re-asked (#4403, #4404, #4405) A root <Errored> on its fallback waits on the source that threw (D-39). The seam drops a source it judges settled and redraws, and the output's pass re-catches the tree's error without re-running it. Each shape left the tree carrying an error the graph would never re-derive, so the redraw re-caught it every round ("Potential Infinite Loop Detected"). - #4404 (D-41: a derivation that reads a staged error throws it): a reader born in the flush read the committed error of a memo whose new error was staged; its stale error committed after the new one. `read` now serves such a reader the staging (`serve`), as it would a staged value. - #4403 (D-41: a recovery equal to the last good value is a change): a pass replacing an error the memo had staged (with a hold it joined, D-8) was judged unchanged when it computed the committed value, so the `latest()` readers that threw the staged error kept it. Such a pass is now a change. - #4405 (D-3: structure reads the screen and re-derives at the commit; D-39): `reruns` skipped a boundary tree errored at the landing of the action that remounted its content, so it never linked the new content whose recovery had to reach it. Boundary structure now re-derives there. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: b16c341 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 40859284590Coverage remained the same at 78.516%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 19.38%1 warning
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…r, not the status wrapper (#4423) * fix(signals): a halt from a computed's error rethrows the user's error, not the status wrapper escalate() halted with the unwrapped cause but rethrew the StatusError, whose own `source` is the reactive node. A flush in a microtask turned that into an unhandled rejection, and a reporter serializing it walked the whole graph (vitest ran out of memory on the #4406 review repro). Rethrow the user's error, as the halt message already reports it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): drop escalate's separate rethrow argument Every call site now rethrows the cause it halts with, so the parameter goes: -6 B minified on the floors instead of +5. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --------- Co-authored-by: Claude <[email protected]>
Summary
Before: A root
<Errored>that was showing its fallback after a sync memo threw could get stuck in a loop when a write or action asked that memo again. Dev threw "Potential Infinite Loop Detected", prod hung, and every later flush threw until the root was disposed. The boundary waits on the source that threw (D-39). The seam drops a source it judges settled and redraws the output. The output's pass then catches the tree's error again without re-running the tree. In all three shapes the tree was left carrying an error the graph would never re-derive, so each redraw caught it again.After: The boundary settles on the right screen in all three shapes:
[E m1:5]).latest(s1)through m0 (D-25), the same as the no-remount sibling.How (signals only, 3 hunks):
read's errored branch, a reader that reads a memo whose new error is staged (CONFIG_STAGED_ERROR) is served that staging (serve), the same way it would be served a staged value. Before, a reader born in the flush threw the committed error, and its stale error committed after the new one.recompute, a pass that replaces an error this node staged is alwaysvalueChanged. In the original script the error was staged with the hold of the action whose slot the poisoning write joined (D-8). The pass that brought back the committed value was judged unchanged, so thelatest()readers that had thrown the staged error kept it.rerunsno longer skips boundary structure (aCONFIG_FRAMEnode with no effect_type) when it is errored at a landing. Skipped, the tree stayed linked to the disposed content and never linked the remounted content whose recovery had to reach it.No guard drops work, and no effects or public APIs are added. The cost is about +61–69 B minified in every scenario that ships the core. Hello world ("app: render + one signal") goes from 26946 to 27012 min (+66) and from 9817 to 9873 br (+56).
Size-Exception: signals core floor: 20115 → 20178 min (+63), 7476 → 7497 br (+21). The minified figure is over its frozen 20137. The cost is the three rule fixes for #4403/#4404/#4405, which stop a dev/prod loop.
Size-Exception: app hydrating (no stores): 53650 → 53715 min (+65), 18770 → 18769 br (−1). It was already over its frozen 53620 on next.
Size-Exception: page base server components: 101668 → 101733 min (+65), 33705 → 33684 br (−21). It was already over its frozen 101559 on next.
Size-Exception: page live server components: 116062 → 116124 min (+62), 38346 → 38330 br (−16). It was already over its frozen 115217 on next.
Note on #4403's "Expected": while the action recovering the memo to its last good value is held, the screen stays
[E m:2], and the content shows only once the action lands. This follows D-41 (recovery is the mirror of an error: staged, revealed with its frame) and D-39. The minimized form already holds on next since #4401. Its pin stays as a guard, and the original script is the one that still fails there.Fixes #4403
Fixes #4404
Fixes #4405
How did you test this change?
New pins:
packages/signals/tests/errored-recovery-wedge-4403-4404-4405.test.tsholds the minimized shapes in signals primitives. The [2.0 / next] A write that re-poisons a throwing memo chain under an Errored on its fallback, batched with a keyed-Show remount, wedges the flush: Potential Infinite Loop Detected (last staged node: signal) — async generator seed 91501 case 5807 #4404, [2.0 / next] An action that un-poisons a throwing memo under an Errored showing its fallback (Show when=latest(m)) wedges the flush: Potential Infinite Loop Detected (last staged node: value) — async generator seed 3289 case 3963 #4403 (original script) and [2.0 / next] A second action that un-poisons a throwing memo chain, after the first action completed in the flush that errored the root and remounted the readers, wedges the flush: Potential Infinite Loop Detected (last staged node: computed) — async generator seed 7 case 1158 #4405 pins fail on next @ f94dcd5 and pass here. The [2.0 / next] An action that un-poisons a throwing memo under an Errored showing its fallback (Show when=latest(m)) wedges the flush: Potential Infinite Loop Detected (last staged node: value) — async generator seed 3289 case 3963 #4403 minimized pin passes on both (fixed by fix(signals): latest() read inline and through a memo show the same screen #4401) and stays as a guard.packages/web/test/async-gen/errored-recovery-wedge-4403-4405.spec.tsholdsrunScenarioreplays: [2.0 / next] An action that un-poisons a throwing memo under an Errored showing its fallback (Show when=latest(m)) wedges the flush: Potential Infinite Loop Detected (last staged node: value) — async generator seed 3289 case 3963 #4403 seed 3289 case 3963, the [2.0 / next] A second action that un-poisons a throwing memo chain, after the first action completed in the flush that errored the root and remounted the readers, wedges the flush: Potential Infinite Loop Detected (last staged node: computed) — async generator seed 7 case 1158 #4405 minimized form, and [2.0 / next] A second action that un-poisons a throwing memo chain, after the first action completed in the flush that errored the root and remounted the readers, wedges the flush: Potential Infinite Loop Detected (last staged node: computed) — async generator seed 7 case 1158 #4405 seed 7 case 1158. All three fail on next with a liveness finding and pass here. The [2.0 / next] A write that re-poisons a throwing memo chain under an Errored on its fallback, batched with a keyed-Show remount, wedges the flush: Potential Infinite Loop Detected (last staged node: signal) — async generator seed 91501 case 5807 #4404 replays don't reproduce throughrunScenarioon next, so they were not added.Each hunk was switched off in turn and the pins re-run. Each hunk is needed by its own pin.
Suites:
@solidjs/signals: 5931 passed.solid-js: 917 passed.@solidjs/webclient: 1559 passed, including the async-genregression.spec.tsgate.@solidjs/webhydrate: 708 passed.@solidjs/webserver: 1903 passed, 1 failed. The failure isserver-functions-adapter-request"keeps a rebuild failure's own error…", which also fails on next.pnpm --filter @solidjs/web test-typespasses.Generated by Claude Code