Skip to content

fix(signals): a held staging beats an inherited verdict posture, and a probing reader no longer blocks the landing (#4544) - #4625

Merged
ryansolid merged 1 commit into
nextfrom
fix/4544-held-staging-vs-inherited
Nov 2, 2026
Merged

ryansolid merged 1 commit into
nextfrom
fix/4544-held-staging-vs-inherited

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Summary

Before: in #4544's app, a branch the hold opened could read the signed-out session beside a signed-in condition. That happened when its memo inherited a verdict posture by reading a memo that calls isPending(plan) or latest(plan). With a never-resolving signed-out fetch, the hold never landed. The same hang showed when params itself called isPending(plan) or isPending(session).

After: all four forms land on 1 rows, whether the writes go in one flush() or are split across several.

  • Memo isPending and memo latest: the branch reads the hold's own write.
  • Inline isPending(plan) and same-node isPending(session): the pass is a verdict reader and reads the screen while held. The write's hold still lands, and the pass re-derives at the landing.

This replaces #4612 and follows the precedence settled for #4544:

  1. F-6: a probe in this pass's own body makes the pass a verdict reader that reads the screen, over a staging it holds.
  2. D-25 / isPending/latest on a superseded transition depend on whether the parked async memo also reads them #3884: a pass that didn't probe this time reads plainly when it is the hold's own work.
  3. 2.0.0-rc.20: a memo that calls isPending()/latest() on an unrelated signal makes a branch opened by a held update read the pre-hold world; <Loading> never settles #4544: a staging you already hold beats an inherited posture, meaning you read a verdict memo but didn't probe yourself.

How:

  • Inherited posture: holdStagingHook, installed by the verdict engine and called only behind observeFlightHook, answers O(1) whether an inherited-posture pass is reading a pending sync write of its hold. If so, frameRead doesn't serve it the committed value. In one flush, stagedScreen serves it the flush's own write and takes it out of the verdict lane.
  • Own probe: CONFIG_OWN_PROBE is rebuilt every pass from REACTIVE_WINDOW, next to CONFIG_VERDICT.
  • No sticky marks: a probe's mark is scoped to the hold (_x._askedIn), so it ends when that hold lands or dissolves.
  • Liveness: a render effect born into the hold that is pending on a fetch it asked for, after reading a settled verdict reader, is marked CONFIG_AHEAD (F-6). blockedBy skips it when judging a transaction, but not a lane (S-29), so the hold lands and the reader re-derives (A15).
  • Docs: D-25 records line 3 and the pending-effect carve-out.

How did you test this change?

Size vs next: hello world, every floor and every non-verdict row are 0 B. The four verdict rows grow:

Row brotli minified
signals: + isPending/latest +145 +467
page: live server components +153 +469
page: compiled live server components +122 +492
page: live + router +194 +486

Closes #4544


Generated by Claude Code

…#4544)

A pass whose verdict posture is only inherited (it read a memo that calls
isPending/latest, and neither it nor its node's last pass opened a window of
its own) is the hold's work where it reads the hold's own sync write: it is
served that staging and joins the hold. The branch a hold opens then reads
the hold's world and the hold lands, in one flush() or several (F-10).
Before the flush parks, such a read leaves the verdict lane an earlier read
of the verdict memo seated the pass in.

A staging a window asked of under the hold that holds it (or in this flush)
still reads as the screen, so `[latest(x), x()]` through a memo stays atomic
(A10, D-26). The mark (`_askedIn`) names that hold and expires with it:
nothing sticky. CONFIG_OWN_PROBE is renewed by every pass from
REACTIVE_WINDOW.

A pass that probes in its own body stays the hold's verdict reader (F-6).
When a render effect born into the hold reads such a reader's settled value
and goes pending on what that display-ahead value asks for, the pending no
longer holds the hold (CONFIG_AHEAD, F-6): the hold lands and the reader
re-derives at the landing (A15). A lane's reveal still waits (S-29).

Everything is gated on the verdict engine's hooks.

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

changeset-bot Bot commented Nov 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 95b153e

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 2, 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.00 KB 0 B 0 B 0 B 15.01 KB ✅
signals: + isPending/latest 11.86 KB +145 B (+1.2%) +467 B +467 B 11.73 KB ❌ over by 129 B
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 18.91 KB 0 B 0 B 0 B 18.93 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 32.84 KB 0 B 0 B 0 B 32.90 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.55 KB 0 B 0 B 0 B 13.57 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.32 KB 0 B 0 B 0 B 15.33 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.91 KB 0 B 0 B 0 B 29.93 KB ✅ 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.40 KB 0 B 0 B 0 B 26.41 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.65 KB 0 B 0 B 0 B 32.66 KB ✅ 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) 33.89 KB 0 B 0 B 0 B 33.91 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.21 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 39.74 KB +153 B (+0.4%) +469 B +469 B 39.60 KB ❌ over by 143 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.18 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.83 KB 0 B 0 B 0 B 35.84 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.18 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 43.74 KB +122 B (+0.3%) +492 B +492 B 43.63 KB ❌ over by 106 B eager (counted): web.js 23.26 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.41 KB 0 B 0 B 0 B 41.43 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.58 KB, trace.js 9.14 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 49.50 KB +194 B (+0.4%) +486 B +486 B 49.32 KB ❌ over by 178 B eager (counted): client.js 28.50 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.16 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.85 KB 0 B 0 B 0 B 22.87 KB ✅

