fix: do not skip event handlers registered after a once handler - #3476
fix: do not skip event handlers registered after a once handler#3476chuckcarpenter wants to merge 1 commit into
once handler#3476Conversation
`trigger()` spliced `this.bindings[event]` while iterating it with `forEach`. Removing a spent `once` binding shifted every later binding down one, but the loop still advanced, so the handler that moved into the vacated slot was never called. Iterate over a copy and remove `once` bindings by identity rather than by loop index. Removing by index into a snapshot is not enough on its own: the indexes drift once more than one `once` handler is registered for the same event, leaving a spent binding behind to fire again on the next trigger. `off()` had the same splice-during-iteration bug, leaving one binding behind when the same handler was registered more than once. Rewrite it as a filter. Co-authored-by: Brett Ausmeier <brett@decodedev.co.za>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough
ChangesEvent binding safety
Estimated code review effort: 3 (Moderate) | ~20 minutes Mergeability Score: ⚪ Minimal · up to This localized change fixes skipped event handlers and duplicate-handler removal, with unit tests covering the affected cases; no actionable merge-blocking risk remains after normal checks and review. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
shepherd.js/test/unit/evented.spec.js(node:2) ESLintIgnoreWarning: The ".eslintignore" file is no longer supported. Switch to using the "ignores" property in "eslint.config.js": https://eslint.org/docs/latest/use/configure/migration-guide#ignore-files Oops! Something went wrong! :( ESLint: 10.8.1 A config object is using the "root" key, which is not supported in flat config system. Flat configs always act as if they are the root config file, so this key can be safely removed. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (1)
🛟 Help
|

Supersedes #3201.
Event handlers registered after a
oncehandler were silently never called.trigger()splicedthis.bindings[event]while iterating it withforEach, so removing a spentoncebinding shifted every later binding down one while the loop still advanced — the handler that moved into the vacated slot got skipped.@bausmeier reported this in #3201 and proposed iterating over a copy. That fixes the reported case, but not all of them:
indexbecomes an index into the copy whilesplicestill targets the live array, so the indexes drift as soon as more than oneoncehandler is registered for the same event. Threeoncehandlers all fire, but one spent binding survives and fires again on the next trigger.So this removes
oncebindings by identity (indexOf) rather than by loop index.off()had the same splice-during-iteration bug — registering the same handler twice and callingoffonce left one binding behind. Rewritten as a filter.Iterating a snapshot also means a handler removed mid-dispatch still runs if it was registered when the event fired, matching Node's
EventEmitter.Tests
Four new cases in
evented.spec.js, each confirmed to fail onmainand pass here:oncehandler still fires (the originally reported bug)oncehandlers all fire and none are left behindoncehandler fires only on the first of two triggers, while anonhandler fires on bothoff()removes every binding for a handler registered more than onceI also fixed an existing assertion in the
once()block that checkedtoBeTruthy()under the message "custom event removed after one trigger" — it passed only because an empty array is truthy, so it was never testing what it claimed.Full unit suite passes (228 tests), types and lint clean.
Summary by CodeRabbit
Bug Fixes
Tests