Skip to content

fix(signals): a reader mounted mid-flight reads a derived store like its memo (#4491) - #4500

Merged
ryansolid merged 4 commits into
nextfrom
claude/project-thread-x1ore2-4491
Oct 30, 2026
Merged

ryansolid merged 4 commits into
nextfrom
claude/project-thread-x1ore2-4491

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Before: take createStore(d => { d.v = m1() }) over an async memo whose input reads latest(). A reader that mounted while m1 was re-asked showed the old question's answer. On the same mount, a reader of m1() is held (F-2).

After: the store reader is held like the memo's reader, and shows the new answer at the landing (D-30: a derived store reads like its memo). This holds for key reads, Object.keys and JSON.stringify, with or without a plain write beside the flight.

Cause: the derive is a verdict reader through what it reads (D-25 / F-6). At a re-ask it is served m1's committed value and settles over it, so its leaves read as final.

How:

  • pullFamily: a tracked reader of a settled verdict-reader derive, with no window of its own, reads the flights among the derive's deps, as the memo's reader would. So it is held, and the derive is that hold's work until it answers.
  • runProjectionComputed: a pass with fam.woke or fam.w set clears CONFIG_VERDICT for that pass and reads plainly, as a pending node's pass does (isPending/latest on a superseded transition depend on whether the parked async memo also reads them #3884). Its landing is then the answer the held reader shows. Note this is wider than the [2.0 / next] A reader that mounts while an unread derived store's flight is up shows the old question's answer (D-30, F-2) #4491 shape: it applies to any pass where the derive's pending sits on its leaves' readers (fam.woke), not only after the new pullFamily read. The signals and web suites and the async-app generator are unchanged by it.
  • The derive's posture is never written outside its pass. The read marks the family with a non-sticky flag that the derive's next pass consumes, and a read that finds no flight clears it. A later flight then runs as for a store read from the start, after a changed landing and after an equal one.
  • Both halves sit behind observeFlightHook, because a verdict reader exists only with the verdict engine. Store bundles without isPending/latest pay 0 B.

Rules: D-30, F-2, F-6, D-25.

Not covered here: pullFamily only looks at the derive's direct deps, so a memo between the derive and the flight (d.v = m2() with m2 = memo(() => m1() * 10)) still shows the old answer. That shape is also wrong for a plain memo reader (createMemo(() => m1()) mounted mid-flight shows r:2, r:4 instead of r:4), so it's a core bug in the memo form, filed and fixed separately.

Tests: tests/unread-derive-midflight-mount-4491.test.ts (11 cases, pinned against the memo form). Signals 6091 passed / 17 expected fail; web 1579 passed / 7 expected fail.

Size (client, vs next 643a7a7; no ledger edits in this PR):

Scenario Brotli Minified
page: base server components 33,735 → 33,721 (−14) +16
page: live server components 38,969 → 39,004 (+35) +14
page: compiled base server components 35,662 → 35,723 (+61) +21
page: compiled live server components 42,931 → 42,949 (+18) +6
page: base + router 41,132 → 41,159 (+27) +21
page: live + router 48,569 → 48,522 (−47) +12

Every store bundle and every other scenario is unchanged; hello world stays at 9,973 B.

Closes #4491


Generated by Claude Code

…its memo (#4491)

`createStore(d => { d.v = m1() })` over an async memo whose input reads
`latest()` has a derive that is a verdict reader through what it reads
(D-25 / F-6). At a re-ask it is served `m1`'s committed value and settles
over it, so its leaves read as final: a reader that mounted mid-flight
showed the old question's answer before the new one (in the mounting
flush once #4479 skips the unread derive, at the landing before it),
where a reader of `m1()` is held (F-2). D-30: a derived store reads like
its memo.

`pullFamily`: a tracked reader of a settled verdict-reader derive with no
window of its own reads the flights among the derive's deps, as the
memo's reader would, so it is held; the derive is that hold's work until
it answers. `runProjectionComputed`: a derive whose pending is on its
leaves' readers (`fam.woke`) reads plainly, as a pending node's pass does
(#3884), so its landing is the answer the held reader shows. Store-only:
no frozen floor moves.

The #4479 review's probe forms (key read, Object.keys, JSON) are pinned
against the memo form: mounted mid-flight with and without a plain write
beside the flight, on screen at the re-ask, and mounted after the landing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…is never written outside its pass

Review of a78d91a: a verdict reader exists only with the verdict engine,
so both halves of the fix now sit behind `observeFlightHook`. Store bundles
without isPending/latest fold them away (createStore, compiled CSR and
compiled hydrating at +0 B vs next), so the cap raises recorded for
createStore and compiled CSR are dropped.

The pull no longer clears the derive's CONFIG_VERDICT. That clear was
sticky: when the flight's landing equalled the screen, the derive did not
re-run and stayed a non-verdict reader into its next flight. The read now
marks the family (`fam.w`), the derive's next pass consumes the mark and
reads plainly, and a read that finds no flight up clears it. The posture
stays the passes' own (core `recompute`). New tests: after the held mount,
a later flight runs as for a store read from the start and as the memo
form, after a changed landing and after an equal one. The equal-landing
case fails on a78d91a (verdict=false after the landing).

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

changeset-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6d71df1

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 30, 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.41 KB 0 B 0 B +15 B 7.43 KB ✅
signals: + createStore 14.96 KB 0 B 0 B +6 B 14.97 KB ✅
signals: + isPending/latest 11.27 KB 0 B 0 B 0 B 11.29 KB ✅
signals: + until/refresh (truth readers, no optimism) 8.05 KB 0 B 0 B +2 B 8.06 KB ✅
app: render + one signal (the simple-app floor) 9.79 KB 0 B 0 B +15 B 9.78 KB ⚠️ over by 8 B, 5 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.72 KB 0 B 0 B 0 B 18.74 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 32.47 KB 0 B 0 B 0 B 32.48 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.45 KB 0 B 0 B +9 B 13.47 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.19 KB 0 B 0 B +9 B 15.20 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.76 KB 0 B 0 B +9 B 29.78 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 +2 B 9.98 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.27 KB 0 B 0 B +13 B 26.33 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.48 KB 0 B 0 B +7 B 32.54 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.72 KB −14 B (−0.0%) +16 B +16 B 33.77 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.19 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 39.00 KB +35 B (+0.1%) +14 B +14 B 38.98 KB ⚠️ over by 24 B, 6 B minified headroom assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.17 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.72 KB +61 B (+0.2%) +21 B +21 B 35.72 KB ❌ over by 3 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.13 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 42.95 KB +18 B (+0.0%) +6 B +6 B 42.95 KB ✅ eager (counted): web.js 22.93 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.19 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.16 KB +27 B (+0.1%) +21 B +21 B 41.25 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.13 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 48.52 KB −47 B (−0.1%) +12 B +12 B 48.58 KB ✅ eager (counted): client.js 28.04 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.14 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.79 KB 0 B 0 B 0 B 22.80 KB ✅

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

  • app: render + one signal (the simple-app floor): over brotli cap by 8 B; minified 26,754 B vs 26,739 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: live server components (base + live/GET + action + isPending/latest): over brotli cap by 24 B; minified 117,872 B vs 117,858 B recorded with the cap (+14 B) — 6 B of the 20 B minified allowance left; +14 B minified over this PR's base

❌ Fails the gate

  • page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread): over brotli cap by 3 B; minified 108,069 B vs 108,048 B recorded with the cap (+21 B) — 1 B past the 20 B minified allowance; +21 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 30, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 41118328857

Coverage remained the same at 78.1%

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: 1362
Covered Lines: 1125
Line Coverage: 82.6%
Relevant Branches: 1090
Covered Branches: 790
Branch Coverage: 72.48%
Branches in Coverage %: Yes
Coverage Strength: 29.82 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

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

✅ 200 untouched benchmarks
⏩ 5 skipped benchmarks1


Comparing claude/project-thread-x1ore2-4491 (6d71df1) with next (bb7f9b5)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 (14a8190) during the generation of this report, so bb7f9b5 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

ryansolid commented Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

Size gate (check), updated after #4501 re-recorded the ledger and this branch merged it (6d71df1): the one row still failing is this PR's own growth, compiled base server components (+61 B brotli, +21 B minified, 1 B past the minified allowance), as listed in the body. Live server components is over its brotli cap within its allowance. Under the current batch process, fix PRs carry no ledger edits, and this row is re-recorded in one ledger PR after the batch merges. Store-free bundles are 0 B.


Generated by Claude Code

ryansolid commented Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

CodSpeed's two flags on this head are both reference benches that move without code changes on their path. projection derive: write one NESTED field (reference) swings ±13–15% with module byte size alone: a V8 lazy sweep after the pre-run gc() either lands in the single measured run or misses it, and a 200 B comment on next reproduced the jump earlier. memo + sync render effect only (reference) swings with runner type, which CodSpeed also flags here as different hardware. On that projection bench, this PR adds only a null check of observeFlightHook and one _config flag test, on the derive's untracked read path. The derive there reads no isPending/latest, so it is not a verdict reader and the new loop never runs.


Generated by Claude Code

@ryansolid
ryansolid merged commit 64fedc7 into next Oct 30, 2026
8 of 9 checks passed
ryansolid added a commit that referenced this pull request Oct 30, 2026
…4499, #4500, #4493, #4488, #4498) (#4507)

next @ b5077ea is over six caps after the batch merged back to back (each
fix PR carried no ledger edits, per the batch process). Re-records,
measured over next @ b5077ea (linux):

- signals: + createStore: 14.97 -> 14.99 KB, minified 43,924 -> 43,956
- signals: + isPending/latest: 11.29 -> 11.35 KB, minified 31,101 -> 31,245
- app: compiled CSR: 26.33 -> 26.36 KB, minified 80,035 -> 80,074
- page: live server components (frozen floor): 38.98 -> 39.10 KB,
  minified 117,858 -> 118,033
- page: compiled live server components: 42.95 -> 43.01 KB,
  minified 126,018 -> 126,181
- page: live + router: 48.58 -> 48.65 KB, minified 143,269 -> 143,439

and the recorded minified only (caps unchanged) where the batch drifted
more than 20 B: hydrating + every store (98,535 -> 98,561), compiled
hydrating (100,803 -> 100,836), base server components (frozen,
101,675 -> 101,706), compiled base server components (108,048 ->
108,081), base + router (125,503 -> 125,539).

Hello world is 9,971 B (cap 9.98 KB). The build leaves the pinned
mangle-props.json unchanged.


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

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