fix(signals): an async memo's held landing keeps the committed frame's dependencies (#3461) - #3464
Merged
Merged
Conversation
…s dependencies (solidjs#3461) `selected = createMemo(async () => (b() ? b() : a()))` with `b` held by a slow flight: selected's held pass read only `b`, and asyncWrite trimmed the dependency tail unconditionally before the write, so the `selected -> a` edge was gone even though the landed value was then staged under the hold. The committed `Selected: 0` still derived from `a`, but the mainline `a` write no longer reached selected or entered its hold through the stamp, and committed beside the stale derivation: `A: 1 | B: 0 | Selected: 0`. The landing now follows A30's gate (the arm `recompute` and `commitPendingNode` already implement for sync passes and effects): trim after the write, only when the landing published (`_pendingValue === NOT_PENDING`); a transition-held landing leaves the tail for its commit. The `a` write reaches selected, joins the hold, and the four values reveal as one frame, identical to the sync control. Spec: A30 gains the landing arm; rules index regenerated. Core floor flat (23,752); brotli core-floor cap ratcheted 8.85 -> 8.90 KB for +2 B of noise. Changeset included. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: c24e38e The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 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 |
ryansolid
added a commit
to brenelz/solid
that referenced
this pull request
Sep 15, 2026
…reset walk on top of solidjs#3464/solidjs#3465/solidjs#3466 Measured at 28861 B against next's 28815 (+46). The PR alone sat 2 B under against the pre-solidjs#3464 next; the three fixes that landed meanwhile used the room. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid
added a commit
that referenced
this pull request
Sep 15, 2026
…s still wait on (#3459) (#3467) * fix(signals): a Loading `on` reset collects what its forwarded readers still wait on (#3459) The `on` reset cleared the boundary's sources and rebuilt them from the pending notifications that followed. A reader already pending from an earlier write never re-notifies (status propagation dedupes on its `_pendingSources`), so with `fast` and `slow` read from sibling effects only the fresh flight was collected; the wake committed the held write (#3375: a reader behind a collecting boundary holds nothing), `fast` landed a microtask later, and the boundary revealed `B: 1 | Fast: 1 | Slow: 0` with `slow` still in the air. The reset now harvests, from every live transaction's `_asyncReporters` (INV-3, the one record of a forwarded reader), the sources of each reporter it routes (`_holds`: under this queue with no collecting pending-type boundary between, the `reporterBlocksSource` test) plus that reporter's `_pendingSources`, and flips to the fallback when it found any. The hold the ruling takes off the lane lands on the boundary: `B: 1 | Loading`, then `Fast: 1 | Slow: 1` together. `scheduler.ts` exports `transitions` for the walk; no scheduling logic changes. Core floor unchanged (23,752). Docs bullet, rules index, changeset. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(size): hydrating+stores cap 28.85 -> 28.90 KB — #3459's reset walk on top of #3464/#3465/#3466 Measured at 28861 B against next's 28815 (+46). The PR alone sat 2 B under against the pre-#3464 next; the three fixes that landed meanwhile used the room. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com> * docs(signals): regenerate rules index after the INTERNALS merge resolution Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Brenley Dueck <brenleydueck@Brenleys-Mac-mini.local> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Ryan Carniato <ryansolid@gmail.com> Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3461.
The gap
setB(1)is held byslow. Half a second latersetA(1)publishes alone:A: 1 | B: 0 | Slow: 0 | Selected: 0for the rest of the hold, andSelectedno longer agrees with the inputs on screen (b()is 0, so it should reada()). At 2000 everything settles to 1. Withselecteda plaincreateMemotheawrite is held with the transaction and the four values reveal as one frame.Traced:
selected's held pass reads onlyb(throwsNotReadyat the flight's registration, keeping its full dependency list with_depsTailatb). Its flight lands in a microtask into the hold, andasyncWritecalledtrimStaleDepsunconditionally, before the write, so theselected -> aedge was gone even thoughsetSignalthen staged the landed value under the transaction. The committedSelected: 0still derived froma, but the mainlineawrite no longer reachedselected, never entered its hold through the stamp, and committed beside the stale derivation. A30 (#3410) already rules this for a sync pass (recomputetrims only when the pass published,commitPendingNodetrims a staged pass) and for effects (#3438); the async landing predates the rule (the trim is from the initial import) and was the missing arm.Fix
The landing follows A30's gate:
asyncWritetrims after the write and only when the landing published (_pendingValue === NOT_PENDING, an equal-value or lane landing); a transition-held landing leaves the tail forcommitPendingNode, exactly as a staged sync pass does. One call moved:Now the
awrite reachesselected, its stamped recompute enters the hold, and the frame is5000: A: 1 | B: 1 | Selected: 1 | Slow: 1, identical to the sync control.Trade-offs, stated plainly: a plain (unheld) landing's trim moves from the landing to the same flush's commit, A30's stated cost model. In the #3373 shape (a landing held while another input is still pending,
_errorset) the stale tail lives until the next clean pass or landing, which is over-subscription only, never a missed wake.Side finding, not fixed here: the equal-value variant
selected = async () => (b() ? 0 : a())still tears the same way in both the sync and the async shape, because A30 lets a pass that changed nothing trim at its tail (a landing equal to the committed value published nothing new). This PR mirrors that carve-out deliberately; closing it is a rule decision and is noted in the A30 paragraph.Verification
@solidjs/signals: 180 files / 2038 tests green (2036 passed, 1 skipped, plus the new file), including the newtests/async-landing-deps-3461.test.ts(the issue shape on a manual clock pinned to exact frames, and the sync control). The only non-green run washeap-mark-incremental"linear in N (2.0.0-rc.7: mounting N rows is O(N²) when each row creates an effect over a signal written during its own creation — markHeap re-walks the whole pure heap on every memo pull #3350)", a timing test that passed on re-run in isolation.@solidjs/web: 88 files / 793 tests green, including the newtest/async-landing-deps-3461.spec.tsx, a jsdom port of the issue's playground (real click, DOM sampled through the hold, with the plain-memo control); fails onnext, passes here.nextbuild (+2 B, noise for the moved call); every other scenario is within noise of the baseline. "signals: + createStore" is pre-existing red onnextby 9 B (15,809 vs the 15,800 cap, from the store changes in perf(signals,web,universal,html): merge/omit are always lazy views; consumers read the leaves #3454/test(signals): store oracle + four signal states; latest() seed leak and body-end visibility fixes #3455); this PR does not touch that cap and measures 15,762 on its own build.--checkclean; the one unresolved test id is pre-existing onnext). Changeset included.🤖 Generated with Claude Code