Skip to content

fix(signals): a source pending on a superseding re-ask keeps its transaction blocked (#3462) - #3466

Merged
ryansolid merged 2 commits into
solidjs:nextfrom
brenelz:fix/superseded-source-blocks-3462
Sep 15, 2026
Merged

ryansolid merged 2 commits into
solidjs:nextfrom
brenelz:fix/superseded-source-blocks-3462

Conversation

@brenelz

@brenelz brenelz commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #3462.

The gap

const details = createMemo(() => delay(2000, a()));
const selected = createMemo(() => delay(2000, a() + b() + details()));
<p>Show: {String(show())}</p>
<p>Panel: {show() ? selected() : "hidden"}</p>
// click: setB(1); 500ms; setShow(true); 500ms; setA(1); 500ms; setShow(true)

setB(1) re-asks selected while nothing observes it, so the flight is in the air with no hold. setShow(true) flips the conditional onto the pending selected: the reveal opens a transaction holding show = true, blocked on selected (INV-3). setA(1) re-asks details; selected is now pending on that flight instead of its own. Then the second setShow(true): on next, Show: true publishes at once beside Panel: hidden, and Panel says 3 only when the whole chain lands seconds later. Without the repeated write, both publish together at the landing.

Traced: transitionComplete judged a reporter's source by its own flight alone, the self entry in _pendingSources (#3375 chose that over _error.source, which a later-pending input overwrites). The details re-ask superseded selected's own flight and retired the self entry, leaving selected pending on details; the verdict flipped to complete while selected's reader still could not render. The transaction was parked, so nothing re-judged it. The repeated same-value write re-entered it through setSignal's initTransition(el._transition) (which runs before the equality check), the flush trusted the verdict and committed show = true, and the panel effect, replayed against a selected whose inputs had been published beneath its first flight (CONFIG_INPUTS_PUBLISHED), refused the carve-out and parked on the chain with no transaction of its own.

Fix

A registered source blocks while it is still pending on anything, not only on its own flight:

if (sourceObserved(transition, source) && source._x?._pendingSources?.size) {

This is the verdict A15 already implies (async work observed by a reader settles as one unit with the writes that asked it) and matches what the landing does anyway: enterWaiting folds every transaction with a live reporter on the source into the landing's transaction. The only state the change touches is a source whose own flight a superseding re-ask retired; the #3375 boundary-reset case is decided earlier by reporterBlocksSource's fallback prune and still passes. The reporter set is unchanged and nothing entangles earlier than before; the transaction simply stays parked until the chain lands.

before: Show: false | Panel: hidden -> Show: true | Panel: hidden -> Show: true | Panel: 3
after:  Show: false | Panel: hidden -> Show: true | Panel: 3

Known edge not addressed: a parked transaction whose blocking source later settles synchronously (its re-run after the upstream landing returns a plain value, so there is no landing of its own to fold the waiter in) has no wake, before and after this change; the fold at a landing is the only re-entry the reporter path has.

Verification

  • @solidjs/signals: 180 files / 2038 tests (2037 green, 1 skipped as on next; the timing-only heap-mark-incremental "linear in N" case failed once in the full run and passes in isolation), including the new tests/superseded-source-blocks-3462.test.ts (the issue shape on deferred gates, plus the no-repeat control pinned to the same two frames).
  • @solidjs/web: 88 files / 793 tests green, including the new test/superseded-source-blocks-3462.spec.tsx, a jsdom port of the playground (real click, DOM sampled through the hold, delays scaled to 100ms) with the same control; the issue shape fails on next, passes here.
  • In-package minified core floor 23,752 -> 23,750 (-2 B). Brotli caps measured against a pristine next (a8a8949) build: core floor 8,849 -> 8,851 (+2 B), which tips a cap that sat 1 B under the baseline, so it is ratcheted 8.85 -> 8.90 KB with the measurement in the note; + createStore is pre-existing red on next by 9 B (15,809 vs 15,800, from 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 reads 15,770 here; every other scenario is within noise of the baseline and under its cap.
  • Docs: INTERNALS-ASYNC-STATE.md's transitionComplete bullet restated (judged by the set, not the self entry); rules index regenerated (--check clean). Changeset included.

🤖 Generated with Claude Code

…saction blocked (solidjs#3462)

`transitionComplete` judged a reporter's source by its own flight alone
(the self entry in `_pendingSources`). An upstream re-ask that supersedes
that flight retires the entry and leaves the source pending on the re-ask,
so the verdict flipped to complete while the source's reader still could
not render. The transaction was parked, so nothing re-judged it until a
re-entry: repeating `setShow(true)` while the first write was held
re-entered it through setSignal's stamp path, and the flush committed
`Show: true` beside `Panel: hidden`, with the panel catching up seconds
later at the chain's landing.

The source now blocks while its `_pendingSources` is non-empty; the
landing folds the transaction in as before (A15), and the no-repeat frames
are unchanged. Doc bullet restated, rules index regenerated, changeset
added. Core floor brotli 8,849 -> 8,851 B (+2) tips a cap that sat 1 B
under the baseline: ratcheted 8.85 -> 8.90 KB with the measurement noted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2a3ae8c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 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

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

@codspeed

codspeed Bot commented Sep 15, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 172 untouched benchmarks


Comparing brenelz:fix/superseded-source-blocks-3462 (2a3ae8c) with next (0da94f9)

Open in CodSpeed

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/signals/docs/RULES-INDEX.md
#	scripts/size/.size-limit.js
@ryansolid
ryansolid merged commit 5f7da9d into solidjs:next Sep 15, 2026
7 checks passed
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>
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.

2 participants