Skip to content

fix(signals): two flights through one memo settle as one unit; lane-dirtied zombies run (#3443, #3444) - #3450

Merged
ryansolid merged 3 commits into
nextfrom
fix/async-memo-overlap
Sep 15, 2026
Merged

ryansolid merged 3 commits into
nextfrom
fix/async-memo-overlap

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Fixes #3443. Fixes #3444.

#3443 — a shared memo disagrees with its async inputs during overlapping updates

const a = createMemo(() => delay(1000, inputA()));
const b = createMemo(() => delay(1000, inputB()));
const sum = createMemo(() => a() + b());
// setA(1); await delay(500); setB(1);

Before: 3000: A: 1 alone, 3500: B: 1 | Sum: 2. Traced with the reporter map: setB opens T3 and b's pending propagates onto sum — a memo T2 holds — without recomputing it (its inputs' values are unchanged), so the memo's stamped re-entry never runs and T2 never learns it is waiting on b. sum's reader registers on b in T3 (INV-3 is keyed by transaction); T2's verdict at a's landing sees only its own reporters, commits, and reveals A: 1 beside the committed Sum: 0.

Fix: notifyStatus's dependent walk, on a pending propagation reaching a memo stamped by another live transaction, enters that transaction (initTransition(sub._transition) — a merge, or with nothing active an entry that adopts the ambient batch). This is A15's headline case (a shared derivation of both flights settles as one unit) applied at the one moment it is known. Effects are skipped: an effect entangles nothing by itself (A15 shared-hole corollary, #3407) — the plain-getter shape stays parallel (3000: A: 1 | Sum: 1, 3500: B: 1 | Sum: 2), pinned alongside.

Result: 3500: A: 1 | B: 1 | Sum: 2.

Semantic to note: the write that starts the second flight is held with the first when its async work flows into a held memo — page=1 waits with count=1 while details re-asks, even though Sum reads page plainly. Pinned. The existing #3375 boundary-reset pin moves accordingly: the reset now publishes Sum: 2 once instead of the two-pass Sum: 1 | Sum: 2 its comment described as a drain artifact; every other frame in that pin is unchanged.

#3444latest() inside a Show stays stale while removal is pending

When the parking batch is the transaction, flush cancels the zombie recomputes its staged writes queued (cancelZombieRecompute) — a zombie renders mainline until the commit disposes it, and the staged world is one it never displays. A zombie dirtied through the lane channel (REACTIVE_OPTIMISTIC_DIRTY: an override, or the latest() companion the write synced) is the exception: the lane's values are the mainline frame, so the still-visible branch showed latest(count) at 0 beside the same read outside at 1. Such a zombie now runs; its pass runs under the lane and its run lands on the lane's effect queue, so a held lane defers it as it defers every other reader's.

Result: 500: Inside: 1 | Outside: 1, 1500: Inside gone.

Verification

@changeset-bot

changeset-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: aa2cfcc

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

…irtied zombies run (#3443, #3444)

flight, entangles the two at the propagation. The second flight only
propagated pending onto the memo (no recompute — its inputs' values were
unchanged), so the memo's stamped re-entry never ran and the first
transaction never learned it was waiting on the second: it revealed `A: 1`
beside the committed `Sum: 0`, and `Sum: 2` arrived with `B: 1`.
`notifyStatus`'s dependent walk now enters the held memo's transaction
(A15: a shared derivation of both). Effects are skipped — an effect
entangles nothing by itself (A15 shared-hole corollary). The write that
starts the second flight is held with the first when its async work flows
into a held memo; the #3375 boundary-reset pin publishes `Sum: 2` once at
the reset instead of a two-pass `Sum: 1 | Sum: 2`.

zombie recomputes its staged writes queued (a world the zombie never
displays). A zombie dirtied through the lane channel (OPTIMISTIC_DIRTY —
an override or a `latest()` companion) displays exactly that world
mainline until the commit disposes it, so it runs instead: `latest(count)`
inside a branch a held `Show` is removing now follows the value outside.

Docs: A15 mechanism completed (#3443) and pinned by
tests/overlapping-flights.test.ts; INTERNALS bullets for both; rules
index regenerated. Floor 23,365 -> 23,419 (+54 B, conscious bump to
23,450); three brotli caps ratcheted 0.05 KB with notes.

Co-authored-by: Claude via Cursor <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid force-pushed the fix/async-memo-overlap branch from a056d36 to d89a3c8 Compare September 15, 2026 06:21
@codspeed

codspeed Bot commented Sep 15, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 10.67%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 159 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
projection derive: write one NESTED field (reference) 2.5 ms 2.8 ms -10.67%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/async-memo-overlap (aa2cfcc) with next (75c5113)

Open in CodSpeed

…3443 carve-out)

A memo stamped by another live transaction but holding nothing of it —
status clear, nothing staged — is not entangled when a second flight
propagates pending onto it. The stamp alone decides nothing (#3334): a
Dynamic switched twice mid-flight dragged the superseded first call's
gate into the live second call's reveal (web call-driven-lifecycle
args-switch-gate: expected Two, got Zero). Keyed on STATUS_PENDING or a
staged _pendingValue instead; #3443's shapes are unchanged.

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

coveralls commented Sep 15, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 34938687153

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.08 hits per line

💛 - Coveralls

Measured at 8718 B against next's 8691 (+27). The stamp-only first cut fit
under 8.70 at 8690; keying entanglement on STATUS_PENDING / a staged value
is what tips the cap. Every other scenario unchanged and within cap.

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

Copy link
Copy Markdown
Member Author

Pushed a cap bump on top of 42632d5: the holds carve-out tips the signals: core floor scenario to 8718 B against next's 8691, 18 B over the 8.70 KB cap (check failed on it). Ratcheted to 8.75 KB with the measurement in the note; the comment's "no bump, measured at 8690" described the stamp-only first cut. All ten scenarios pass locally.

Reviewed the carve-out itself: keying on STATUS_PENDING / a staged _pendingValue rather than the stamp matches the #3334 precedent, and the check on sub runs before the recursive notifyStatus(sub, …) that would mark it, so it reads the memo's pre-propagation state. Locally on this head: signals 174/1895, web 86/783 (args-switch-gate 17/17), solid 27/618, rules-index current.

Claude via Cursor

@ryansolid
ryansolid merged commit 05c7e21 into next Sep 15, 2026
6 of 7 checks passed
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