Skip to content

fix(signals): optimistic lanes release when nothing authoritative remains; shared render effects don't entangle unrelated writes (#3426, #3427, #3407) - #3434

Merged
ryansolid merged 1 commit into
nextfrom
fix/optimistic-lane-release
Sep 14, 2026
Merged

ryansolid merged 1 commit into
nextfrom
fix/optimistic-lane-release

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Fixes #3426, fixes #3427, fixes #3407.

One theme: the pure graph does not wait where it is not forced to, and a hold is a fact about what a pass observed — never about which reader happened to exist.

#3426 — the last async reader unmounting mid-action releases the frame

const details = createMemo(() => delay(1500, value()));   // derived from the optimistic value
run = action(function* () { setValue(1); yield delay(3000); });
<Show when={show()}><Details /></Show>                    // unmounted at 2500

The lane held Value: 1 on the Details reader observing details pending. A live action parks its transaction without a settle verdict, so that reader's registration was never pruned when it died; the lane waited for a flight nobody observed.

Rule: the hold check prunes as it reads. reporterBlocksSource is now sourceObserved — the live-reporter test shared by the settle verdict (transitionComplete), the lane hold (waitingTransition) and the landing. The frame reveals at the unmount and reverts when the action ends, like a plain write would.

#3427 — the action body ending starts the correction

const details = createMemo(() => delay(1000, value()));
run = action(function* () { setValue(1); yield delay(500); });

The settle waited for details(1) to land (3000: Value: 1 | Details: 1 flashed), then reverted and re-asked details(0) (4000) — a waterfall with a flash, for a guess the action had stopped justifying at 2500.

Rule (A18 body-end corollary): once the bodies have ended and nothing authoritative is in flight — no override node's own source (transitionBlocked), no held flight that does not derive from an override — each override's truth supersedes it, exactly as an arriving differing truth does (#3331): the graph re-derives from the truth as the transaction's held work and settles when that lands (3500, nothing visible changes). Display is untouched: the override stays on screen until commit. Carve-outs, all existing rulings: a co-written pending flag stays through the action's own plain load (setSaving(true); setPage(2) — the attribution-holds saving-indicator pin depends on this); optimistic store edits keep the settle-then-revert order; companions snap at settlement. The typical refresh-inside-the-action pattern never hits the hook — the refresh is the transaction's own authoritative work and holds through.

Mechanism: Transition._acted (set by action, merged), GlobalQueue._endOptimism called from flush after the heap and before the verdict; a supersession re-runs the heap in the same pass.

#3407 — a shared render effect no longer entangles an unrelated sync write

text(() => `${b()}:${detailsA()}`);     // one hole, two bindings
bump = action(function* () { setB(p => p + 1); });

A plain setB published 1:0 beside the pending detailsA; the same write inside an action merged into the async's transaction and waited (0:0 → 2:1). recompute's head re-entered a stamped effect's transaction whenever any other transaction was active — before the pass ran, before anything could have been observed.

Rule (A15 shared-hole corollary): a render effect's pass belongs to whatever dirtied it. The stamp re-entry is memo-only (a memo's value is its transaction's work); a stamped effect dirtied by another writer runs that writer's pass, reads the held flight as a stale reader (committed value, coherent with the flight's committed inputs) and publishes with the writer. Only a pass that actually observes a pending flight — the reveal carve-out refused — joins it. The re-entry's one legitimate job, completing every reveal that discovered a flight, moves to the landing: settleTransitionenterWaiting folds in every transaction still waiting on the node. Consequence beyond the issue: two independent flights read in one hole now land at their own times (next: one unit).

Verification

Based on next after #3432.

Co-authored-by: Claude via Cursor noreply@cursor.com

…ains; shared render effects don't entangle unrelated writes (#3426, #3427, #3407)

Three of gabbev's optimistic/transaction reports, one theme: the pure graph
does not wait where it is not forced to, and a hold is a fact about what a
pass observed — never about which reader happened to exist.

- #3426: the last async reader of an optimistic value unmounting mid-action
  releases the frame. `reporterBlocksSource` is now `sourceObserved`, shared
  by the settle verdict, the lane hold (`waitingTransition`) and the landing:
  the hold check prunes dead reporters itself. A live action parks its
  transaction without a verdict, so this is the only prune such a frame ever
  gets — the lane used to wait for a flight nobody observed.

- #3427: the action body ending starts the correction (`endOptimism`, called
  from `flush` after the heap and before the verdict). With the bodies over
  (`_acted`, no live `_actions`) and nothing authoritative up — no override
  node's own source in flight, no held plain load the action asked for —
  each override's truth supersedes it as an arriving differing truth does
  (A18): the graph re-derives now and the transaction settles when THAT
  lands. Before: wait for the obsolete lane-derived flight, flash the
  obsolete optimistic frame, revert, re-ask — a waterfall. Display is
  unchanged (override on screen until commit). A co-written pending flag
  stays through the action's own load (attribution-holds pin); optimistic
  store edits keep settle-then-revert; companions snap at settlement.

- #3407: a render effect's pass belongs to whatever dirtied it. `recompute`
  re-entered a stamped effect's transaction whenever another was active, so
  a sync `action` write to a signal that merely shared a hole with a held
  async merged into the async's transaction and waited (0:0 → 2:1) while the
  same plain write passed through. The stamp re-entry is memo-only now; the
  re-entry's one legitimate job — completing every reveal that discovered a
  flight — moves to the landing (`settleTransition` → `enterWaiting`). A15
  amended in place: a shared reader entangles nothing by itself; only a pass
  that observes a flight pending joins it. Two independent flights read in
  one hole now land at their own times.

Docs: SPEC A15 shared-hole corollary, A18 body-end corollary; INTERNALS §2/§3
mechanism notes; RULES-INDEX regenerated. Floor 23,058 → 23,195 (+78 B
#3426/#3427, +59 B #3407); size-limit caps ratcheted with notes.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9e21d80

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

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 34885888106

Coverage remained the same at 71.842%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1007
Covered Lines: 772
Line Coverage: 76.66%
Relevant Branches: 790
Covered Branches: 519
Branch Coverage: 65.7%
Branches in Coverage %: Yes
Coverage Strength: 15.03 hits per line

💛 - Coveralls

@codspeed-hq

codspeed-hq Bot commented Sep 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 160 untouched benchmarks


Comparing fix/optimistic-lane-release (9e21d80) with next (a031271)

Open in CodSpeed

@ryansolid
ryansolid merged commit 25c5064 into next Sep 14, 2026
7 checks passed
@ryansolid
ryansolid deleted the fix/optimistic-lane-release branch September 14, 2026 19:46
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