Skip to content

Commit c07ba89

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 c249e17 commit c07ba89

File tree

3 files changed

+156
-26
lines changed

3 files changed

+156
-26
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: 108 additions & 23 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.
@@ -200,6 +222,24 @@ describe('Messenger', () => {
200222
expect(handler.callCount).toBe(1);
201223
});
202224

225+
it('automatically delegates events to parent upon first publish', () => {
226+
type MessageEvent = { type: 'Fixture:message'; payload: [string] };
227+
const parentMessenger = new Messenger<'Parent', never, MessageEvent>({
228+
namespace: 'Parent',
229+
});
230+
const messenger = new Messenger<'Fixture', never, MessageEvent>({
231+
namespace: 'Fixture',
232+
parent: parentMessenger,
233+
});
234+
235+
const handler = sinon.stub();
236+
parentMessenger.subscribe('Fixture:message', handler);
237+
messenger.publish('Fixture:message', 'hello');
238+
239+
expect(handler.calledWithExactly('hello')).toBe(true);
240+
expect(handler.callCount).toBe(1);
241+
});
242+
203243
it('should allow publishing multiple different events to subscriber', () => {
204244
type MessageEvent =
205245
| { type: 'Fixture:message'; payload: [string] }
@@ -488,6 +528,38 @@ describe('Messenger', () => {
488528
});
489529
});
490530

