Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/messenger/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Unlike `delegate`, this method requires all external actions and events to be listed, producing a TypeScript error showing exactly which items are missing.
- Add `MessengerNamespace` utility type to extract the namespace from a Messenger type ([#8338](https://github.com/MetaMask/core/pull/8338))

### Fixed

- Prohibit retroactive delivery of published events: a handler subscribed while an event is being published no longer receives the in-flight event ([#9773](https://github.com/MetaMask/core/pull/9773))
- The subscriber collection is now snapshotted before it is iterated during publish, so only handlers registered at the time the event was published are called, matching the behavior of `EventEmitter`. Consequently, a handler unsubscribed while an event is being published still receives the in-flight event.

## [2.0.0]

### Added
Expand Down
58 changes: 58 additions & 0 deletions packages/messenger/src/Messenger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,64 @@ describe('Messenger', () => {
expect(handler2.mock.calls).toHaveLength(1);
});

it('does not publish event to subscriber that was added during publish', () => {
type MessageEvent = { type: 'Fixture:message'; payload: [string] };
const messenger = new Messenger<'Fixture', never, MessageEvent>({
namespace: 'Fixture',
});

const addedDuringPublishHandler = jest.fn();
const handler = jest.fn(() => {
messenger.subscribe('Fixture:message', addedDuringPublishHandler);
});
messenger.subscribe('Fixture:message', handler);
messenger.publish('Fixture:message', 'hello');

expect(handler.mock.calls).toHaveLength(1);
expect(addedDuringPublishHandler).not.toHaveBeenCalled();
});

it('publishes subsequent event to subscriber that was added during publish of an earlier event', () => {
type MessageEvent = { type: 'Fixture:message'; payload: [string] };
const messenger = new Messenger<'Fixture', never, MessageEvent>({
namespace: 'Fixture',
});

const addedDuringPublishHandler = jest.fn();
const handler = jest.fn(() => {
messenger.subscribe('Fixture:message', addedDuringPublishHandler);
});
messenger.subscribe('Fixture:message', handler);
messenger.publish('Fixture:message', 'hello');
messenger.unsubscribe('Fixture:message', handler);
messenger.publish('Fixture:message', 'there');

expect(addedDuringPublishHandler).toHaveBeenCalledWith('there');
expect(addedDuringPublishHandler.mock.calls).toHaveLength(1);
});

it('publishes event to subscriber that was removed during publish of the same event', () => {
type MessageEvent = { type: 'Fixture:message'; payload: [string] };
const messenger = new Messenger<'Fixture', never, MessageEvent>({
namespace: 'Fixture',
});

const removedDuringPublishHandler = jest.fn();
const handler = jest.fn(() => {
messenger.unsubscribe('Fixture:message', removedDuringPublishHandler);
});
messenger.subscribe('Fixture:message', handler);
messenger.subscribe('Fixture:message', removedDuringPublishHandler);
messenger.publish('Fixture:message', 'hello');

expect(removedDuringPublishHandler).toHaveBeenCalledWith('hello');
expect(removedDuringPublishHandler.mock.calls).toHaveLength(1);

messenger.publish('Fixture:message', 'there');

expect(removedDuringPublishHandler.mock.calls).toHaveLength(1);
});

describe('on first state change with an initial payload function registered', () => {
it('publishes event if selected payload differs', () => {
const state = {
Expand Down
7 changes: 6 additions & 1 deletion packages/messenger/src/Messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,12 @@ export class Messenger<
const subscribers = this.#events.get(eventType);

if (subscribers) {
for (const [handler, { selector }] of subscribers.entries()) {
// The subscriber collection is snapshotted before iterating so that
// mutating it during publish (e.g. a handler subscribing or
// 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.

try {
if (selector) {
const previousValue = this.#eventPayloadCache.get(handler);
Expand Down