diff --git a/packages/composable-controller/package.json b/packages/composable-controller/package.json index 9e185a2fd65..e2d8b1710dd 100644 --- a/packages/composable-controller/package.json +++ b/packages/composable-controller/package.json @@ -47,7 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^8.4.0" + "@metamask/base-controller": "^8.4.0", + "@metamask/messenger": "^0.3.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/composable-controller/src/ComposableController.test.ts b/packages/composable-controller/src/ComposableController.test.ts index ca4b15d8c84..6058e7287c7 100644 --- a/packages/composable-controller/src/ComposableController.test.ts +++ b/packages/composable-controller/src/ComposableController.test.ts @@ -1,15 +1,24 @@ -import type { RestrictedMessenger } from '@metamask/base-controller'; import { BaseController, - Messenger, + type ControllerStateChangeEvent, + type ControllerGetStateAction, + type StateConstraint, deriveStateFromMetadata, -} from '@metamask/base-controller'; +} from '@metamask/base-controller/next'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + MOCK_ANY_NAMESPACE, + Messenger, + type MessengerActions, + type MessengerEvents, + type MockAnyNamespace, +} from '@metamask/messenger'; import type { Patch } from 'immer'; import * as sinon from 'sinon'; import type { ChildControllerStateChangeEvents, + ComposableControllerActions, ComposableControllerEvents, } from './ComposableController'; import { @@ -19,20 +28,29 @@ import { // Mock BaseController classes +type RootMessenger = Messenger< + MockAnyNamespace, + MessengerActions | MessengerActions, + MessengerEvents | MessengerEvents +>; + type FooControllerState = { foo: string; }; +type FooControllerAction = ControllerGetStateAction< + 'FooController', + FooControllerState +>; type FooControllerEvent = { type: `FooController:stateChange`; payload: [FooControllerState, Patch[]]; }; -type FooMessenger = RestrictedMessenger< +type FooMessenger = Messenger< 'FooController', - never, + FooControllerAction, FooControllerEvent | QuzControllerEvent, - never, - QuzControllerEvent['type'] + RootMessenger >; const fooControllerStateMetadata = { @@ -66,17 +84,20 @@ class FooController extends BaseController< type QuzControllerState = { quz: string; }; +type QuzControllerAction = ControllerGetStateAction< + 'QuzController', + QuzControllerState +>; type QuzControllerEvent = { type: `QuzController:stateChange`; payload: [QuzControllerState, Patch[]]; }; -type QuzMessenger = RestrictedMessenger< +type QuzMessenger = Messenger< 'QuzController', - never, + QuzControllerAction, QuzControllerEvent, - never, - never + RootMessenger >; const quzControllerStateMetadata = { @@ -107,50 +128,17 @@ class QuzController extends BaseController< } } -type ControllerWithoutStateChangeEventState = { - qux: string; -}; - -type ControllerWithoutStateChangeEventMessenger = RestrictedMessenger< - 'ControllerWithoutStateChangeEvent', - never, - QuzControllerEvent, - never, - QuzControllerEvent['type'] +type ComposableControllerMessenger = Messenger< + 'ComposableController', + ControllerGetStateAction<'ComposableController', State>, + | ControllerStateChangeEvent<'ComposableController', State> + | FooControllerEvent, + RootMessenger >; -const controllerWithoutStateChangeEventStateMetadata = { - qux: { - persist: true, - anonymous: true, - }, -}; - -class ControllerWithoutStateChangeEvent extends BaseController< - 'ControllerWithoutStateChangeEvent', - ControllerWithoutStateChangeEventState, - ControllerWithoutStateChangeEventMessenger -> { - constructor(messagingSystem: ControllerWithoutStateChangeEventMessenger) { - super({ - messenger: messagingSystem, - metadata: controllerWithoutStateChangeEventStateMetadata, - name: 'ControllerWithoutStateChangeEvent', - state: { qux: 'qux' }, - }); - } - - updateState(qux: string) { - super.update((state) => { - state.qux = qux; - }); - } -} - type ControllersMap = { FooController: FooController; QuzController: QuzController; - ControllerWithoutStateChangeEvent: ControllerWithoutStateChangeEvent; }; describe('ComposableController', () => { @@ -161,39 +149,39 @@ describe('ComposableController', () => { describe('BaseController', () => { it('should compose controller state', () => { type ComposableControllerState = { - FooController: FooControllerState; QuzController: QuzControllerState; + FooController: FooControllerState; }; - const messenger = new Messenger< - never, - | ComposableControllerEvents - | FooControllerEvent - | QuzControllerEvent - >(); - const fooMessenger = messenger.getRestricted< - 'FooController', - never, - QuzControllerEvent['type'] - >({ - name: 'FooController', - allowedActions: [], - allowedEvents: ['QuzController:stateChange'], + const messenger: RootMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, }); - const quzMessenger = messenger.getRestricted({ - name: 'QuzController', - allowedActions: [], - allowedEvents: [], + const fooMessenger: FooMessenger = new Messenger({ + namespace: 'FooController', + parent: messenger, + }); + messenger.delegate({ + messenger: fooMessenger, + events: ['QuzController:stateChange'], + }); + const quzMessenger: QuzMessenger = new Messenger({ + namespace: 'QuzController', + parent: messenger, }); const fooController = new FooController(fooMessenger); const quzController = new QuzController(quzMessenger); - const composableControllerMessenger = messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: [ - 'FooController:stateChange', - 'QuzController:stateChange', - ], + const composableControllerMessenger = new Messenger< + 'ComposableController', + never, + FooControllerEvent | QuzControllerEvent, + RootMessenger + >({ + namespace: 'ComposableController', + parent: messenger, + }); + composableControllerMessenger.delegate({ + messenger: fooMessenger, + events: ['FooController:stateChange', 'QuzController:stateChange'], }); const composableController = new ComposableController< ComposableControllerState, @@ -216,20 +204,32 @@ describe('ComposableController', () => { FooController: FooControllerState; }; const messenger = new Messenger< - never, - | ComposableControllerEvents + MockAnyNamespace, + | FooControllerAction + | ComposableControllerActions, | FooControllerEvent - >(); - const fooControllerMessenger = messenger.getRestricted({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], + | ComposableControllerEvents + >({ + namespace: MOCK_ANY_NAMESPACE, + }); + const fooControllerMessenger = new Messenger< + 'FooController', + FooControllerAction, + FooControllerEvent, + typeof messenger + >({ + namespace: 'FooController', + parent: messenger, }); const fooController = new FooController(fooControllerMessenger); - const composableControllerMessenger = messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: ['FooController:stateChange'], + const composableControllerMessenger: ComposableControllerMessenger = + new Messenger({ + namespace: 'ComposableController', + parent: messenger, + }); + composableControllerMessenger.delegate({ + messenger: fooControllerMessenger, + events: ['FooController:stateChange'], }); new ComposableController< ComposableControllerState, @@ -260,26 +260,47 @@ describe('ComposableController', () => { FooController: FooControllerState; }; const messenger = new Messenger< - never, + MockAnyNamespace, + | ComposableControllerActions + | QuzControllerAction + | FooControllerAction, | ComposableControllerEvents | ChildControllerStateChangeEvents - >(); - const quzControllerMessenger = messenger.getRestricted({ - name: 'QuzController', - allowedActions: [], - allowedEvents: [], + >({ namespace: MOCK_ANY_NAMESPACE }); + const quzControllerMessenger = new Messenger< + 'QuzController', + QuzControllerAction, + QuzControllerEvent, + typeof messenger + >({ + namespace: 'QuzController', + parent: messenger, }); const quzController = new QuzController(quzControllerMessenger); - const fooControllerMessenger = messenger.getRestricted({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], + const fooControllerMessenger = new Messenger< + 'FooController', + FooControllerAction, + FooControllerEvent, + typeof messenger + >({ + namespace: 'FooController', + parent: messenger, }); const fooController = new FooController(fooControllerMessenger); - const composableControllerMessenger = messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: ['QuzController:stateChange', 'FooController:stateChange'], + const composableControllerMessenger = new Messenger< + 'ComposableController', + ComposableControllerActions, + | ComposableControllerEvents + | FooControllerEvent + | QuzControllerEvent, + typeof messenger + >({ + namespace: 'ComposableController', + parent: messenger, + }); + messenger.delegate({ + messenger: composableControllerMessenger, + events: ['QuzController:stateChange', 'FooController:stateChange'], }); new ComposableController< ComposableControllerState, @@ -308,17 +329,29 @@ describe('ComposableController', () => { }); it('should throw if controller messenger not provided', () => { - const messenger = new Messenger(); - const quzControllerMessenger = messenger.getRestricted({ - name: 'QuzController', - allowedActions: [], - allowedEvents: [], + const messenger = new Messenger< + MockAnyNamespace, + QuzControllerAction | FooControllerAction, + QuzControllerEvent | FooControllerEvent + >({ namespace: MOCK_ANY_NAMESPACE }); + const quzControllerMessenger = new Messenger< + 'QuzController', + QuzControllerAction, + QuzControllerEvent, + typeof messenger + >({ + namespace: 'QuzController', + parent: messenger, }); const quzController = new QuzController(quzControllerMessenger); - const fooControllerMessenger = messenger.getRestricted({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], + const fooControllerMessenger = new Messenger< + 'FooController', + FooControllerAction, + FooControllerEvent, + typeof messenger + >({ + namespace: 'FooController', + parent: messenger, }); const fooController = new FooController(fooControllerMessenger); expect( @@ -339,19 +372,34 @@ describe('ComposableController', () => { }; const notController = new JsonRpcEngine(); const messenger = new Messenger< - never, + MockAnyNamespace, + | ComposableControllerActions + | FooControllerAction, ComposableControllerEvents | FooControllerEvent - >(); - const fooControllerMessenger = messenger.getRestricted({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], + >({ namespace: MOCK_ANY_NAMESPACE }); + const fooControllerMessenger = new Messenger< + 'FooController', + FooControllerAction, + FooControllerEvent, + typeof messenger + >({ + namespace: 'FooController', + parent: messenger, }); const fooController = new FooController(fooControllerMessenger); - const composableControllerMessenger = messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: ['FooController:stateChange'], + const composableControllerMessenger = new Messenger< + 'ComposableController', + ComposableControllerActions, + | ComposableControllerEvents + | FooControllerEvent, + typeof messenger + >({ + namespace: 'ComposableController', + parent: messenger, + }); + messenger.delegate({ + messenger: composableControllerMessenger, + events: ['FooController:stateChange'], }); expect( () => @@ -374,73 +422,6 @@ describe('ComposableController', () => { ).toThrow(INVALID_CONTROLLER_ERROR); }); - it('should not throw if composing a controller without a `stateChange` event', () => { - const messenger = new Messenger(); - const controllerWithoutStateChangeEventMessenger = messenger.getRestricted({ - name: 'ControllerWithoutStateChangeEvent', - allowedActions: [], - allowedEvents: [], - }); - const controllerWithoutStateChangeEvent = - new ControllerWithoutStateChangeEvent( - controllerWithoutStateChangeEventMessenger, - ); - const fooControllerMessenger = messenger.getRestricted({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], - }); - const fooController = new FooController(fooControllerMessenger); - expect( - () => - new ComposableController({ - controllers: { - ControllerWithoutStateChangeEvent: - controllerWithoutStateChangeEvent, - FooController: fooController, - }, - messenger: messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: ['FooController:stateChange'], - }), - }), - ).not.toThrow(); - }); - - it('should not throw if a child controller `stateChange` event is missing from the messenger events allowlist', () => { - const messenger = new Messenger< - never, - FooControllerEvent | QuzControllerEvent - >(); - const QuzControllerMessenger = messenger.getRestricted({ - name: 'QuzController', - allowedActions: [], - allowedEvents: [], - }); - const quzController = new QuzController(QuzControllerMessenger); - const fooControllerMessenger = messenger.getRestricted({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], - }); - const fooController = new FooController(fooControllerMessenger); - expect( - () => - new ComposableController({ - controllers: { - QuzController: quzController, - FooController: fooController, - }, - messenger: messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: ['FooController:stateChange'], - }), - }), - ).not.toThrow(); - }); - describe('metadata', () => { it('includes expected state in debug snapshots', () => { type ComposableControllerState = { diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index e46fa4870a0..04be89d88fb 100644 --- a/packages/composable-controller/src/ComposableController.ts +++ b/packages/composable-controller/src/ComposableController.ts @@ -1,12 +1,13 @@ import type { - RestrictedMessenger, StateConstraint, StateMetadata, StateMetadataConstraint, ControllerStateChangeEvent, + ControllerGetStateAction, BaseControllerInstance as ControllerInstance, -} from '@metamask/base-controller'; -import { BaseController } from '@metamask/base-controller'; +} from '@metamask/base-controller/next'; +import { BaseController } from '@metamask/base-controller/next'; +import type { Messenger } from '@metamask/messenger'; export const controllerName = 'ComposableController'; @@ -20,6 +21,15 @@ export type ComposableControllerStateConstraint = { [controllerName: string]: StateConstraint; }; +/** + * The `getState` action type for the {@link ComposableControllerMessenger}. + * + * @template ComposableControllerState - A type object that maps controller names to their state types. + */ +export type ComposableControllerGetStateAction< + ComposableControllerState extends ComposableControllerStateConstraint, +> = ControllerGetStateAction; + /** * The `stateChange` event type for the {@link ComposableControllerMessenger}. * @@ -41,6 +51,15 @@ export type ComposableControllerEvents< ComposableControllerState extends ComposableControllerStateConstraint, > = ComposableControllerStateChangeEvent; +/** + * A union type of action types available to the {@link ComposableControllerMessenger}. + * + * @template ComposableControllerState - A type object that maps controller names to their state types. + */ +export type ComposableControllerActions< + ComposableControllerState extends ComposableControllerStateConstraint, +> = ComposableControllerGetStateAction; + /** * A utility type that extracts controllers from the {@link ComposableControllerState} type, * and derives a union type of all of their corresponding `stateChange` events. @@ -75,13 +94,11 @@ export type AllowedEvents< */ export type ComposableControllerMessenger< ComposableControllerState extends ComposableControllerStateConstraint, -> = RestrictedMessenger< +> = Messenger< typeof controllerName, - never, + ComposableControllerActions, | ComposableControllerEvents - | AllowedEvents, - never, - AllowedEvents['type'] + | AllowedEvents >; /** @@ -106,7 +123,7 @@ export class ComposableController< * * @param options - Initial options used to configure this controller * @param options.controllers - An object that contains child controllers keyed by their names. - * @param options.messenger - A restricted messenger. + * @param options.messenger - A controller messenger. */ constructor({ controllers, @@ -163,15 +180,11 @@ export class ComposableController< delete this.metadata[name]; delete this.state[name]; // eslint-disable-next-line no-empty - } catch (_) {} - // False negative. `name` is a string type. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + } catch {} throw new Error(`${name} - ${INVALID_CONTROLLER_ERROR}`); } try { - this.messagingSystem.subscribe( - // False negative. `name` is a string type. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + this.messenger.subscribe( `${name}:stateChange`, (childState: StateConstraint) => { this.update((state) => { diff --git a/yarn.lock b/yarn.lock index 07e9b555049..3dd35a37f7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2867,6 +2867,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.4.0" "@metamask/json-rpc-engine": "npm:^10.1.0" + "@metamask/messenger": "npm:^0.3.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" immer: "npm:^9.0.6"