Skip to content

signals: a re-armed boundary owns content another hold staged (U-9) - #4059

Merged
ryansolid merged 4 commits into
nextfrom
claude/u9-rearm-boundary-scope
Oct 14, 2026
Merged

ryansolid merged 4 commits into
nextfrom
claude/u9-rearm-boundary-scope

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Requested by Ryan · project thread

Summary

Before: a Loading boundary re-armed by on could not show its fallback when the content under it read a hold that another change made. The content joined that hold, so no fallback showed and the boundary reported pending. In the S-19 shape (a held tree under a re-armed boundary) the boundary redrew forever.

After: the re-armed boundary owns that content, as D-14 and D-16 rule. The fallback shows with the re-arming change, and the content reveals at the hold's commit, which no longer waits for it. A re-arm whose own change joins that hold still shows no fallback, because the frame waits on the same data (the "a read made pending before the flush joins its hold" pins are unchanged). LOADING_ON_OUTSIDE_HOLD is now reported only for a flight the change itself started.

This is U-9, the last of the re-arm deferrals. Nine pins flip from it.fails to it:

  • the redraw loop;
  • the two re-armed held mounts;
  • the four "committed content under a re-armed boundary" shapes;
  • §3b (two outside-reader shapes);
  • §3c after the action (three shapes).

How:

  • arm records what an earlier hold staged under the boundary (heldUnder, scheduler.ts) and flips. The boundary's output leaves that hold (unhold), so the fallback publishes now.
  • A pass under a re-armed boundary that reads a hold is the boundary's, not the tick's. _fresh now answers for committed passes too (armedOver), at the held re-pass in recompute and in joinPass. A re-notified pending through such a pass does not join the flush, unless the change re-asked that flight.
  • The seam folds a re-arm back into the hold when its own flush joined that hold: a fallback staged into the very hold its readers wait on is dropped.
  • prune keeps the tree as a held reader while armed, which is the loop fix.

Size, measured locally against next @ a4b3391:

scenario minified brotli brotli now / cap
signals: core floor +0 B −2 B 7,467 / 7,480
signals: + createStore +0 B −7 B 14,936 / 14,940
app: render + one signal (simple-app floor) +0 B +23 B 9,977 / 9,980
app: CSR with Show/For/Loading/Errored/lazy +416 B +140 B 13,181 / 13,070 (over)
app: hydrating (no stores) +417 B +187 B 18,074 / 17,930 (over)
app: hydrating + every store primitive family +417 B +94 B 29,631 / 29,570 (over)
app: CSR, observe tier +416 B +139 B 14,778 / 14,640 (over)
app: CSR, observe + attribution +417 B +138 B 29,370 / 29,240 (over)

The floors are held at 0 B minified. The core's hook calls (X && X(...) to X?.(...)) and removalStagedBy were golfed to pay for the two new hook arguments. The five boundary apps are over their caps by 61–144 B brotli. The caps are left untouched until the maintainer rules.

How did you test this change?

  • pnpm --filter @solidjs/signals build:js && npx vitest run in packages/signals: 5229 passed, 2 expected fail (U-5, U-6), 2 skipped.
  • Internal reader counts in four committed-content pins: held 1/1, flight 3/2. latest of held re-armed content is the staged value (c1 1, A11), as for a fresh boundary's content.
  • Each piece was ablated against the full suite; the ones kept are the ones whose removal fails tests.
  • node packages/signals/scripts/rules-index.mjs --check: current.
  • node scripts/size/size.mjs (table above).

Size

Size-Exception: a re-armed boundary owns content another hold staged (D-14/D-16/U-9). The seven Loading-boundary app scenarios grow +416 to +418 B minified, so their caps are raised (hydrating with no stores is a floor-caps.json entry). The floors are 0 B minified. Accepted by the maintainer on 2026-10-14.

🤖 Generated with Claude Code

https://claude.ai/code/session_011JxYhsd8CeAgCp6h5DU8ic


Generated by Claude Code


Generated by Claude Code

A Loading boundary re-armed by `on` now owns what reads a hold under it
(D-14, D-16): the fallback shows with the re-arming change, the content
reveals at that hold's commit, and the hold does not wait for it. The
held tree under a re-armed boundary no longer redraws forever.

- `arm` records what an earlier hold staged under the boundary
  (`heldUnder`) and flips; the output leaves that hold (`unhold`).
- A pass under a re-armed boundary that reads a hold is the boundary's,
  not the tick's (`_fresh` answers for committed passes too), and a
  re-ask of an old flight through it does not join the flush.
- A re-arm whose own flush joins that hold folds back into it: the seam
  drops a fallback staged into the very hold its readers wait on.
