fix: prohibit retroactive subscriptions to published events - #9773
Open
ZayanKhan-12 wants to merge 2 commits into
Open
fix: prohibit retroactive subscriptions to published events#9773ZayanKhan-12 wants to merge 2 commits into
ZayanKhan-12 wants to merge 2 commits into
Conversation
The messenger iterated over the live subscriber map when publishing an event, so a handler that subscribed during publication would receive the in-flight event even though it was not registered when the event was published. This caused issues such as React render loops when a subscription handler mutated state that re-triggered a subscription. Snapshot the subscriber collection before iterating so that only handlers registered at publish time are called, matching the behavior of EventEmitter. Fixes MetaMask#4700 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 5a6fd94. Configure here.
| // unsubscribing) does not affect which handlers are called for this | ||
| // event. Only the handlers registered at the time the event was | ||
| // published are called, matching the behavior of `EventEmitter`. | ||
| for (const [handler, { selector }] of [...subscribers.entries()]) { |
There was a problem hiding this comment.
Selector cache cleared mid-publish
Medium Severity
Snapshotting subscribers so a handler removed during publish still runs conflicts with unsubscribe clearing #eventPayloadCache immediately. When a later selector handler is removed by an earlier one, it still runs with previousValue as undefined, so it can fire incorrectly, skip a real change to undefined, and leave an orphaned cache entry.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5a6fd94. Configure here.
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.


Explanation
When the messenger publishes an event, it iterates over the live subscriber
Mapviaentries(). Because the iterator reflects mutations made while it is being consumed, a handler that callssubscribefor the same event during publication causes the newly added handler to be invoked with the in-flight event, even though it was not registered when the event was published.This differs from typical event systems (such as Node's
EventEmitter, which snapshots its listener array before emitting), and it has caused real-world bugs — e.g. a React render loop on mobile where a subscription handler mutated a hook dependency that re-triggered the subscription, causing the retroactively added handler to run immediately and crash the app.This PR snapshots the subscriber collection before iterating in
Messenger.#publish, so that only handlers registered at the time the event was published are called:EventEmitter'sremoveListenersemantics: "removeListener() will remove, at most, one instance of a listener... this will not remove them from emit() in progress".Both paths through
#publishare covered, since the publicpublishmethod and the internal delegated publish share the same implementation.Regression tests are added for all three behaviors above. All three failed against the previous implementation.
References
Fixes #4700
Checklist
🤖 Generated with Claude Code
Note
Medium Risk
Changes core event-dispatch semantics across MetaMask core; behavior shifts for code that relied on live-map iteration, but aligns with EventEmitter and fixes real re-entrancy bugs.
Overview
Fixes retroactive event delivery in
@metamask/messenger: handlers thatsubscribewhile an event is being published no longer receive that in-flight publish.Messenger.#publishnow iterates over a snapshot of the subscriber map ([...subscribers.entries()]) instead of the liveMapiterator, so subscribe/unsubscribe during dispatch only affects later publishes. Handlers removed mid-publish still run for the current event (aligned with NodeEventEmitter).Regression tests cover subscribe-during-publish, unsubscribe-during-publish, and later delivery after a mid-publish subscribe. The unreleased changelog documents the fix (Fixes #4700).
Reviewed by Cursor Bugbot for commit 5a6fd94. Bugbot is set up for automated code reviews on this repo. Configure here.