531+
it('automatically delegates to parent when an initial payload is registered', () => {
532+
const state = {
533+
propA: 1,
534+
propB: 1,
535+
};
536+
type MessageEvent = {
537+
type: 'Fixture:complexMessage';
538+
payload: [typeof state];
539+
};
540+
const parentMessenger = new Messenger<'Parent', never, MessageEvent>({
541+
namespace: 'Parent',
542+
});
543+
const messenger = new Messenger<'Fixture', never, MessageEvent>({
544+
namespace: 'Fixture',
545+
parent: parentMessenger,
546+
});
547+
const handler = sinon.stub();
548+
549+
messenger.registerInitialEventPayload({
550+
eventType: 'Fixture:complexMessage',
551+
getPayload: () => [state],
552+
});
553+
554+
parentMessenger.subscribe('Fixture:complexMessage', handler, (obj) => obj.propA);
555+
messenger.publish('Fixture:complexMessage', state);
556+
expect(handler.callCount).toBe(0);
557+
state.propA += 1;
558+
messenger.publish('Fixture:complexMessage', state);
559+
expect(handler.getCall(0)?.args).toStrictEqual([2, 1]);
560+
expect(handler.callCount).toBe(1);
561+
});
562+
491563
it('should publish event to many subscribers with the same selector', () => {
492564
type MessageEvent = {
493565
type: 'Fixture:complexMessage';
@@ -679,7 +751,7 @@ describe('Messenger', () => {
679751
});
680752

681753
class TestService {
682-
name = 'TestService' as const;
754+
name = 'TestService';
683755

684756
getType() {
685757
return 'api';
@@ -712,7 +784,7 @@ describe('Messenger', () => {
712784
});
713785

714786
class TestService {
715-
name = 'TestService' as const;
787+
name = 'TestService';
716788

717789
privateValue = 'secret';
718790

@@ -738,7 +810,7 @@ describe('Messenger', () => {
738810
});
739811

740812
class TestService {
741-
name = 'TestService' as const;
813+
name = 'TestService';
742814

743815
async fetchData(id: string) {
744816
return `data-${id}`;
@@ -754,12 +826,12 @@ describe('Messenger', () => {
754826

755827
it('should not throw when given an empty methodNames array', () => {
756828
type TestAction = { type: 'TestController:test'; handler: () => void };
757-
const messenger = new Messenger<'TestController', TestAction, never>({
758-
namespace: 'TestController',
829+
const messenger = new Messenger<'TestService', TestAction, never>({
830+
namespace: 'TestService',
759831
});
760832

761833
class TestController {
762-
name = 'TestController' as const;
834+
name = 'TestController';
763835
}
764836

765837
const controller = new TestController();
@@ -778,12 +850,12 @@ describe('Messenger', () => {
778850
type: 'TestController:getValue';
779851
handler: () => string;
780852
};
781-
const messenger = new Messenger<'TestController', TestAction, never>({
782-
namespace: 'TestController',
853+
const messenger = new Messenger<'TestService', TestAction, never>({
854+
namespace: 'TestService',
783855
});
784856

785857
class TestController {
786-
name = 'TestController' as const;
858+
name = 'TestController';
787859

788860
readonly nonFunction = 'not a function';
789861

@@ -812,28 +884,20 @@ describe('Messenger', () => {
812884
| { type: 'ChildController:baseMethod'; handler: () => string }
813885
| { type: 'ChildController:childMethod'; handler: () => string };
814886

815-
const messenger = new Messenger<'ChildController', TestActions, never>({
816-
namespace: 'ChildController',
887+
const messenger = new Messenger<'TestService', TestActions, never>({
888+
namespace: 'TestService',
817889
});
818890

819-
class BaseController<Namespace extends string> {
820-
name: Namespace;
821-
822-
constructor({ namespace }: { namespace: Namespace }) {
823-
this.name = namespace;
824-
}
891+
class BaseController {
892+
name = 'BaseController';
825893

826894
baseMethod() {
827895
return 'base method';
828896
}
829897
}
830898

831-
class ChildController extends BaseController<'ChildController'> {
832-
name = 'ChildController' as const;
833-
834-
constructor() {
835-
super({ namespace: 'ChildController' });
836-
}
899+
class ChildController extends BaseController {
900+
name = 'ChildController';
837901

838902
childMethod() {
839903
return 'child method';
@@ -1055,6 +1119,27 @@ describe('Messenger', () => {
10551119
});
10561120

10571121
describe('revoke', () => {
1122+
it('throws when attempting to revoke from parent', () => {
1123+
type ExampleEvent = {
1124+
type: 'Source:event';
1125+
payload: ['test'];
1126+
};
1127+
const parentMessenger = new Messenger<'Parent', never, ExampleEvent>({
1128+
namespace: 'Parent',
1129+
});
1130+
const sourceMessenger = new Messenger<'Source', never, ExampleEvent>({
1131+
namespace: 'Source',
1132+
parent: parentMessenger,
1133+
});
1134+
1135+
expect(() =>
1136+
sourceMessenger.revoke({
1137+
messenger: parentMessenger,
1138+
events: ['Source:event'],
1139+
}),
1140+
).toThrow('Cannot revoke from parent');
1141+
});
1142+
10581143
it('allows revoking a delegated event', () => {
10591144
type ExampleEvent = {
10601145
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
/**
@@ -379,6 +407,12 @@ export class Messenger<
379407
}:'`,
380408
);
381409
}
410+
if (
411+
this.#parent &&
412+
!this.#subscriptionDelegationTargets.get(eventType)?.has(this.#parent)
413+
) {
414+
this.delegate({ events: [eventType], messenger: this.#parent });
415+
}
382416
this.#registerInitialEventPayload({ eventType, getPayload });
383417
}
384418

@@ -445,6 +479,12 @@ export class Messenger<
445479
`Only allowed publishing events prefixed by '${this.#namespace}:'`,
446480
);
447481
}
482+
if (
483+
this.#parent &&
484+
!this.#subscriptionDelegationTargets.get(eventType)?.has(this.#parent)
485+
) {
486+
this.delegate({ events: [eventType], messenger: this.#parent });
487+
}
448488
this.#publish(eventType, ...payload);
449489
}
450490

@@ -708,6 +748,9 @@ export class Messenger<
708748
events?: readonly DelegatedEvent['type'][];
709749
messenger: DelegatedMessenger<DelegatedAction, DelegatedEvent>;
710750
}) {
751+
if (messenger === this.#parent) {
752+
throw new Error('Cannot revoke from parent');
753+
}
711754
for (const actionType of actions) {
712755
const delegationTargets = this.#actionDelegationTargets.get(actionType);
713756
if (!delegationTargets || !delegationTargets.has(messenger)) {

0 commit comments

Comments
 (0)