Skip to content

fix(signals): an Errored boundary recovers instead of looping when its memo is re-asked (#4403, #4404, #4405) - #4406

Merged
ryansolid merged 2 commits into
nextfrom
claude/project-thread-x1ore2-4403
Oct 29, 2026
Merged

ryansolid merged 2 commits into
nextfrom
claude/project-thread-x1ore2-4403

Conversation

@ryansolid

Copy link
Copy Markdown
Member

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:

How (signals only, 3 hunks):

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:

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/web client: 1559 passed, including the async-gen regression.spec.ts gate.
  • @solidjs/web hydrate: 708 passed.
  • @solidjs/web server: 1903 passed, 1 failed. The failure is server-functions-adapter-request "keeps a rebuild failure's own error…", which also fails on next.
  • pnpm --filter @solidjs/web test-types passes.

Generated by Claude Code

…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-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b16c341

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 15 packages
Name Type
@solidjs/signals Patch
test-integration Patch
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
solid-js Patch
@solidjs/universal Patch
board-example Patch
canvas-example Patch
server-dbmon-example Patch
todos-server-example Patch

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

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.51 KB +26 B (+0.3%) +63 B +52 B 7.44 KB ❌ over by 72 B
signals: + createStore 15.04 KB +43 B (+0.3%) +69 B −1166 B 15.04 KB ⚠️ over by 5 B, 1186 B minified headroom
signals: + isPending/latest 11.02 KB +26 B (+0.2%) +69 B +1287 B 10.38 KB ❌ over by 639 B
signals: + until/refresh (truth readers, no optimism) 8.14 KB +26 B (+0.3%) +67 B +58 B 8.05 KB ❌ over by 91.99999999999909 B
app: render + one signal (the simple-app floor) 9.87 KB +23 B (+0.2%) +66 B −6 B 9.75 KB ⚠️ over by 117 B, 26 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.79 KB +66 B (+0.4%) +65 B +106 B 18.49 KB ❌ over by 300 B lazy-page.js 0.04 KB
app: hydrating + every store primitive family 32.41 KB +8 B (+0.0%) +61 B +1225 B 31.80 KB ❌ over by 607 B lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.52 KB +24 B (+0.2%) +65 B +69 B 13.40 KB ❌ over by 119 B lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.28 KB +34 B (+0.2%) +69 B +65 B 15.14 KB ❌ over by 136 B lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.84 KB +5 B (+0.0%) +65 B +62 B 29.70 KB ❌ over by 138 B lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.06 KB +30 B (+0.3%) +66 B −5 B 9.93 KB ⚠️ over by 129 B, 25 B minified headroom
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.38 KB −14 B (−0.1%) +68 B −253 B 26.27 KB ⚠️ over by 111 B, 273 B minified headroom stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.57 KB +31 B (+0.1%) +62 B −158 B 32.25 KB ⚠️ over by 317 B, 178 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.76 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.77 KB +25 B (+0.1%) +65 B +185 B 33.37 KB ❌ over by 399 B assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.03 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 38.66 KB −10 B (−0.0%) +65 B +1285 B 37.75 KB ❌ over by 905 B assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.04 KB, wire.js 3.47 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.68 KB −8 B (−0.0%) +61 B +192 B 35.43 KB ❌ over by 249 B assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 8.99 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 42.59 KB +21 B (+0.0%) +75 B +1396 B 41.56 KB ❌ over by 1035 B eager (counted): web.js 22.69 KB; assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.07 KB, sc-comments.js 0.19 KB, trace.js 9.05 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.30 KB +130 B (+0.3%) +67 B +187 B 40.81 KB ❌ over by 493 B assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.03 KB, serverForms.js 3.57 KB, trace.js 9.01 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 48.27 KB +25 B (+0.1%) +63 B +1407 B 47.16 KB ❌ over by 1113 B eager (counted): client.js 27.86 KB; assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.03 KB, serverForms.js 3.30 KB, trace.js 9.00 KB, wire.js 3.49 KB
server: floor (getRequestEvent + isServer) 1.27 KB 0 B 0 B −69 B 1.34 KB ✅
server: renderToString (the server-render floor) 22.79 KB 0 B 0 B 0 B 22.80 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • signals: + createStore: over brotli cap by 5 B; minified 44,164 B vs 45,330 B recorded with the cap (−1166 B) — 1186 B of the 20 B minified allowance left; +69 B minified over this PR's base
  • app: render + one signal (the simple-app floor): over brotli cap by 117 B; minified 27,023 B vs 27,029 B recorded with the cap (−6 B) — 26 B of the 20 B minified allowance left; +66 B minified over this PR's base
  • app: compiled floor (one template, one text hole, one delegated click): over brotli cap by 129 B; minified 27,555 B vs 27,560 B recorded with the cap (−5 B) — 25 B of the 20 B minified allowance left; +66 B minified over this PR's base
  • app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store): over brotli cap by 111 B; minified 80,249 B vs 80,502 B recorded with the cap (−253 B) — 273 B of the 20 B minified allowance left; +68 B minified over this PR's base
  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 317 B; minified 100,886 B vs 101,044 B recorded with the cap (−158 B) — 178 B of the 20 B minified allowance left; +62 B minified over this PR's base

