Skip to content

fix(signals): a derived store's propagated error reaches its readers once - #4463

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

ryansolid merged 4 commits into
nextfrom
claude/project-thread-x1ore2-family-status-error

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Before: a sync derived store (createStore(fn) or createOptimisticStore(fn)) over a sync memo handed each new error to its reader twice in one flush, so an effect's error arm ran twice per error. The derive learns the error twice: once by propagation (core propagateStatus → the family's familyStatus) and once from its own pass re-run against it. #4457 added a same-error skip to errorFamily, but the propagation call passed only the status, so the skip never applied there. Next behaves the same for both store kinds, so this predates #4457.

After: each error reaches the reader once. A memo throwing E1, then E2, then recovering logs ERR E1, ERR E2, v 0, not each error twice.

How: propagateStatus passes the error along (fam?.st(status, error)), and familyStatus hands it to errorFamily, which skips a reader already errored with that same error. The stale comment that said there was no error to compare is updated.

Size vs next @ cddaa76, measured locally (linux): +2 B minified on every floor and +6 B where stores ship. Hello world is 9,974 B brotli, under 10,000. Brotli moves −88 to +53 B from prop mangling. No cap changes.

Tests: a new case in packages/signals/tests/derived-store-error-delivered-once.test.ts for both store kinds. It fails on next, with each error logged twice, and passes with the fix. In the signals suite, 5,982 tests passed. The only failures were the 35 dist-artifacts tests, which need a dist build that worktree didn't have.

Follow-up to the #4457 review.

🤖 Generated with Claude Code

https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2

…once

propagateStatus handed the derive's family a status without its error, so
errorFamily's same-error skip could not apply and a sync derive over a sync
memo re-ran its reader's error arm twice in one flush (#4457 review).

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: 10663ec

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.42 KB +1 B (+0.0%) +2 B +15 B 7.43 KB ✅
signals: + createStore 14.96 KB +7 B (+0.0%) +6 B +6 B 14.97 KB ✅
signals: + isPending/latest 11.11 KB −3 B (−0.0%) +2 B +2 B 11.12 KB ✅
signals: + until/refresh (truth readers, no optimism) 8.05 KB +9 B (+0.1%) +2 B +2 B 8.06 KB ✅
app: render + one signal (the simple-app floor) 9.78 KB +2 B (+0.0%) +2 B +15 B 9.78 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.73 KB −11 B (−0.1%) +2 B +15 B 18.74 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 32.37 KB −97 B (−0.3%) +6 B +6 B 32.48 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.43 KB −21 B (−0.2%) +2 B +2 B 13.47 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.19 KB +10 B (+0.1%) +2 B +2 B 15.20 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.76 KB +8 B (+0.0%) +2 B +2 B 29.78 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.97 KB +11 B (+0.1%) +2 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.33 KB +28 B (+0.1%) +6 B +6 B 26.33 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.52 KB +8 B (+0.0%) +6 B 0 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.67 KB −88 B (−0.3%) +2 B +2 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.03 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 38.76 KB −79 B (−0.2%) +2 B +2 B 38.85 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.03 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.70 KB +3 B (+0.0%) +2 B +2 B 35.72 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.01 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 42.69 KB −26 B (−0.1%) +2 B +2 B 42.73 KB ✅ eager (counted): web.js 22.75 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.05 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.23 KB +53 B (+0.1%) +2 B 0 B 41.25 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.03 KB, serverForms.js 3.57 KB, trace.js 9.03 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 48.43 KB +35 B (+0.1%) +2 B +2 B 48.44 KB ✅ eager (counted): client.js 27.94 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.03 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 ✅

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 41073676164

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.9 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 10.46%

1 warning

⚠️ 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 improved benchmark
✅ 199 untouched benchmarks
⏩ 5 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ arrow in template 12.3 ms 11.1 ms +10.46%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/project-thread-x1ore2-family-status-error (10663ec) with next (131ceb8)

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

