Skip to content

Commit a607066

Browse files
Gudahttmcmiremikesposito
authored
feat: Migrate controller guidelines and examples to new Messenger (#6335)
## Explanation Migrate the controller guidelines and the `sample-controllers` package to the `next` export of the `@metamask/base-controller` package and the new `Messenger` class from `@metamask/messenger`. ## References Relates to #5626 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Migrate controller docs and sample-controllers to the new @metamask/messenger Messenger and @metamask/base-controller/next, updating APIs, metadata, tests, and dependencies. > > - **Docs** > - Rewrite guidelines and data-services to use `@metamask/messenger` `Messenger` instead of `RestrictedMessenger`; update examples for actions/events typing, root/child messenger setup, delegation, and selector utilities (`MessengerActions`/`MessengerEvents`). > - Replace references to “messaging system” with “messenger”; update code samples to `messenger.call/subscribe/publish/registerMethodActionHandlers`. > - **Sample Controllers** (`@metamask/sample-controllers`) > - Migrate to `@metamask/base-controller/next` and `@metamask/messenger`. > - Replace `messagingSystem.*` with `messenger.*`; update controller/service messenger types to `Messenger<...>` and constructor usage. > - Update state metadata (`anonymous` → `includeInDebugSnapshot`). > - Revise tests to use new `Messenger`, mock namespace utilities, and `MessengerActions/MessengerEvents`. > - Add dependency on `@metamask/messenger`; update TS project references. > - Changelog: mark BREAKING changes for Messenger migration and metadata rename. > - **Repo/README** > - Dependency graph: add `sample_controllers --> messenger`; add middleware/controller edge; list `@metamask/messenger` package. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c1ad7a4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Elliot Winkler <[email protected]> Co-authored-by: Michele Esposito <[email protected]>
1 parent 08302be commit a607066

14 files changed

+348
-309
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ linkStyle default opacity:0.5
333333
remote_feature_flag_controller --> controller_utils;
334334
remote_feature_flag_controller --> messenger;
335335
sample_controllers --> base_controller;
336+
sample_controllers --> messenger;
336337
sample_controllers --> controller_utils;
337338
sample_controllers --> network_controller;
338339
seedless_onboarding_controller --> base_controller;

docs/controller-guidelines.md

Lines changed: 158 additions & 178 deletions
Large diffs are not rendered by default.

docs/data-services.md

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
## What is a data service?
44

5-
A **data service** is a pattern for making interactions with an external API (fetching token prices, storing accounts, etc.). It is implemented as a plain TypeScript class with methods that are exposed through the messaging system.
5+
A **data service** is a pattern for making interactions with an external API (fetching token prices, storing accounts, etc.). It is implemented as a plain TypeScript class with methods that are exposed through a messenger.
66

77
## Why use this pattern?
88

99
If you want to talk to an API, it might be tempting to define a method in the controller or a function in a separate file. However, implementing the data service pattern is advantageous for the following reasons:
1010

1111
1. The pattern provides an abstraction that allows for implementing and reusing strategies that are common when working with external APIs, such as batching, automatic retries with exponential backoff, etc.
12-
2. By integrating with the messaging system, other parts of the application can make use of the data service without needing to go through the controller, or in fact, without needing a reference to the data service at all.
12+
2. By integrating with a messenger, other parts of the application can make use of the data service without needing to go through the controller, or in fact, without needing a reference to the data service at all.
1313

1414
## How to create a data service
1515

@@ -78,7 +78,7 @@ Next we'll define the messenger. We give the messenger a namespace, and we expos
7878
```typescript
7979
// (top of file)
8080

81-
import type { RestrictedMessenger } from '@metamask/base-controller';
81+
import type { Messenger } from '@metamask/messenger';
8282

8383
const SERVICE_NAME = 'GasPricesService';
8484

@@ -95,21 +95,19 @@ export type GasPricesServiceEvents = never;
9595

9696
type AllowedEvents = never;
9797

98-
export type GasPricesServiceMessenger = RestrictedMessenger<
98+
export type GasPricesServiceMessenger = Messenger<
9999
typeof SERVICE_NAME,
100100
GasPricesServiceActions | AllowedActions,
101-
GasPricesServiceEvents | AllowedEvents,
102-
AllowedActions['type'],
103-
AllowedEvents['type']
101+
GasPricesServiceEvents | AllowedEvents
104102
>;
105103

106104
// ...
107105
```
108106

109-
Note that we need to add `@metamask/base-controller` as a direct dependency of the package to bring in the `RestrictedMessenger` type (here we assume that our package is called `@metamask/gas-prices-controller`):
107+
Note that we need to add `@metamask/messenger` as a direct dependency of the package to bring in the `Messenger` type (here we assume that our package is called `@metamask/gas-prices-controller`):
110108

111109
```shell
112-
yarn workspace @metamask/gas-prices-controller add @metamask/base-controller
110+
yarn workspace @metamask/gas-prices-controller add @metamask/messenger
113111
```
114112

115113
Finally we will register the method as an action handler on the messenger:
@@ -145,7 +143,7 @@ export class GasPricesService {
145143
<details><summary><b>View whole file</b></summary><br />
146144
147145
```typescript
148-
import type { RestrictedMessenger } from '@metamask/base-controller';
146+
import type { Messenger } from '@metamask/messenger';
149147

150148
const SERVICE_NAME = 'GasPricesService';
151149

@@ -162,12 +160,10 @@ export type GasPricesServiceEvents = never;
162160

163161
type AllowedEvents = never;
164162

165-
export type GasPricesServiceMessenger = RestrictedMessenger<
163+
export type GasPricesServiceMessenger = Messenger<
166164
typeof SERVICE_NAME,
167165
GasPricesServiceActions | AllowedActions,
168-
GasPricesServiceEvents | AllowedEvents,
169-
AllowedActions['type'],
170-
AllowedEvents['type']
166+
GasPricesServiceEvents | AllowedEvents
171167
>;
172168

173169
type GasPricesResponse = {
@@ -272,10 +268,12 @@ import { Messenger } from '@metamask/base-controller';
272268
// ...
273269

274270
function buildMessenger(): GasPricesServiceMessenger {
275-
return new Messenger().getRestricted({
276-
name: 'GasPricesService',
277-
allowedActions: [],
278-
allowedEvents: [],
271+
return new Messenger<
272+
'GasPricesService',
273+
GasPricesServiceActions,
274+
GasPricesServiceEvents
275+
>({
276+
namespace: 'GasPricesService',
279277
});
280278
}
281279
```
@@ -321,7 +319,11 @@ describe('GasPricesService', () => {
321319
```typescript
322320
import nock from 'nock';
323321

324-
import type { GasPricesServiceMessenger } from './gas-prices-service';
322+
import type {
323+
GasPricesServiceMessenger,
324+
GasPricesServiceActions,
325+
GasPricesServiceEvents,
326+
} from './gas-prices-service';
325327
import { GasPricesService } from './gas-prices-service';
326328

327329
describe('GasPricesService', () => {
@@ -375,10 +377,12 @@ describe('GasPricesService', () => {
375377
});
376378

377379
function buildMessenger(): GasPricesServiceMessenger {
378-
return new Messenger().getRestricted({
379-
name: 'GasPricesService',
380-
allowedActions: [],
381-
allowedEvents: [],
380+
return new Messenger<
381+
'GasPricesService',
382+
GasPricesServiceActions,
383+
GasPricesServiceEvents
384+
>({
385+
namespace: 'GasPricesService',
382386
});
383387
}
384388
```
@@ -387,30 +391,37 @@ function buildMessenger(): GasPricesServiceMessenger {
387391
388392
## How to use a data service
389393
390-
Let's say that we wanted to use our data service that we built above. To do this, we will instantiate the messenger for the data service — which itself relies on a global messenger — and then the data service itself.
394+
Let's say that we wanted to use our data service that we built above. To do this, we will instantiate the messenger for the data service — which itself relies on a root messenger — and then the data service itself.
391395
392396
First we need to import the data service:
393397
394398
```typescript
395399
import { GasPricesService } from '@metamask/gas-prices-service';
396400
```
397401
398-
Then we create a global messenger:
402+
Then we create a root messenger:
399403
400404
```typescript
401-
const globalMessenger = new Messenger();
405+
const rootMessenger = new Messenger<'Root', AllActions, AllEvents>({
406+
namespace: 'Root',
407+
});
402408
```
403409
404410
Then we create a messenger for the GasPricesService:
405411
406412
```typescript
407-
const gasPricesServiceMessenger = globalMessenger.getRestricted({
408-
allowedActions: [],
409-
allowedEvents: [],
413+
const gasPricesServiceMessenger = new Messenger<
414+
'GasPricesService',
415+
GasPricesServiceActions,
416+
GasPricesServiceEvents,
417+
typeof rootMessenger
418+
>({
419+
namespace: 'GasPricesService',
420+
parent: rootMessenger,
410421
});
411422
```
412423
413-
Now we instantiate the data service to register the action handler on the global messenger. We assume we have a global `fetch` function available:
424+
Now we instantiate the data service to register the action handler on the root messenger. We assume we have a global `fetch` function available:
414425
415426
```typescript
416427
const gasPricesService = new GasPricesService({
@@ -421,7 +432,7 @@ const gasPricesService = new GasPricesService({
421432
422433
Great! Now that we've set up the data service and its messenger action, we can use it somewhere else.
423434
424-
Let's say we wanted to use it in a controller. We'd just need to allow that controller's messenger access to `GasPricesService:fetchGasPrices` by passing it via the `allowedActions` option.
435+
Let's say we wanted to use `GasPricesService:fetchGasPrices` in a controller. First, that controller's messenger would need to include `GasPricesService:fetchGasPrices` in its type defintion.
425436
426437
This code would probably be in the controller package itself. For instance, if we had a file `packages/send-controller/send-controller.ts`, we might have:
427438
@@ -436,23 +447,30 @@ type SendControllerEvents = ...;
436447

437448
type AllowedEvents = ...;
438449

439-
type SendControllerMessenger = RestrictedMessenger<
450+
type SendControllerMessenger = Messenger<
440451
'SendController',
441452
SendControllerActions | AllowedActions,
442453
SendControllerEvents | AllowedEvents,
443-
AllowedActions['type'],
444-
AllowedEvents['type']
445454
>;
446455
```
447456
457+
Then we'll need to allow that controller's messenger access to `GasPricesService:fetchGasPrices` by delegating it from the root messenger:
458+
459+
```typescript
460+
rootMessenger.delegate({
461+
actions: ['GasPricesService:fetchGasPrices'],
462+
messenger: sendControllerMessenger,
463+
});
464+
```
465+
448466
Then, later on in our controller, we could say:
449467
450468
```typescript
451469
class SendController extends BaseController {
452470
// ...
453471

454472
await someMethodThatUsesGasPrices() {
455-
const gasPrices = await this.#messagingSystem.call(
473+
const gasPrices = await this.messenger.call(
456474
'GasPricesService:fetchGasPrices',
457475
);
458476
// ... use gasPrices somehow ...

packages/sample-controllers/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **BREAKING:** Migrate to new `Messenger` class ([#6335](https://github.com/MetaMask/core/pull/6335))
13+
- **BREAKING:** Rename metadata property `anonymous` to `includeInDebugSnapshot` ([#6335](https://github.com/MetaMask/core/pull/6335))
14+
1015
## [2.0.2]
1116

1217
### Changed

packages/sample-controllers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
},
4949
"dependencies": {
5050
"@metamask/base-controller": "^8.4.2",
51+
"@metamask/messenger": "^0.3.0",
5152
"@metamask/utils": "^11.8.1"
5253
},
5354
"devDependencies": {

packages/sample-controllers/src/sample-gas-prices-controller.test.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { Messenger, deriveStateFromMetadata } from '@metamask/base-controller';
1+
import { deriveStateFromMetadata } from '@metamask/base-controller/next';
2+
import {
3+
Messenger,
4+
MOCK_ANY_NAMESPACE,
5+
type MockAnyNamespace,
6+
type MessengerActions,
7+
type MessengerEvents,
8+
} from '@metamask/messenger';
29
import { SampleGasPricesController } from '@metamask/sample-controllers';
310
import type { SampleGasPricesControllerMessenger } from '@metamask/sample-controllers';
411

512
import { flushPromises } from '../../../tests/helpers';
6-
import type {
7-
ExtractAvailableAction,
8-
ExtractAvailableEvent,
9-
} from '../../base-controller/tests/helpers';
1013
import { buildMockGetNetworkClientById } from '../../network-controller/tests/helpers';
1114

1215
describe('SampleGasPricesController', () => {
@@ -301,7 +304,7 @@ describe('SampleGasPricesController', () => {
301304
deriveStateFromMetadata(
302305
controller.state,
303306
controller.metadata,
304-
'anonymous',
307+
'includeInDebugSnapshot',
305308
),
306309
).toMatchInlineSnapshot(`Object {}`);
307310
});
@@ -362,8 +365,9 @@ describe('SampleGasPricesController', () => {
362365
* required by the controller under test.
363366
*/
364367
type RootMessenger = Messenger<
365-
ExtractAvailableAction<SampleGasPricesControllerMessenger>,
366-
ExtractAvailableEvent<SampleGasPricesControllerMessenger>
368+
MockAnyNamespace,
369+
MessengerActions<SampleGasPricesControllerMessenger>,
370+
MessengerEvents<SampleGasPricesControllerMessenger>
367371
>;
368372

369373
/**
@@ -389,7 +393,7 @@ type WithControllerOptions = {
389393
* @returns The root messenger.
390394
*/
391395
function getRootMessenger(): RootMessenger {
392-
return new Messenger();
396+
return new Messenger({ namespace: MOCK_ANY_NAMESPACE });
393397
}
394398

395399
/**
@@ -402,14 +406,19 @@ function getRootMessenger(): RootMessenger {
402406
function getMessenger(
403407
rootMessenger: RootMessenger,
404408
): SampleGasPricesControllerMessenger {
405-
return rootMessenger.getRestricted({
406-
name: 'SampleGasPricesController',
407-
allowedActions: [
408-
'SampleGasPricesService:fetchGasPrices',
409+
const messenger: SampleGasPricesControllerMessenger = new Messenger({
410+
namespace: 'SampleGasPricesController',
411+
parent: rootMessenger,
412+
});
413+
rootMessenger.delegate({
414+
actions: [
409415
'NetworkController:getNetworkClientById',
416+
'SampleGasPricesService:fetchGasPrices',
410417
],
411-
allowedEvents: ['NetworkController:stateChange'],
418+
events: ['NetworkController:stateChange'],
419+
messenger,
412420
});
421+
return messenger;
413422
}
414423

415424
/**

0 commit comments

Comments
 (0)