❌ Fails the gate

  • signals: core floor (createSignal/Memo/Effect/Root/flush): over brotli cap by 72 B; minified 20,189 B vs 20,137 B recorded with the cap (+52 B) — 32 B past the 20 B minified allowance; +63 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • signals: + isPending/latest: over brotli cap by 639 B; minified 30,373 B vs 29,086 B recorded with the cap (+1287 B) — 1267 B past the 20 B minified allowance; +69 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • signals: + until/refresh (truth readers, no optimism): over brotli cap by 91.99999999999909 B; minified 22,004 B vs 21,946 B recorded with the cap (+58 B) — 38 B past the 20 B minified allowance; +67 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: hydrating (no stores) with Show/For/Loading/Errored/lazy: over brotli cap by 300 B; minified 53,726 B vs 53,620 B recorded with the cap (+106 B) — 86 B past the 20 B minified allowance; +65 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: hydrating + every store primitive family: over brotli cap by 607 B; minified 98,651 B vs 97,426 B recorded with the cap (+1225 B) — 1205 B past the 20 B minified allowance; +61 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: CSR with Show/For/Loading/Errored/lazy: over brotli cap by 119 B; minified 37,276 B vs 37,207 B recorded with the cap (+69 B) — 49 B past the 20 B minified allowance; +65 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: CSR, observe tier (same app on the observe artifacts): over brotli cap by 136 B; minified 42,228 B vs 42,163 B recorded with the cap (+65 B) — 45 B past the 20 B minified allowance; +69 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • app: CSR, observe tier + attribution engine enabled: over brotli cap by 138 B; minified 88,563 B vs 88,501 B recorded with the cap (+62 B) — 42 B past the 20 B minified allowance; +65 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: base server components (hydrating + dynamic + frames + sf reference): over brotli cap by 399 B; minified 101,744 B vs 101,559 B recorded with the cap (+185 B) — 165 B past the 20 B minified allowance; +65 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: live server components (base + live/GET + action + isPending/latest): over brotli cap by 905 B; minified 116,986 B vs 115,701 B recorded with the cap (+1285 B) — 1265 B past the 20 B minified allowance; +65 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread): over brotli cap by 249 B; minified 108,110 B vs 107,918 B recorded with the cap (+192 B) — 172 B past the 20 B minified allowance; +61 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: compiled live server components (the compiled base page + live/GET + action + isPending/latest): over brotli cap by 1035 B; minified 125,129 B vs 123,733 B recorded with the cap (+1396 B) — 1376 B past the 20 B minified allowance; +75 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 493 B; minified 125,574 B vs 125,387 B recorded with the cap (+187 B) — 167 B past the 20 B minified allowance; +67 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 1113 B; minified 142,390 B vs 140,983 B recorded with the cap (+1407 B) — 1387 B past the 20 B minified allowance; +63 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

@coveralls

coveralls commented Oct 29, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 40859284590

Coverage remained the same at 78.516%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1355
Covered Lines: 1125
Line Coverage: 83.03%
Relevant Branches: 1084
Covered Branches: 790
Branch Coverage: 72.88%
Branches in Coverage %: Yes
Coverage Strength: 29.98 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 19.38%

1 warning

⚠️ 3 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚡ 2 improved benchmarks
✅ 198 untouched benchmarks
⏩ 5 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ projection derive: write one NESTED field (reference) 2.7 ms 2.1 ms +26.98%
⚡ arrow in template 12.3 ms 11 ms +12.24%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/project-thread-x1ore2-4403 (b16c341) with next (81a12d8)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@ryansolid
ryansolid merged commit 3b7ea81 into next Oct 29, 2026
7 of 8 checks passed
ryansolid added a commit that referenced this pull request Oct 29, 2026
…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]>
@ryansolid
ryansolid deleted the claude/project-thread-x1ore2-4403 branch November 5, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants