Skip to content

fix(signals): a pass joined only through born-held nodes is not re-derived by a lane entry (#3969) - #3980

Open
ryansolid wants to merge 1 commit into
nextfrom
fix/born-held-lane-loop-3969
Open

ryansolid wants to merge 1 commit into
nextfrom
fix/born-held-lane-loop-3969

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Fixes #3969

Why only in the browser: the issue reproduces under the Vite dev server, not in a production build or under jsdom, because the ingredient is hot reload. solid-refresh runs every component inside a transparent memo (createProxy in solid-js/refresh). Built the same app without HMR in headless Chromium: no loop; under vite dev: two guard trips per Save, as reported. hot: false makes them go away. The new jsdom spec wraps components the way solid-refresh does and reproduces it deterministically (Spinner wrapped, App wrapped, or both; unwrapped passes on next too).

Mechanism: Save's setSelected(false) swaps the <Show> to its fallback, held with the refetch. The fallback mounts Spinner inside its HMR memo, which is born held into the parked change. The insert effect for the <Show> reads that memo (joining the hold through it, REACTIVE_JOINED) and then isPending(project)'s verdict lane. enterLane treated the pass as lane work that read a held write and queued it to re-derive on the committed world, but a born-held node has no committed value: the re-run read the same staging. The lane held; the effect re-ran on the screen and registered for the lane's reveal; the lane showed and re-ran it; repeat until the guard.

Fix: stagedRead already kept reads of a node born staged out of stagedReaders ("a re-run would read the same staging and re-queue every round"); it now doesn't mark them as staged reads at all, and enterLane's repair needs a staged read beside the join. A pass whose held reads were all of born-held nodes is left alone; one that read a held write with a committed value is re-derived as before.

No public API change. Size: core floor −1 B minified, isPending/latest +10 B minified / −1 B brotli (local). Tests: packages/web/test/hmr-spinner-loop-3969.spec.tsx (four wrappings; also pins that the screen keeps selected while the swap is held). Signals suite: 5155 passed, 22 expected fail. Web suite: unchanged (the 30 failures in element-claims/select-async-value also fail on next locally — stale native compiler build here).

I couldn't reduce it to a signals-only test with createMemo/createRenderEffect stand-ins for Show and insert; the web spec is the pin.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LVm3wseHt9wRTY8LfPiHmQ


Generated by Claude Code

…rived by a lane entry (#3969)

A render effect read a memo born held into a parked change and then a
verdict lane's node (isPending). enterLane treated the pass as lane work
that read a held write (REACTIVE_JOINED) and queued it to re-derive on the
committed world, but a born-held node has none: the re-run read the same
staging. The lane held, the effect re-ran on the screen, the lane showed
and re-ran it, every round, until the loop guard. Under the dev server
every component runs in a transparent memo (solid-refresh), which is the
born-held node.

A read of a node born staged no longer marks REACTIVE_STAGED_READ
(stagedRead already kept it out of stagedReaders), and enterLane's repair
needs a staged read beside the join.

Fixes #3969

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

changeset-bot Bot commented Oct 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6262b38

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

This PR includes changesets to release 12 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
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

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.44 KB −4 B (−0.1%) −1 B +31 B 7.45 KB ✅
signals: + createStore 14.71 KB +60 B (+0.4%) −1 B +9 B 14.71 KB ✅
signals: + isPending/latest 9.70 KB +3 B (+0.0%) +10 B −103 B 9.73 KB ✅
app: render + one signal (the simple-app floor) 9.91 KB 0 B −1 B −39 B 9.94 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.74 KB −2 B (−0.0%) −1 B −650 B 17.93 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.07 KB −13 B (−0.0%) +10 B −580 B 29.27 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.92 KB −43 B (−0.3%) −1 B −47 B 12.98 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.56 KB −7 B (−0.0%) −1 B −31 B 14.56 KB ⚠️ over by 5 B, 51 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.86 KB −35 B (−0.1%) −1 B −1 B 28.91 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.10 KB −15 B (−0.1%) −1 B −57 B 10.13 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.40 KB −2 B (−0.0%) −1 B −34 B 25.46 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.16 KB −11 B (−0.0%) −1 B −656 B 31.39 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.43 KB 0 B 0 B 0 B 11.43 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.88 KB −9 B (−0.0%) −1 B −2311 B 34.33 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 8.78 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.59 KB −33 B (−0.1%) +10 B −2443 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 8.77 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) 34.75 KB +62 B (+0.2%) −1 B −3476 B 35.66 KB ✅ assets.js 0.78 KB, bind.js 1.82 KB, decode.js 6.23 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.74 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.27 KB −46 B (−0.1%) +10 B −3549 B 41.21 KB ✅ eager (counted): web.js 21.28 KB; assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.72 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.20 KB −43 B (−0.1%) −1 B −2317 B 41.79 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.80 KB, server.js 1.02 KB, serverForms.js 3.59 KB, trace.js 8.79 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.02 KB −40 B (−0.1%) +10 B −2389 B 47.49 KB ✅ eager (counted): client.js 27.50 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.81 KB, server.js 1.02 KB, serverForms.js 3.30 KB, trace.js 8.75 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.40 KB 0 B 0 B +4 B 20.42 KB ✅

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

  • app: CSR, observe tier (same app on the observe artifacts): over brotli cap by 5 B; minified 41,274 B vs 41,305 B recorded with the cap (−31 B) — 51 B of the 20 B minified allowance left; −1 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).

@codspeed

codspeed Bot commented Oct 11, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

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

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 1 regressed benchmark
✅ 192 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ createStore setter: delete + set one root key (#3044 overlay) 1.8 ms 2.1 ms -14.28%
⚡ memo + sync render effect only (reference) 32.2 ms 27.9 ms +15.48%
⚡ projection derive: write one NESTED field (reference) 2.4 ms 2.1 ms +14.28%

Tip

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


Comparing fix/born-held-lane-loop-3969 (6262b38) with next (e9d549f)1

Open in CodSpeed

Footnotes

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

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.

2 participants