fix(signals): isPending() inside a memo reports a hold whose staged node is unstamped (#3457) - #3465
Merged
ryansolid merged 3 commits intoSep 15, 2026
Conversation
…ode is unstamped (solidjs#3457) A memo wrapping isPending(copy) cached false for a whole hold while a direct isPending(copy) render-effect probe read true. `slow` pended first and opened the transaction; `copy` then staged its value straight into the transaction's batch, unstamped until the flush stashed the hold. The wrapper memo, recomputing on the companion flip, read copy's fresh staged value, and heldAwaitingAsync's reporter scan was gated on the missing `_transition` stamp, so the A10 fresh-read pairing rule muted the verdict even though the transaction's async source was still computing. Run the reporter scan for an unstamped node too: with a transaction active, the ambient batch IS that transaction, so the node's staged write belongs to the transaction `t` resolved to. The pairing rule still stands for a landed answer awaiting reveal, and the solidjs#3078 action carve-out is untouched. Spec note under A10; rules index regenerated; changeset. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 6861fa5 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 |
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # packages/signals/docs/RULES-INDEX.md
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 #3457.
The gap
setCount(1)holds the write for a second. Observed onnext(jsdom port, 100ms delay):Expected
Memo pending: truebesideDirect pending: truefor the whole hold.Traced:
slowrecomputes first, pends, and queue notification opens the transaction (the ambient batch is adopted;currentBatchbecomes the transaction).copyrecomputes next, with the transaction already active:queuePendingNodepushes its staged1straight into the transaction's pending nodes, but nothing stampscopy._transitionon that path (stamping happens at batch adoption and at stash).syncCompanions(#3413) correctly flipscopy's pending companion totrue, which OPT-dirtiespendingon the companion's lane.pendingrecomputes under that lane (A31) and probescopy. A memo is not a stale reader, solaneReadsCommitteddeclines and the probe reads the fresh staged1.recordFreshReadasksheldAwaitingAsync(copy):tresolves toactiveTransition, whose_asyncReportersalready listsslowas pending, butcopycarries no stamp yet and the scan was gated onif (!et) return false. Socopylands infreshReads, the A10 pairing rule mutes the companion'strue, and the memo cachesfalse. It is registered insuppressedProbes, butwakeSuppressedProbesonly fires when a reporter is added, andslowregistered beforecopyever recomputed, so nothing corrects it. The direct render effect is stale, reads the committed0under the lane, is not a fresh read, and reportstrue. Hence the disagreement for the whole hold.The stamp gate was added by #3078 alongside the
t = activeTransitionfallback, purely to keep the reporter scan's previous reach; it carries no rule of its own. #3413 (companion sync) and #3445 (A31 lane posture) both hold here: the companion flips, the memo runs under its own lane. The failure is in the verdict pairing, not the value read.Fix
Drop the stamp gate in
heldAwaitingAsyncso the reporter scan also runs for an unstamped node. This is sound because wheneveractiveTransitionis non-null,currentBatch === activeTransition, so a node with a staged_pendingValueand no stamp necessarily belongs to that transaction:tresolved to the transaction that owns its staged write. The A10 pairing rule is unchanged for what it was ruled for, a LANDED answer awaiting reveal: the scan returns true only while a source in the transaction is still pending with reporters, so a transaction with nothing computing still mutes the verdict for a reader that saw the fresh value, stamped or not. The #3078 action carve-out is untouched.Trade-off stated plainly: the still-computing carve-out now also applies in the mid-flush window between a transaction's adoption and its stash. The alternative, stamping
_transitioninqueuePendingNodewhen the batch is a transaction, would touch the scheduler hot path and the core byte floor for the same outcome.Timeline after the fix (same jsdom port):
Verification
@solidjs/signals: 180 files / 2036 tests passed, 1 skipped, including the newtests/ispending-memo-unstamped-hold-3457.test.ts(A10 / #3457 isPending(copy) inside a memo reports the hold like the direct probe; fails onnext, passes here). 2.0.0-rc.8 isPending reports false for a held synchronous memo #3413, Adding combined isPending breaks atomic publication #3442 and 2.0.0-rc.3 | didn't get the memo (isPendingmemo reads inconsistently in a transition) #3078 pins still green.@solidjs/web: 88 files / 792 tests passed, including the newtest/ispending-memo-unstamped-hold-3457.spec.tsx(A10 / #3457 both probes read true for the whole hold, false after the reveal; the playground repro with a real click and the DOM sampled through the hold; fails onnext, passes here).tests/treeshake.test.tsgreen;verdict.tsis shed from the core floor bundle, so the floor is unchanged.scripts/size, measured against a pristinea8a89497build):+ isPending/latest11,258 -> 11,253 B (-5 B); every other scenario byte-identical to the baseline (core floor 8,849; simple-app 11,563; hydrating 18,991 / 28,768; CSR 14,429; observe 15,807 / 27,335; frames 11,370). No cap ratcheted. Note:signals: + createStoreis 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) and is unchanged by this PR (+0 B).verdict.tscites A10 at the site; rules index regenerated (--checkclean; the one unresolved test id is pre-existing onnext). Changeset included.🤖 Generated with Claude Code