Skip to content

fix(signals): an error-boundary output reaching only hidden effects holds nothing through memos and nested boundaries (#4639 follow-up) - #4655

Merged
ryansolid merged 5 commits into
nextfrom
claude/fix-4639-gaps
Nov 3, 2026
Merged

ryansolid merged 5 commits into
nextfrom
claude/fix-4639-gaps

Conversation

@ryansolid

@ryansolid ryansolid commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #4639 (#4638), for the gaps found in its review ("Merge, fix next").

What was wrong

#4639 made a pending error-boundary output transparent to the hold: it is on screen only through its readers, so read only behind fallbacks it holds nothing. That only looked at the output's direct readers. Three shapes from the same root still held the re-armed frame (the <Loading on={sel()}> fallback never showed):

  1. memo: a memo built outside the <Loading> sits between the error-boundary output and the content.
  2. nested: an error boundary over an error boundary, both built outside the <Loading>.
  3. unread: an error-boundary output nobody reads, next to the content's plain read of the same source.

Also (4): DEV reported LOADING_ON_OUTSIDE_HOLD for the plain #4638 shape while the fallback was on screen. That false report was already on next before #4639: reportOutsideHold treated the error boundary's tree as an outside frame reader.

Probe frames after sel(3 → 1) (detail and comments both pending):

shape before after
memo / nested / unread Crash | c 1 (held) loading #1 | loading c
direct (#4638) loading #1 | loading c, warns same, no warning
header (output rendered outside) Crash / Crash | c 1, warns unchanged (D-16)

Which rules decide the expected frames

  • Vocabulary, "Reader": "Only a render effect is a reader … a memo counts only through the render effects that read it, wherever it was created, and a pending value no render effect reaches holds nothing and reaches no boundary." This covers (1), (2) and (3).
  • F-3: "A boundary catches a pending value when the render effect that reads it is under the boundary: where the memo was created does not matter." This covers (1) and (2).
  • F-4 / D-14: what an armed boundary catches holds nothing outside it. A flight whose only observers are render effects behind a fallback holds no change, so both fallbacks show with the change's frame.
  • D-16: only a live reader outside the boundary keeps a re-arm's fallback from being seen, and LOADING_ON_OUTSIDE_HOLD reports exactly that case ("the fallback can never be seen"). With the fallback on screen the report is false (4). The header shape still holds and still warns.

No existing pinned test changes.

Change (packages/signals/src/boundaries.ts)

  • hiddenBy(r, through?): below a CONFIG_REDERIVE output it now walks through every non-effect node (memo, nested boundary output, tree) to the render effects it reaches. A node that reaches none counts as hidden (1, which is truthy). This only ever happens for a non-effect, and heldRun only asks about effects. At the top level the walk still starts only from a boundary output, so other callers are unchanged. Walking from every non-effect node at the top level broke web tests.
  • reportOutsideHold (DEV only): a reader under a boundary whose output is hidden is not an outside reader.

hiddenBy is on the boundary path. About +55 B minified on rows with Loading/Errored (the per-walk seen Set that bounds the walk on memo lattices, and the render-effect terminal that keeps user effects from being readers). Hello world is 0 B. The Set is kept over an array: includes is O(n²) on wide fan-outs.

Size-Exception: the #4639 follow-up walks an error-boundary output through memos and nested boundaries, bounded per walk (seen Set) so wide graphs stay linear; about +55 B minified on Loading/Errored rows including the frozen hydrating, base and live server-component floors. Caps are re-recorded by the ledger PR after the merge batch.

Tests

  • packages/signals/tests/loading-on-errorboundary-through-4639.test.ts: direct, memo, nested and unread, each resolving and rejecting (unread resolves only). All four show both fallbacks now and log no LOADING_ON_OUTSIDE_HOLD. The header shape (D-16) holds and warns once. 7 of 8 fail on next.
  • packages/web/test/loading-on-errorboundary-through-4639.spec.tsx: memo, nested and unread through <Loading>. All 3 fail on next.
  • Signals tests touching boundaries (152 files) and web tests touching Loading/Errored (109 files) pass.

Fixes the review gaps of #4639 (#4638).


Generated by Claude Code

…olds nothing through memos and nested boundaries (#4639 follow-up)

#4639 made an error-boundary output transparent only to its direct readers.
A memo or a second error boundary built outside the `<Loading>` between it
and the content still held the frame, and so did an output nobody reads.
`hiddenBy` now walks through every non-effect node below a boundary output
to the render effects it reaches, and an output reaching none holds nothing
(Vocabulary "Reader", F-3, F-4, D-14). The DEV LOADING_ON_OUTSIDE_HOLD
report no longer fires for a reader under such a boundary while the
re-armed fallback is on screen (D-16).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
@changeset-bot

changeset-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f47ee47

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 Nov 3, 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.42 KB 0 B 0 B −91 B 7.43 KB ✅
signals: + createStore 15.01 KB 0 B 0 B 0 B 15.03 KB ✅
signals: + isPending/latest 11.90 KB 0 B 0 B 0 B 11.92 KB ✅
signals: + until/refresh (truth readers, no optimism) 8.05 KB 0 B 0 B −84 B 8.06 KB ✅
app: render + one signal (the simple-app floor) 9.80 KB 0 B 0 B −150 B 9.80 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 19.04 KB +12 B (+0.1%) +55 B +186 B 18.97 KB ❌ over by 67 B lazy-page.js 0.04 KB
app: hydrating + every store primitive family 33.11 KB +42 B (+0.1%) +55 B +190 B 32.98 KB ❌ over by 130 B lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.71 KB +39 B (+0.3%) +55 B +186 B 13.65 KB ❌ over by 64 B lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.48 KB +34 B (+0.2%) +55 B +186 B 15.41 KB ❌ over by 75 B lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 30.11 KB +31 B (+0.1%) +55 B +186 B 30.03 KB ❌ over by 79 B lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.97 KB 0 B 0 B −187 B 9.98 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.56 KB −15 B (−0.1%) +55 B +186 B 26.54 KB ❌ over by 17 B stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.86 KB +44 B (+0.1%) +55 B +187 B 32.79 KB ❌ over by 66 B stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.77 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 34.10 KB +70 B (+0.2%) +55 B +186 B 33.95 KB ❌ over by 146 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.22 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 39.94 KB +2 B (+0.0%) +55 B +206 B 39.92 KB ❌ over by 24 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.24 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.99 KB +71 B (+0.2%) +55 B +186 B 35.93 KB ❌ over by 59 B assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.17 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 44.04 KB +19 B (+0.0%) +55 B +206 B 43.97 KB ❌ over by 70 B eager (counted): web.js 23.52 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.22 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.56 KB −60 B (−0.1%) +55 B +192 B 41.53 KB ❌ over by 27 B 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.58 KB, trace.js 9.20 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 49.77 KB +75 B (+0.2%) +55 B +206 B 49.63 KB ❌ over by 140 B eager (counted): client.js 28.75 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.22 KB, wire.js 3.49 KB
server: floor (getRequestEvent + isServer) 1.27 KB 0 B 0 B 0 B 1.29 KB ✅
server: renderToString (the server-render floor) 22.86 KB 0 B 0 B +24 B 22.87 KB ✅

❌ Fails the gate

  • app: hydrating (no stores) with Show/For/Loading/Errored/lazy: over brotli cap by 67 B; minified 54,092 B vs 53,906 B recorded with the cap (+186 B) — 166 B past the 20 B minified allowance; +55 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 130 B; minified 99,641 B vs 99,451 B recorded with the cap (+190 B) — 170 B past the 20 B minified allowance; +55 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 64 B; minified 37,563 B vs 37,377 B recorded with the cap (+186 B) — 166 B past the 20 B minified allowance; +55 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 75 B; minified 42,513 B vs 42,327 B recorded with the cap (+186 B) — 166 B past the 20 B minified allowance; +55 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 79 B; minified 88,842 B vs 88,656 B recorded with the cap (+186 B) — 166 B past the 20 B minified allowance; +55 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: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store): over brotli cap by 17 B; minified 80,500 B vs 80,314 B recorded with the cap (+186 B) — 166 B past the 20 B minified allowance; +55 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: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 66 B; minified 101,221 B vs 101,034 B recorded with the cap (+187 B) — 167 B past the 20 B minified allowance; +55 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 146 B; minified 102,212 B vs 102,026 B recorded with the cap (+186 B) — 166 B past the 20 B minified allowance; +55 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 24 B; minified 120,310 B vs 120,104 B recorded with the cap (+206 B) — 186 B past the 20 B minified allowance; +55 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 59 B; minified 108,546 B vs 108,360 B recorded with the cap (+186 B) — 166 B past the 20 B minified allowance; +55 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 70 B; minified 128,562 B vs 128,356 B recorded with the cap (+206 B) — 186 B past the 20 B minified allowance; +55 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 27 B; minified 126,300 B vs 126,108 B recorded with the cap (+192 B) — 172 B past the 20 B minified allowance; +55 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 140 B; minified 146,123 B vs 145,917 B recorded with the cap (+206 B) — 186 B past the 20 B minified allowance; +55 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 Nov 3, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 41613840475

Coverage remained the same at 78.249%

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: 1370
Covered Lines: 1132
Line Coverage: 82.63%
Relevant Branches: 1108
Covered Branches: 807
Branch Coverage: 72.83%
Branches in Coverage %: Yes
Coverage Strength: 30.27 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 5.83%

1 warning

⚠️ 1 benchmark is 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

⚡ 1 improved benchmark
❌ 2 regressed benchmarks
✅ 197 untouched benchmarks
⏩ 5 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ projection derive: write one NESTED field (reference) 2.1 ms 2.4 ms -12.18%
❌ arrow in template 10.9 ms 12.1 ms -9.75%
⚡ middle: remove at 500 + insert at 500 (1000 rows) 4.4 ms 4.2 ms +5.38%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/fix-4639-gaps (f47ee47) with next (7acde09)2

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. ↩

  2. No successful run was found on next (dd5f9d1) during the generation of this report, so 7acde09 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

…, trim (#4639 follow-up review)

- hiddenBy walks each node once per walk (seen set): a memo lattice below an
  error-boundary output costs its size, not its paths.
- A user effect reached through the output is never a reader (Vocabulary
  "Reader"): it holds nothing, as a plain read of the source.
- DEV LOADING_ON_OUTSIDE_HOLD: the output check applies only when the reader
  is that boundary's tree (a header render effect inside it still reports).
- s !== null -> s (trim).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
…#4639 follow-up review trim)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
@ryansolid
ryansolid merged commit f53d927 into next Nov 3, 2026
7 of 8 checks passed
ryansolid added a commit that referenced this pull request Nov 3, 2026
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