❌ Fails the gate

  • signals: + isPending/latest: over brotli cap by 129 B; minified 32,645 B vs 32,178 B recorded with the cap (+467 B) — 447 B past the 20 B minified allowance; +467 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 143 B; minified 119,699 B vs 119,230 B recorded with the cap (+469 B) — 449 B past the 20 B minified allowance; +469 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 106 B; minified 127,958 B vs 127,466 B recorded with the cap (+492 B) — 472 B past the 20 B minified allowance; +492 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 178 B; minified 145,519 B vs 145,033 B recorded with the cap (+486 B) — 466 B past the 20 B minified allowance; +486 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).

Copy link
Copy Markdown
Member Author

The red check is the size gate. It flags the four lane/verdict rows (isPending/latest, live server components, compiled live server components, live + router) at +467 to +492 B minified. Hello world and every floor are 0 B. That growth is waiting on a size call. Fix PRs don't edit scripts/size, so the batch ledger re-record after merge covers these rows.


Generated by Claude Code

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 41496607670

Coverage remained the same at 78.205%

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: 1369
Covered Lines: 1131
Line Coverage: 82.62%
Relevant Branches: 1104
Covered Branches: 803
Branch Coverage: 72.74%
Branches in Coverage %: Yes
Coverage Strength: 29.92 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Nov 2, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 21.46%

2 warnings

⚠️ 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

⚠️ 1 benchmark with a significant change ran on different hardware

Its base and head results were measured on machines with a different CPU, core count or memory, so the change might not come from the code. Run base and head on the same runner type.

Learn more about Macro Runners

See affected benchmarks

❌ 1 regressed benchmark
✅ 199 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.7 ms -21.46%

Tip

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


Comparing fix/4544-held-staging-vs-inherited (95b153e) with next (b244261)

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

Copy link
Copy Markdown
Member Author

CodSpeed flags one benchmark, projection-root-write "write one NESTED field (reference)" at −21%. It's known noise: this store bench flips ±13–27% with module byte size alone, and CodSpeed notes it ran on different hardware for base and head. This PR doesn't touch store code, and the other 199 benchmarks are unchanged.


Generated by Claude Code

@ryansolid
ryansolid merged commit 343f007 into next Nov 2, 2026
7 of 9 checks passed
ryansolid added a commit that referenced this pull request Nov 2, 2026
Numbers from next's push size job 125476580572 at 343f007. Hello world
stays under its 9.98 KB cap.


Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE

Co-authored-by: Claude <noreply@anthropic.com>
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