Conversation
…js#3554) unobserved() splices an auto-dispose memo out of parent._firstChild when its last subscriber leaves, but the reawaken in prepareComputed only recomputed it. The node then ran live outside the chain with a null _prevSibling and a stale _nextSibling, so its next dormancy took the head branch of the splice and wrote the stale pointer into parent._firstChild, orphaning every sibling created after it (still subscribed, still running after the root's dispose()). Dev builds threw the owner-chain-head invariant from solidjs#3552 mid-flush instead. The reawaken now links the node back at the chain head before the recompute, through the same linkChild helper createOwner and setupComputedNode use for the creation-time link. A zombie is not relinked: disposeChildren skipped its splice, so it still sits on the parent's pending chain. Core floor 25,671 -> 25,684 bytes. Claude-Session: https://claude.ai/code/session_01JH7R7yDguTxuXpDioMzMCT Co-Authored-By: Claude <noreply@anthropic.com>
A dormant lazy memo read after its owner's dispose() is no longer linked into that owner's child chain. disposeChildren skips the dormancy splice when the parent is already REACTIVE_DISPOSED, so a relinked node stayed at the head and the next reawaken linked it to itself, which sent markDisposal and DEV.getChildren into an endless loop; the dead chain is also never drained again, because dispose() early-returns on REACTIVE_DISPOSED. linkChild resets node._prevSibling itself instead of relying on the caller having cleared it. Core floor 25,684 -> 25,716 bytes, under the 25,750 cap. Co-Authored-By: Claude <noreply@anthropic.com>
A lazy memo a cleanup reawakens while its owner's held children are torn down stays on the owner's chain. recompute drains those children in place without flagging the owner, so the reawaken relinked the memo, and the drain's trailing _firstChild = null then dropped it again, live and off the chain; its next dormancy wrote null into the head and dev builds threw the owner-chain-head invariant. disposeChildren now detaches the chain before its loop and points each child's _prevSibling at itself, so the splice never writes the detached remainder back into the head and a node linked during the drain survives it. Core floor 25,716 -> 25,724 bytes. The treeshake ledger records the branch's bump (25,671 -> 25,724). Co-Authored-By: Claude <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: e4a7cb9 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 not alter performance
Comparing Footnotes
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3554
What was broken
An auto-dispose memo (
createMemo(fn, { lazy: true })) that went dormant wasspliced out of its owner's child chain and never relinked when a later read
reawakened it. Its next dormancy ran the splice again from outside the chain
and wrote a stale
_nextSiblingintoparent._firstChild, orphaning everysibling created after it: never disposed by the owner, still subscribed to
their sources, still running after the root's
dispose(). Dev builds threwthe
owner-chain-headinvariant from #3552 mid-flush instead.Root cause
unobserved()callsdisposeChildren(node, true), which unlinks the node,nulls
_prevSiblingand leaves_nextSiblingintact so outer walks stilladvance. The reawaken in
prepareComputedonly calledrecompute(comp, true),whose flag wipe cleared
REACTIVE_DISPOSED. The node was then live by flagsbut outside the chain, so the next splice took the head branch and clobbered
the head with that stale pointer.
What changed
prepareComputedlinks the node back at the chain head before recomputing, so"live" again implies "in the chain" and the owner's
dispose()and rerunsreach a reawakened memo. The relink is skipped for a zombie, whose splice was
skipped and which still sits on the pending chain, and for a node whose parent
is already disposed, whose chain can never be drained again.
disposeChildrennow detachesnode._firstChildbefore its loop and pointseach child's
_prevSiblingat itself, so a node that a cleanup links during aheld-children drain stays on the chain instead of being written back into the
head at its next dormancy.
The creation-time link in
createOwnerandsetupComputedNodeand the newrelink share one
linkChildhelper. Theowner-chain-headinvariant isunchanged and pins the regression. The core minified floor moves 25,671 to
25,724, under the 25,750 cap, and is recorded in the treeshake ledger.
The alternative in the issue, skipping the splice for dormant nodes, was
rejected: it leaves disposed members in the live chain for every walker to
tolerate, retains every dormant lazy memo until its owner disposes, and
contradicts the chain invariant the #3552 test encodes.
Verification
packages/signals/tests/dormant-memo-chain-3554.test.tsadds four cases. Threefail on pristine
nextand all four pass here, under the dev tier and underSIGNALS_TIER=observe. Full suites: signals 3,468 passed, solid 654 passed,web 834 passed.
Left open
Dormant off-chain nodes keep
CONFIG_AUTO_DISPOSEand still recompute aftertheir owner dies, the pre-existing #3024 gap. This change neither widens nor
closes it.
A relinked node moves to the chain head, so sibling disposal order for a
reawakened memo is no longer creation order.