- LOADING_ON_OUTSIDE_HOLD only for a flight the change itself started.
- Floors held at 0 B minified by golfing optional hook calls.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JxYhsd8CeAgCp6h5DU8ic
@changeset-bot

changeset-bot Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: af6186a

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

This PR includes changesets to release 13 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
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 14, 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.47 KB −2 B (−0.0%) 0 B −8 B 7.48 KB ✅
signals: + createStore 14.94 KB −7 B (−0.0%) 0 B −22 B 14.94 KB ✅
signals: + isPending/latest 9.75 KB −2 B (−0.0%) 0 B −8 B 9.77 KB ✅
app: render + one signal (the simple-app floor) 9.98 KB +23 B (+0.2%) 0 B −8 B 9.98 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.07 KB +187 B (+1.0%) +417 B 0 B 18.09 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.63 KB +94 B (+0.3%) +417 B 0 B 29.65 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.18 KB +140 B (+1.1%) +416 B 0 B 13.20 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.78 KB +139 B (+0.9%) +416 B 0 B 14.79 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.37 KB +138 B (+0.5%) +417 B 0 B 29.38 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.17 KB +6 B (+0.1%) 0 B −8 B 10.17 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.90 KB +102 B (+0.4%) +417 B 0 B 25.92 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.77 KB +71 B (+0.2%) +418 B 0 B 31.78 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.49 KB 0 B 0 B 0 B 11.49 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 34.35 KB +158 B (+0.5%) +417 B −985 B 34.33 KB ⚠️ over by 14 B, 1005 B minified headroom assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 9.00 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 38.11 KB +145 B (+0.4%) +417 B −1054 B 38.15 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 9.01 KB, wire.js 0.93 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.20 KB +204 B (+0.6%) +417 B −2075 B 35.66 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.98 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.81 KB +126 B (+0.3%) +417 B −2068 B 41.21 KB ✅ eager (counted): web.js 21.68 KB; assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.23 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.97 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.67 KB +71 B (+0.2%) +417 B −991 B 41.79 KB ✅ assets.js 0.78 KB, bind.js 1.85 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, server.js 1.02 KB, serverForms.js 3.58 KB, trace.js 9.01 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.47 KB +142 B (+0.3%) +411 B −991 B 47.49 KB ✅ eager (counted): client.js 27.89 KB; assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, serverForms.js 3.30 KB, trace.js 8.97 KB, wire.js 0.94 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.53 KB 0 B 0 B 0 B 20.55 KB ✅

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

  • page: base server components (hydrating + dynamic + frames + sf reference): over brotli cap by 14 B; minified 105,719 B vs 106,704 B recorded with the cap (−985 B) — 1005 B of the 20 B minified allowance left; +417 B minified over this PR's base

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

Copy link
Copy Markdown
Member Author

The check job fails at the size gate (node gate.mjs size-head.json size-base.json). This PR's own bytes cause it: seven Loading-boundary app scenarios go 61–144 B brotli over their caps (CSR, hydrating, hydrating+stores, observe, observe+attribution, compiled CSR, compiled hydrating). The hello-world floors stay at 0 B minified.

I haven't fixed this because the choice is a maintainer's call: raise those caps, golf further, or drop U-9 and file it as an issue. I've asked in the project thread and am leaving the caps alone until that's answered. The PR stays a draft and watched.


Generated by Claude Code

@coveralls

coveralls commented Oct 14, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38588969410

Coverage remained the same at 77.515%

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: 1326
Covered Lines: 1084
Line Coverage: 81.75%
Relevant Branches: 1040
Covered Branches: 750
Branch Coverage: 72.12%
Branches in Coverage %: Yes
Coverage Strength: 27.49 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 2 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 195 untouched benchmarks


Comparing claude/u9-rearm-boundary-scope (af6186a) with next (f048691)

Open in CodSpeed

ryansolid commented Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

CodSpeed's first run flagged one benchmark: mount-rows "memo + sync render effect only (reference)", down 13.6%, with a runtime-environment warning. The only hot-path change here sits inside the held re-pass branch in recompute, and mount-rows never enters it.

Update: I re-ran the same commit (c2513e3) and all 195 benchmarks came back untouched, so the first reading was runner noise.


Generated by Claude Code

Size-Exception: a re-armed boundary owns content another hold staged
(D-14/D-16/U-9). +416..418 B minified on the seven Loading-boundary app
scenarios; the floors are 0 B minified. Accepted by the maintainer
2026-10-14.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JxYhsd8CeAgCp6h5DU8ic
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JxYhsd8CeAgCp6h5DU8ic
@ryansolid
ryansolid marked this pull request as ready for review October 14, 2026 15:46
@ryansolid
ryansolid merged commit 2da0f8e into next Oct 14, 2026
8 checks passed
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