Repository navigation
fix(signals): a derived store's readers settle with its derive, not by a re-run - #4493
Conversation
… 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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…135a77 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
# Conflicts: # scripts/size/scenarios.js
…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
…0fc285 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 39c2377 The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 packages
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 |
Size (brotli, eager entry chunk)
|
Coverage Report for CI Build 41118336775Coverage remained the same at 78.1%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will degrade performance by 12.41%1 warning
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
…tch PR) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…-x1ore2-store-refetch-error
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
|
Two things on this PR's checks: Size gate ( CodSpeed. Its flags are three reference benches that move without code on their path:
This PR's settle arm runs only when a settle walk fully settles a node, not on a write pass. Generated by Claude Code |
…-x1ore2-store-refetch-error
…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>
|
Confirmed this failure in an app on rc.19, with a standalone reproduction and a tested rc.19 runtime backport: https://gist.github.com/Peyton-Spencer/edae55128c740f01357d116844e3f7bd The reproduction refetches an async memo, resolves it to the same value, and checks a memo that reads an unchanged projection leaf. After draining microtasks and scheduler flushes, Results on macOS / Node 26.8.2:
For The gist includes Your merged source fix passes this reproduction on current |
Before: take a derived store
createStore(d => { d.v = m0() })over an async memom0. The derive goes pending onm0's flight, and the flight then settles without the derive re-running. That happens when a rejection is staged in the flight's transaction, or when a re-ask returns to the committed question. The settle walk retiredm0from the derive, but the store's readers wait on the derive itself (wakeFamily), and nothing retired that. SoisPendingof the store stayedtrueforever (F-9), and the staged rejection's transaction never landed, so the error never showed (F-12). The memo form,m0()read directly, settles in both cases.After: the store reads like the memo. A re-ask that nets to the committed question ends
isPending, and a refetch's rejection reaches the store's readers, once.How:
settlePendingSource(core/async.ts), when the walk fully settles a node that has no blocked pass to re-run, it settles a derived store's family (fam?.st()). That is how a memo's readers settle through its dependents (F-9, A9). It runs only when a settle walk fully settles a node, so there is no per-pass cost.storeParkHook, which only the store module sets, so store-free bundles fold it away and pay 0 B.familyStatus(store/projection.ts), no status means "settle the readers" (settleFamily). The_blockedmarkwakeFamilyused to set is gone.derived-store-error-delivered-once.test.ts) holds.Size (client, vs next 643a7a7, brotli / minified; no ledger edits in this PR):
signals: + createStoreapp: hydrating + every store primitive familyapp: compiled CSRapp: compiled hydratingpage: base server componentspage: live server componentspage: compiled base server componentspage: compiled live server componentspage: base + routerpage: live + routerEvery store-free scenario is unchanged, hello world included (9,973 B). The ungated form put hello world at 9,993 B, over its 9.98 KB cap, from −3 B minified.
How did you test this change?
packages/signals/tests/derived-store-derive-settle.test.tsandpackages/web/test/async-gen/store-derive-settle-e3.spec.ts.Found by the async-gen rerun on rc.20 (pair E3, rows #1 and #9).
🤖 Generated with Claude Code
https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2