fix: Scan the lifecycle queue without its iterator in dequeueAdd/dequeueRemove - #4021
Merged
Conversation
…eueRemove RecycledQueue only supports a single iterator at a time, but dequeueAdd and dequeueRemove iterated the queue with a plain for loop. Both are reachable from inside processLifecycleEvents (through onMount/onLoad callbacks that remove a queued sibling or re-add a removing component), so the nested iteration reset the outer cursor, an already processed add event survived in the queue and was handled again, and the second mount tripped assert(!isMounted). Route dequeueAdd, dequeueRemove, handleResize and handleHotReload through RecycledQueue.forEachWhere, which walks the storage directly and is safe to call during iteration. Fixes #4018 Claude-Session: https://claude.ai/code/session_01Hqw2qU499wSThwN3EwEDR4
ufrshubham
approved these changes
Aug 26, 2026
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.
Description
RecycledQueueonly supports a single iterator at a time: itsiteratorgetter resets the cursor and runs garbage collection.ComponentTreeRoot.dequeueAddanddequeueRemovescanned the queue with a plainfor (final event in queue), and both are reachable from insideprocessLifecycleEvents:Component._removeChildcallsdequeueAddwhen removing a component that is queued but not yet mounted.Component._addChildcallsdequeueRemovewhen re-adding a component that is currently removing.When user code did either of those from
onMount(oronLoad), the nested iteration clobbered the outer cursor. In thedequeueAddcase the outer loop then calledremoveCurrent()on the wrong element, so the already processedaddevent survived in the queue, was handled a second time and trippedassert(!isMounted)inhandleLifecycleEventAdd. In thedequeueRemovecase the nested scan ran to the end and the outerremoveCurrent()hitCannot remove current element if not iterating.dequeueAdd,dequeueRemove,handleResizeandhandleHotReloadnow go throughRecycledQueue.forEachWhere, which walks the backing storage directly and is safe to call during iteration (it is whatcancelQueuedRemovesalready used for the same reason). Behavior is unchanged:dequeueAddstill cancels only the first matching event and still throws if none is found,dequeueRemovestill cancels all matching events.Two regression tests reproduce both crashes deterministically: one mounts a component whose
onMountremoves a queued sibling, the other mounts a component whoseonMountre-adds a component that is being removed in the same tick.Checklist
docsand added dartdoc comments with///.examplesordocs.Breaking Change?
Related Issues
Closes #4018