fix(signals): defer a transaction-owned node's committed children to its commit (#3404) - #3405
fix(signals): defer a transaction-owned node's committed children to its commit (#3404)#3405brenelz wants to merge 1 commit into
Conversation
…its commit (solidjs#3404) A render effect parked on a pending source is stamped with the transaction without recomputing, so its owned children (nested render effects, memos, onCleanup registrations) still belong to the committed frame. When the source landed, recompute saw the stamp and disposed them on the spot, running their cleanups mid-hold — a Portal under a keyed Show disappeared when the first hop of an async chain landed and reappeared only when the whole chain settled. Children are now deferred as zombies until the node commits unless CONFIG_HELD_CHILDREN says the pass that built them never committed (a staged value, a pending window, a run under a held transaction), in which case no frame ever showed them and they die immediately. The bit is set at recompute's tail and cleared by commitPendingNode. A transaction-owned effect recomputed mainline (contested, solidjs#3322) publishes directly and releases its zombies itself rather than letting two generations render until the transaction commits. Co-Authored-By: Claude Fable 5.1 <[email protected]>
🦋 Changeset detectedLatest commit: 323f9ad 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 |
Merging this PR will regress 4 benchmarks
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | merge |
264.5 µs | 330.4 µs | -19.93% |
| ❌ | merge |
227.2 µs | 269 µs | -15.54% |
| ❌ | merge |
265 µs | 287 µs | -7.65% |
| ❌ | merge |
74 µs | 78.1 µs | -5.25% |
| ⚡ | projection derive: write one NESTED field (reference) |
830.9 µs | 222.6 µs | ×3.7 |
| ⚡ | merge |
45 µs | 31 µs | +44.79% |
| ⚡ | construct |
56.5 µs | 39.1 µs | +44.45% |
| ⚡ | merge |
46.6 µs | 32.5 µs | +43.41% |
| ⚡ | construct |
56.7 µs | 39.8 µs | +42.32% |
| ⚡ | construct |
56.6 µs | 39.8 µs | +42.24% |
| ⚡ | merge |
45.4 µs | 32 µs | +41.55% |
| ⚡ | construct |
58.6 µs | 41.9 µs | +39.73% |
| ⚡ | merge |
49.2 µs | 35.6 µs | +38.13% |
| ⚡ | merge |
48.6 µs | 35.4 µs | +37.31% |
| ⚡ | merge |
67.6 µs | 54.5 µs | +24.06% |
| ⚡ | merge |
68 µs | 54.9 µs | +23.95% |
| ⚡ | merge |
72.2 µs | 60.6 µs | +19.24% |
| ⚡ | merge |
55.1 µs | 46.6 µs | +18.31% |
| ⚡ | merge |
54.3 µs | 45.9 µs | +18.31% |
| ⚡ | merge |
87.6 µs | 74.4 µs | +17.68% |
| ... | ... | ... | ... | ... |
ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing brenelz:fix/nested-render-effect-cleanup-3404 (323f9ad) with next (042b540)1
Fixes #3404.
Bug
A render effect parked on a pending async source is stamped with the transaction by status propagation without being recomputed, so its owned children (nested render effects, memos,
onCleanupregistrations) still belong to the committed frame. When the source landed,recomputesaw the_transitionstamp and took the "dispose children immediately" branch, running their cleanups mid-hold. In the issue's repro aPortalunder a keyedShowvanished when the first hop of an async chain landed and only reappeared once the whole chain settled, instead of swapping atomically.Fix
CONFIG_HELD_CHILDRENbit: this node's current children were built by a pass whose result has not committed (a staged value, a pending window, a run under a held transaction).recomputedisposes children immediately only for tracked effects or when the bit is set (no frame ever showed them). Everything else is deferred as zombies until the node's commit, whether or not a transaction owns the node.recomputewhenever the pass's result waits on a commit, and cleared bycommitPendingNode.Tests
packages/signals/tests/nested-render-effect-async-cleanup.test.ts: the issue's shape, a memo withonCleanup, a sync write during the hold (held children die silently, the frame's swap and then wait for the reveal), and the plain sync ordering pinned as it already was.packages/web/test/nested-render-effect-cleanup-issue-3404.spec.tsx: the Portal / Show / Loading scenario from the playground. Fails onnextwith an empty portal target mid-hold, passes with the fix.Changeset and a note in
INTERNALS-ASYNC-STATE.mdincluded.🤖 Generated with Claude Code