Snapshot the listeners emit() walks - #2372
Open
enyo wants to merge 1 commit into
Open
Conversation
`emit` iterated `this._callbacks[event]` directly, so a listener that removed itself with `off` -- how a one-shot listener is usually written, and what teardown code does -- spliced the array while the loop was walking it. Everything after the removed entry shifted down past the loop's index, and the next listener was silently skipped. Iterate over a copy instead, the way EventEmitter does. This also means a listener registered from inside another listener first runs on the next emit rather than the current one. Fixes #2367 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||
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 #2367 (the emitter part — see the note at the bottom about the bonus findings).
The bug
emititerates the live array:and
offsplices in place:for…ofover an array walks by index, so removing the entry at the current index shifts everything after it down one and the loop's next step lands past the neighbour. With[a, b, c]andaremoving itself,bnever runs.A listener that unregisters itself is not exotic — it is how you write a one-shot listener, and it is what teardown code does from inside a
completeorqueuecompletehandler.The fix
Iterate a copy, which is what Node's
EventEmitterdoes.Behaviour change worth flagging
The snapshot cuts both ways: a listener registered from inside another listener used to run in that same
emit(the live array grew under the loop) and now first runs on the next one. That is the standard semantics and there is no way to keep the removal case correct without it, but it is a real change, so it is called out in the changeset and covered by a test that pins the new behaviour.Tests
Three, in
test/unit-tests/emitter.js. Againstmain:expected [ 'first', 'third' ] to deeply equal [ 'first', 'second', 'third' ]— the skip, exactly as reportedexpected [ 'first', 'added' ] to deeply equal [ 'first' ]— the behaviour change aboveoff(event)removing everything mid-emit already passed (itdeletes the key rather than mutating the array); it is there as a guard.The bonus findings in #2367
Both are real —
destroy()'ssplice(indexOf(this), 1)evicting a live instance on -1, andinit()comparingtagNameto lower-case"form". They have nothing to do with the emitter, so they are not in this PR. I have them prepared on a branch; theenctypeone is a dormant behaviour change (a natively-submitted form would start encoding asmultipart/form-data), so it seemed worth your call rather than mine.