Skip to content

fix(signals): held reader disposed / lane direct-commit over a stale hold (#3372, #3377) - #3392

Merged
ryansolid merged 1 commit into
nextfrom
fix/held-restore
Sep 12, 2026
Merged

ryansolid merged 1 commit into
nextfrom
fix/held-restore

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Stacked on #3381 (base: fix/async-chain-supersession); retarget to next once that merges. Cluster 2 of gabbev's 2026-09-11 batch: #3372, #3377.

#3372 — a held write lost when its only reader is unmounted

show=true is written while the <Show> child that reads details is pending, so the write is held with that flight (A15). setMounted(false) then disposes the child — the transaction's only reporter — in an ambient flush. The transaction completes when nothing live reports a flight it waits on, and reporterBlocksSource already treats a disposed reporter as dead, but the flush only ever judges the active transaction: a parked one is re-entered by a stamped landing, a stamped recompute or an action resuming, and this disposal is none of those. No check ever ran; show=true stayed staged forever.

Fix: disposeChildren(self) on a node with _transition and STATUS_PENDING (a pending reader is always queued as a pending node, so the stamp is reliable) records the transaction in wokenTransitions — the wake introduced with the #3375 ruling in #3381 (a82d2e3). The flush re-enters it from the finally of an idle pass, prunes the dead reporter and commits.

Pins: the repro; a wake with a surviving outside reader re-parks; a wake adopts no ambient write staged by the pass that disposed the reader (fails without the !scheduled gate: Other: 1 gets dragged into the hold).

#3377Pair stuck at 0 / 0 after an intermediate write restores the initial value

pair reads details() and latest(count). The latest() companion is an optimistic node, so on the pass where the staged count differs from committed, pair adopts its lane and direct-commits _value (Pair: 2 / 2). On the earlier pass at 2000 — after setCount(0) had equality-cancelled the companion's write, so no lane — pair had staged Pair: 0 / 0 as a transaction hold. The direct commit left that hold in the slot; the transaction's commit then published the older frame over the fresh value.

Fix: the override branch of the direct-commit path already dropped the superseded hold ("so its queued commit can't clobber the fresh value"); it now does so for any lane recompute, override or not. This widens an override-only rule to lane-adopted nodes including memos — suite green, principle uniform (INV-11 corollary in the internals doc), flagging it for review.

Size

Floor +58 B on top of #3381 (22,915 → 22,973). Scenario caps bumped with notes (brotli deltas +28…+88 B).

Verification

signals 1763/1763 (+1 skipped), solid 595, web 734; tsc clean for src/ and the new test; size-limit passes. No existing test expectation changed.

Co-authored-by: Claude via Cursor

@changeset-bot

changeset-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7212a76

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

…hold (#3372, #3377)

- A transaction whose only reporter is disposed by ambient work was never
  re-judged: the flush judges only the active transaction and nothing re-enters
  a parked one. Disposing a pending reader parked in a transaction (`_transition`
  stamped, STATUS_PENDING) now records it in `wokenTransitions` (the wake
  introduced with the #3375 boundary-reset ruling); the flush re-enters it from
  the finally of an idle pass, prunes the dead reporter and commits.
- A lane recompute (OPT-dirty) direct-commits `_value` but left a hold staged
  on an earlier lane-free pass of the same transaction in place; the commit
  published the older frame over the fresh one. The drop of the superseded
  hold now applies override or not.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit 6095955 into next Sep 12, 2026
6 checks passed
@codspeed

codspeed Bot commented Sep 12, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 160 untouched benchmarks


Comparing fix/held-restore (7212a76) with next (d7cb456)

Open in CodSpeed

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.

1 participant