I don't think CodSpeed's −13.5% on projection-root-write "projection derive: write one NESTED field (reference)" comes from this PR, even though the PR touches projection.ts. #4455 and #4456 are on the same base (next cddaa76) and neither touches stores, yet both show this bench at −13.5% and −13.2%. So the base run is the outlier. This bench file's single-run cases swing ±13–15% with module byte size alone, because a V8 lazy sweep after the pre-run gc() either lands in the measured run or misses it.

The change here only adds an error argument to familyStatus, and it runs only when a source's status is pending or errored. This bench does a sync nested write with no async or error status. I'm treating it as noise.


Generated by Claude Code

…rror

Raise the base + router and compiled hydrating caps (brotli layout from
+2 B minified) and give StoreFamily.st its error parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
@ryansolid
ryansolid merged commit e49b0c7 into next Oct 30, 2026
9 checks passed
ryansolid pushed a commit that referenced this pull request Oct 30, 2026
…y a re-run

The derive pending by propagation (familyStatus -> wakeFamily) was marked
blocked so its source's settle re-ran it. For an async derive that re-run
is a new flight: when the source's rejection was staged, the walk in
stageError re-ran the derive in the same flush, ahead of the commit that
publishes the error to it. The new flight woke the family again, so a
reader beside isPending showed a second pending frame before the error
(#4463's error-once test, D-41).

The settle walk now settles the family instead (familyStatus with no
status -> settleFamily) when it retires a derive's pending without a pass
to re-run, as a memo's readers settle through its dependents (F-9, A9).
The error still reaches the derive at the commit (F-12, D-41), and the
re-ask that nets to the committed question ends isPending (F-9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
ryansolid added a commit that referenced this pull request Oct 30, 2026
…y a re-run (#4493)

* fix(signals): a derived store's derive re-runs when a source it pends on settles

`createStore(fn)` over an async memo. The derive went pending on the memo's
flight by propagation (`familyStatus` -> `wakeFamily`), and the store's
readers were parked on the derive itself. When the flight then settled
without the derive re-running (a rejection staged in its transaction, or a
re-ask that returned to the committed question), the settle walk retired
the memo from the derive but never reached the readers parked on it:
`isPending` stayed true forever (F-9), and the staged rejection's
transaction never landed, so the error never showed (F-12, D-41). The wake
now marks the derive blocked, as a pass that threw is, so the settle
re-runs it and its commit settles the family.

Found by the async-gen rerun on rc.20 (pair E3, rows #1 and #9).

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

* chore(size): record the derived-store settle fix's sizes over next @ 7135a77

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

* fix(signals): a derived store's readers settle with its derive, not by a re-run

The derive pending by propagation (familyStatus -> wakeFamily) was marked
blocked so its source's settle re-ran it. For an async derive that re-run
is a new flight: when the source's rejection was staged, the walk in
stageError re-ran the derive in the same flush, ahead of the commit that
publishes the error to it. The new flight woke the family again, so a
reader beside isPending showed a second pending frame before the error
(#4463's error-once test, D-41).

The settle walk now settles the family instead (familyStatus with no
status -> settleFamily) when it retires a derive's pending without a pass
to re-run, as a memo's readers settle through its dependents (F-9, A9).
The error still reaches the derive at the commit (F-12, D-41), and the
re-ask that nets to the committed question ends isPending (F-9).

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

* chore(size): record the derived-store settle fix's sizes over next @ d0fc285

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

* fix(signals): settle a derive's family only in a store build

On next @ 643a7a7 the ungated `else (node as any).fam?.st()` laid out
+20 B brotli on hello world (9,993 B, over its 9.98 KB cap) from -3 B
minified. A derive's family exists only when the store module is loaded,
which is what sets `storeParkHook`, so the arm is gated on it and folds
away in store-free bundles: every one is 0 B vs next, hello world
included (9,973 B). The settle walk's `_blocked` clear is back to next's
form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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