Repository navigation
fix(signals): a derived store over latest() never serves its seed after it first resolves (#4355) - #4369
Conversation
…er it first resolves (#4355) A derived store whose derive reads an async memo through `latest()` resolved, but a memo reading it saw the seed for one pass: a render effect behind that memo committed `undefined`, then the value. A render effect reading the store directly saw only the value. Cause: the derive reads `latest()`, so it is a verdict reader, and a memo that reads it through the store's status pull becomes one too (F-6, #4050). In the landing flush the derive's first write is a staging and the derive is still uninitialized. Core serves a verdict reader the screen for a staged signal (A10, `stagedScreen`), and a store leaf's screen is the committed backing, which is the seed. The seed is never observable (A25, D-30). A memo born staged has the same guard in core `serve`, but store leaves take the signal fast path, which has no status to check. Fix: the store's `nodeValue` serves the leaf's unheld staging while the family's derive has never committed (STATUS_UNINITIALIZED), as it already does for a handler read. Store engine only: +19 B minified on the four store scenarios, every floor unchanged. Tests: packages/signals/tests/derived-store-latest-seed-4355.test.ts (memo, second derived store and direct readers, latest and plain derives) and packages/web/test/derived-store-latest-seed-4355.spec.tsx (a hole through a memo and a direct hole). Size ledger lines added; RULES-INDEX regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 4d17c17 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 40731526454Coverage remained the same at 78.651%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 8.41%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
…-x1ore2-4355 # Conflicts: # scripts/size/scenarios.js
Fixes #4355
Summary
Before. Take a derived
createStore(fn)whose derive reads an async memo throughlatest(). It resolved, but the first memo to read it after the landing got the store's seed for one pass. A render effect behind that memo committedundefinedand thenHartford. A second derived store copyingstate.valuedid the same thing. A render effect reading the store directly saw onlyHartford. So putting a memo between the store and the hole changed what reached the screen. In the report's app,name.trim()threw on that pass.After. The reader sees what the derive wrote,
{ name: "Hartford" }, on its first pass. The effect commits onlyHartford, whether it reads through a memo, through a second derived store, or directly (D-30, A25: a derived store's seed is never observable. D-3: grouping doesn't change the result).How
latest(), it is a verdict reader. A memo that reads the store becomes one too, through the store's status pull of the derive (F-6, [2.0 rc.15 + next] Held navigation never publishes delayed pending feedback #4050).STATUS_UNINITIALIZED. Core serves a verdict reader the screen for a staged signal (A10,stagedScreen). For a store leaf, the screen is the committed backing, which is the seed.serve: no committed value, so the staging is its only value. Store leaves take the signal fast path, which has no status to check, so they missed that guard.The fix is in the store layer only.
nodeValuealready served a projection's unheld staging to a handler read (context === null). It now also serves it while the family's derive has never committed:Nothing changes in core: no new exports, effects or computations.
How did you test this change?
packages/signals/tests/derived-store-latest-seed-4355.test.ts: alatest()derive and a plain derive, each with three readers (memo, second derived store, direct render effect). Each case expects the effect to commit only"Hartford". Onnextthe memo and store readers fail with[undefined, "Hartford"]. All 6 pass with the fix.packages/web/test/derived-store-latest-seed-4355.spec.tsx: a<Loading>hole that rendersstate.value!.name.trim(), once through a memo and once directly. Onnextthe memo form halts withTypeError: Cannot read properties of undefined (reading 'name'). Both pass with the fix.turbo run build --force:RULES-INDEX.mdregenerated for the new A25 citation insrc/)server-functions-adapter-request.spec.tsx"keeps a rebuild failure's own error…". Node 22 accepts the test's fakeAbortSignalinnew Request. That test doesn't touch stores or signals, so this change can't affect it.test-typespasses for solid and web.Size
Measured locally (linux) with
scripts/size, head againstnext@ 6b7e34a. Both trees had a clean build.node gate.mjs head.json base.jsongives: every scenario passes, with none newly over its cap. The 11 brotli warnings are already there onnextlocally, with +0 B minified. Only scenarios that carry the store engine move. Every frozen floor is unchanged, including the compiled hello-world floor at 9,933 B.There are ledger lines in
scripts/size/scenarios.jsfor the four scenarios that grew. No cap changes. The store-family app's recorded minified goes from 96,944 to 96,963 B. It was recorded atnext's exact size, and this change is within the 20 B allowance.floor-caps.jsonis untouched.Generated by Claude Code