Skip to content

Commit 4ff719e

Browse files
committed
feat: Add Messenger parant parameter
Add the `parent` constructor parameter to the `Messenger` class for automatic delegation of all capabilities under that messenger's namespace. This significantly reduces boilerplate, making the messenger easier to use in a similar manner to how the old `RestrictedMessenger` class wored. See this ADR PR for details: MetaMask/decisions#53
1 parent fbeff96 commit 4ff719e

File tree

3 files changed

+145
-3
lines changed

3 files changed

+145
-3
lines changed

packages/messenger/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- These allow delegating or revoking capabilities (actions or events) from one `Messenger` instance to another.
1515
- This allows passing capabilities through chains of messengers of arbitrary length
1616
- See this ADR for details: https://github.com/MetaMask/decisions/blob/main/decisions/core/0012-messenger-delegation.md
17+
- Add `parent` constructor parameter to `Messenger` ([#6142](https://github.com/MetaMask/core/pull/6142))
18+
- All capabilities registered under this messenger's namespace are delegated to the parent automatically. This is similar to how the `RestrictedMessenger` would automatically delegate all capabilities to the messenger it was created from.
1719

1820
### Changed
1921

2022
- **BREAKING:** Add `Namespace` type parameter and required `namespace` constructor parameter ([#6132](https://github.com/MetaMask/core/pull/6132))
2123
- All published events and registered actions should fall under the given namespace. Typically the namespace is the controller or service name. This is the equivalent to the `Namespace` parameter from the old `RestrictedMessenger` class.
2224
- **BREAKING:** Remove `RestrictedMessenger` class ([#6132](https://github.com/MetaMask/core/pull/6132))
23-
- Existing `RestrictedMessenger` instances should be replaced with a `Messenger`. We can now use the same class everywhere, passing capabilities using `delegate`.
25+
- Existing `RestrictedMessenger` instances should be replaced with a `Messenger` with the `parent` constructor parameter set to the global messenger. We can now use the same class everywhere, passing capabilities using `delegate`.
2426
- See this ADR for details: https://github.com/MetaMask/decisions/blob/main/decisions/core/0012-messenger-delegation.md
2527

2628
[Unreleased]: https://github.com/MetaMask/core/

packages/messenger/src/Messenger.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@ describe('Messenger', () => {
2626
expect(count).toBe(1);
2727
});
2828

29+
it('automatically delegates actions to parent upon registration', () => {
30+
type CountAction = {
31+
type: 'Fixture:count';
32+
handler: (increment: number) => void;
33+
};
34+
const parentMessenger = new Messenger<'Parent', CountAction, never>({
35+
namespace: 'Parent',
36+
});
37+
const messenger = new Messenger<'Fixture', CountAction, never>({
38+
namespace: 'Fixture',
39+
parent: parentMessenger,
40+
});
41+
42+
let count = 0;
43+
messenger.registerActionHandler('Fixture:count', (increment: number) => {
44+
count += increment;
45+
});
46+
parentMessenger.call('Fixture:count', 1);
47+
48+
expect(count).toBe(1);
49+
});
50+
2951
it('should allow registering and calling multiple different action handlers', () => {
3052
// These 'Other' types are included to demonstrate that messenger generics can indeed be unions
3153
// of actions and events from different modules.
@@ -225,6 +247,24 @@ describe('Messenger', () => {
225247
expect(handler.callCount).toBe(1);
226248
});
227249

250+
it('automatically delegates events to parent upon first publish', () => {
251+
type MessageEvent = { type: 'Fixture:message'; payload: [string] };
252+
const parentMessenger = new Messenger<'Parent', never, MessageEvent>({
253+
namespace: 'Parent',
254+
});
255+
const messenger = new Messenger<'Fixture', never, MessageEvent>({
256+
namespace: 'Fixture',
257+
parent: parentMessenger,
258+
});
259+
260+
const handler = sinon.stub();
261+
parentMessenger.subscribe('Fixture:message', handler);
262+
messenger.publish('Fixture:message', 'hello');
263+
264+
expect(handler.calledWithExactly('hello')).toBe(true);
265+
expect(handler.callCount).toBe(1);
266+
});
267+
228268
it('should allow publishing multiple different events to subscriber', () => {
229269
type MessageEvent =
230270
| { type: 'Fixture:message'; payload: [string] }
@@ -513,6 +553,42 @@ describe('Messenger', () => {
513553
});
514554
});
515555

556+
it('automatically delegates to parent when an initial payload is registered', () => {
557+
const state = {
558+
propA: 1,
559+
propB: 1,
560+
};
561+
type MessageEvent = {
562+
type: 'Fixture:complexMessage';
563+
payload: [typeof state];
564+
};
565+
const parentMessenger = new Messenger<'Parent', never, MessageEvent>({
566+
namespace: 'Parent',
567+
});
568+
const messenger = new Messenger<'Fixture', never, MessageEvent>({
569+
namespace: 'Fixture',
570+
parent: parentMessenger,
571+
});
572+
const handler = sinon.stub();
573+
574+
messenger.registerInitialEventPayload({
575+
eventType: 'Fixture:complexMessage',
576+
getPayload: () => [state],
577+
});
578+
579+
parentMessenger.subscribe(
580+
'Fixture:complexMessage',
581+
handler,
582+
(obj) => obj.propA,
583+
);
584+
messenger.publish('Fixture:complexMessage', state);
585+
expect(handler.callCount).toBe(0);
586+
state.propA += 1;
587+
messenger.publish('Fixture:complexMessage', state);
588+
expect(handler.getCall(0)?.args).toStrictEqual([2, 1]);
589+
expect(handler.callCount).toBe(1);
590+
});
591+
516592
it('should publish event to many subscribers with the same selector', () => {
517593
type MessageEvent = {
518594
type: 'Fixture:complexMessage';
@@ -1080,6 +1156,27 @@ describe('Messenger', () => {
10801156
});
10811157

10821158
describe('revoke', () => {
1159+
it('throws when attempting to revoke from parent', () => {
1160+
type ExampleEvent = {
1161+
type: 'Source:event';
1162+
payload: ['test'];
1163+
};
1164+
const parentMessenger = new Messenger<'Parent', never, ExampleEvent>({
1165+
namespace: 'Parent',
1166+
});
1167+
const sourceMessenger = new Messenger<'Source', never, ExampleEvent>({
1168+
namespace: 'Source',
1169+
parent: parentMessenger,
1170+
});
1171+
1172+
expect(() =>
1173+
sourceMessenger.revoke({
1174+
messenger: parentMessenger,
1175+
events: ['Source:event'],
1176+
}),
1177+
).toThrow('Cannot revoke from parent');
1178+
});
1179+
10831180
it('allows revoking a delegated event', () => {
10841181
type ExampleEvent = {
10851182
type: 'Source:event';

packages/messenger/src/Messenger.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,22 @@ export class Messenger<
134134
> {
135135
readonly #namespace: Namespace;
136136

137+
/**
138+
* The parent messenger. All actions/events under this namespace are automatically delegated to
139+
* the parent messenger.
140+
*/
141+
readonly #parent?: Messenger<
142+
string,
143+
Action | ActionConstraint,
144+
Event | EventConstraint
145+
>;
146+
137147
readonly #actions = new Map<Action['type'], Action['handler']>();
138148

139149
readonly #events = new Map<Event['type'], EventSubscriptionMap<Event>>();
140150

141151
/**
142-
* The set of messengers we've delegated events to and their event handlers, by event type.
152+
* The set of messengers we've delegated events to, by event type.
143153
*/
144154
readonly #subscriptionDelegationTargets = new Map<
145155
Event['type'],
@@ -178,11 +188,26 @@ export class Messenger<
178188
/**
179189
* Construct a messenger.
180190
*
191+
* If a parent messenger is given, all actions and events under this messenger's namespace will
192+
* be delegated to the parent automatically.
193+
*
181194
* @param args - Constructor arguments
182195
* @param args.namespace - The messenger namespace.
196+
* @param args.parent - The parent messenger.
183197
*/
184-
constructor({ namespace }: { namespace: Namespace }) {
198+
constructor({
199+
namespace,
200+
parent,
201+
}: {
202+
namespace: Namespace;
203+
parent?: Messenger<
204+
string,
205+
Action | ActionConstraint,
206+
Event | EventConstraint
207+
>;
208+
}) {
185209
this.#namespace = namespace;
210+
this.#parent = parent;
186211
}
187212

188213
/**
@@ -208,6 +233,9 @@ export class Messenger<
208233
);
209234
}
210235
this.#registerActionHandler(actionType, handler);
236+
if (this.#parent) {
237+
this.delegate({ actions: [actionType], messenger: this.#parent });
238+
}
211239
}
212240

213241
/**
@@ -381,6 +409,12 @@ export class Messenger<
381409
}:'`,
382410
);
383411
}
412+
if (
413+
this.#parent &&
414+
!this.#subscriptionDelegationTargets.get(eventType)?.has(this.#parent)
415+
) {
416+
this.delegate({ events: [eventType], messenger: this.#parent });
417+
}
384418
this.#registerInitialEventPayload({ eventType, getPayload });
385419
}
386420

@@ -447,6 +481,12 @@ export class Messenger<
447481
`Only allowed publishing events prefixed by '${this.#namespace}:'`,
448482
);
449483
}
484+
if (
485+
this.#parent &&
486+
!this.#subscriptionDelegationTargets.get(eventType)?.has(this.#parent)
487+
) {
488+
this.delegate({ events: [eventType], messenger: this.#parent });
489+
}
450490
this.#publish(eventType, ...payload);
451491
}
452492

@@ -710,6 +750,9 @@ export class Messenger<
710750
events?: readonly DelegatedEvent['type'][];
711751
messenger: DelegatedMessenger<DelegatedAction, DelegatedEvent>;
712752
}) {
753+
if (messenger === this.#parent) {
754+
throw new Error('Cannot revoke from parent');
755+
}
713756
for (const actionType of actions) {
714757
const delegationTargets = this.#actionDelegationTargets.get(actionType);
715758
if (!delegationTargets || !delegationTargets.has(messenger)) {

0 commit comments

Comments
 (0)