Skip to content

fix: prohibit retroactive subscriptions to published events - #9773

Open
ZayanKhan-12 wants to merge 2 commits into
MetaMask:mainfrom
ZayanKhan-12:fix/4700-prohibit-retroactive-subscriptions
Open

fix: prohibit retroactive subscriptions to published events#9773
ZayanKhan-12 wants to merge 2 commits into
MetaMask:mainfrom
ZayanKhan-12:fix/4700-prohibit-retroactive-subscriptions

Conversation

@ZayanKhan-12

@ZayanKhan-12 ZayanKhan-12 commented Aug 4, 2026

Copy link
Copy Markdown

Explanation

When the messenger publishes an event, it iterates over the live subscriber Map via entries(). Because the iterator reflects mutations made while it is being consumed, a handler that calls subscribe for 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:

  • A handler added during publication no longer receives the in-flight event (it receives subsequent events as expected).
  • A handler removed during publication still receives the in-flight event (and none after), consistent with EventEmitter's removeListener semantics: "removeListener() will remove, at most, one instance of a listener... this will not remove them from emit() in progress".

Both paths through #publish are covered, since the public publish method 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

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

🤖 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 that subscribe while an event is being published no longer receive that in-flight publish.

Messenger.#publish now iterates over a snapshot of the subscriber map ([...subscribers.entries()]) instead of the live Map iterator, so subscribe/unsubscribe during dispatch only affects later publishes. Handlers removed mid-publish still run for the current event (aligned with Node EventEmitter).

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.

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>
@ZayanKhan-12
ZayanKhan-12 requested a review from a team as a code owner August 4, 2026 01:04
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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()]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5a6fd94. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[base-controller] ControllerMessenger should prohibit retroactive subscriptions to published events

1 participant