diff --git a/README.md b/README.md index 0b377cff988..9c8ef118646 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ linkStyle default opacity:0.5 chain_agnostic_permission --> network_controller; chain_agnostic_permission --> permission_controller; composable_controller --> base_controller; + composable_controller --> messenger; composable_controller --> json_rpc_engine; core_backend --> base_controller; core_backend --> controller_utils; diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index a16cd0d14f3..fcfb3d02a3f 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -107,9 +107,6 @@ "packages/composable-controller/src/ComposableController.test.ts": { "import-x/namespace": 3 }, - "packages/composable-controller/src/ComposableController.ts": { - "@typescript-eslint/no-unused-vars": 1 - }, "packages/controller-utils/jest.environment.js": { "n/prefer-global/text-encoder": 1, "n/prefer-global/text-decoder": 1, diff --git a/packages/base-controller/src/next/BaseController.ts b/packages/base-controller/src/next/BaseController.ts index cf4dafe793c..7d2c0059ae2 100644 --- a/packages/base-controller/src/next/BaseController.ts +++ b/packages/base-controller/src/next/BaseController.ts @@ -112,7 +112,7 @@ export type StateDeriverConstraint = (value: never) => Json; * This type can be assigned to any `StatePropertyMetadata` type. */ export type StatePropertyMetadataConstraint = { - anonymous: boolean | StateDeriverConstraint; + includeInDebugSnapshot: boolean | StateDeriverConstraint; includeInStateLogs?: boolean | StateDeriverConstraint; persist: boolean | StateDeriverConstraint; usedInUi?: boolean; diff --git a/packages/composable-controller/CHANGELOG.md b/packages/composable-controller/CHANGELOG.md index e68d9c2e7b3..32a4bf9441b 100644 --- a/packages/composable-controller/CHANGELOG.md +++ b/packages/composable-controller/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Migrate `ComposableController` to new `Messenger` from `@metamask/messenger` ([#6710](https://github.com/MetaMask/core/pull/6710)) + - Previously, the controller accepted a `RestrictedMessenger` instance from `@metamask/base-controller`. + +### Fixed + +- Resolve incompatibility of `ChildControllerStateChangeEvents` type with `BaseController` (when used in the `Events` type argument of `ComposableControllerMessenger`) by removing unnecessary nested logic from definition ([#6904](https://github.com/MetaMask/core/pull/6904)) + - Also update generic parameter names `ControllerName` and `ControllerState` to `ChildControllerName`, `ChildControllerState` for reduced ambiguity. + ## [11.1.0] ### Added diff --git a/packages/composable-controller/package.json b/packages/composable-controller/package.json index 0a76fa4552b..1025df087a0 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.1" + "@metamask/base-controller": "^8.4.1", + "@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..d9e81c01e2a 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,26 +28,37 @@ 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 = { foo: { persist: true, - anonymous: true, + includeInDebugSnapshot: true, + usedInUi: false, + includeInStateLogs: false, }, }; @@ -66,23 +86,28 @@ 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 = { quz: { persist: true, - anonymous: true, + includeInDebugSnapshot: true, + usedInUi: false, + includeInStateLogs: false, }, }; @@ -107,50 +132,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 +153,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 fooMessenger: FooMessenger = new Messenger({ + namespace: 'FooController', + parent: messenger, + }); + messenger.delegate({ + messenger: fooMessenger, + events: ['QuzController:stateChange'], }); - const quzMessenger = messenger.getRestricted({ - name: 'QuzController', - allowedActions: [], - allowedEvents: [], + 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 +208,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, + }); + messenger.delegate({ + messenger: composableControllerMessenger, + events: ['FooController:stateChange'], }); new ComposableController< ComposableControllerState, @@ -242,7 +246,10 @@ describe('ComposableController', () => { }); const listener = sinon.stub(); - messenger.subscribe('ComposableController:stateChange', listener); + composableControllerMessenger.subscribe( + 'ComposableController:stateChange', + listener, + ); fooController.updateFoo('qux'); expect(listener.calledOnce).toBe(true); @@ -260,26 +267,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, @@ -307,18 +335,80 @@ describe('ComposableController', () => { }); }); + it('should not throw if child state change event subscription fails', () => { + type ComposableControllerState = { + FooController: FooControllerState; + }; + const messenger = new Messenger< + MockAnyNamespace, + | ComposableControllerActions + | FooControllerAction, + ComposableControllerEvents | FooControllerEvent + >({ namespace: MOCK_ANY_NAMESPACE }); + const fooControllerMessenger = new Messenger< + 'FooController', + FooControllerAction, + FooControllerEvent, + typeof messenger + >({ + namespace: 'FooController', + parent: messenger, + }); + const fooController = new FooController(fooControllerMessenger); + const composableControllerMessenger = new Messenger< + 'ComposableController', + ComposableControllerActions, + | ComposableControllerEvents + | FooControllerEvent, + typeof messenger + >({ + namespace: 'ComposableController', + parent: messenger, + }); + messenger.delegate({ + messenger: composableControllerMessenger, + events: ['FooController:stateChange'], + }); + jest + .spyOn(composableControllerMessenger, 'subscribe') + .mockImplementation(() => { + throw new Error(); + }); + expect( + () => + new ComposableController({ + controllers: { + FooController: fooController, + }, + messenger: composableControllerMessenger, + }), + ).not.toThrow(); + }); + 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 +429,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,97 +479,41 @@ 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 = { FooController: FooControllerState; }; const messenger = new Messenger< - never, + MockAnyNamespace, + | ComposableControllerActions + | FooControllerAction, | ComposableControllerEvents | FooControllerEvent - >(); - const fooMessenger = messenger.getRestricted< + >({ namespace: MOCK_ANY_NAMESPACE }); + const fooControllerMessenger = new Messenger< 'FooController', - never, - never + FooControllerAction, + FooControllerEvent, + typeof messenger >({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], + namespace: 'FooController', + parent: messenger, }); - const fooController = new FooController(fooMessenger); - const composableControllerMessenger = messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: ['FooController:stateChange'], + const fooController = new FooController(fooControllerMessenger); + const composableControllerMessenger = new Messenger< + 'ComposableController', + ComposableControllerActions, + | ComposableControllerEvents + | FooControllerEvent, + typeof messenger + >({ + namespace: 'ComposableController', + parent: messenger, + }); + messenger.delegate({ + messenger: composableControllerMessenger, + events: ['FooController:stateChange'], }); const controller = new ComposableController< ComposableControllerState, @@ -480,7 +529,7 @@ describe('ComposableController', () => { deriveStateFromMetadata( controller.state, controller.metadata, - 'anonymous', + 'includeInDebugSnapshot', ), ).toMatchInlineSnapshot(` Object { @@ -496,24 +545,35 @@ describe('ComposableController', () => { FooController: FooControllerState; }; const messenger = new Messenger< - never, + MockAnyNamespace, + | ComposableControllerActions + | FooControllerAction, | ComposableControllerEvents | FooControllerEvent - >(); - const fooMessenger = messenger.getRestricted< + >({ namespace: MOCK_ANY_NAMESPACE }); + const fooControllerMessenger = new Messenger< 'FooController', - never, - never + FooControllerAction, + FooControllerEvent, + typeof messenger >({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], + namespace: 'FooController', + parent: messenger, }); - const fooController = new FooController(fooMessenger); - const composableControllerMessenger = messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: ['FooController:stateChange'], + const fooController = new FooController(fooControllerMessenger); + const composableControllerMessenger = new Messenger< + 'ComposableController', + ComposableControllerActions, + | ComposableControllerEvents + | FooControllerEvent, + typeof messenger + >({ + namespace: 'ComposableController', + parent: messenger, + }); + messenger.delegate({ + messenger: composableControllerMessenger, + events: ['FooController:stateChange'], }); const controller = new ComposableController< ComposableControllerState, @@ -539,24 +599,35 @@ describe('ComposableController', () => { FooController: FooControllerState; }; const messenger = new Messenger< - never, + MockAnyNamespace, + | ComposableControllerActions + | FooControllerAction, | ComposableControllerEvents | FooControllerEvent - >(); - const fooMessenger = messenger.getRestricted< + >({ namespace: MOCK_ANY_NAMESPACE }); + const fooControllerMessenger = new Messenger< 'FooController', - never, - never + FooControllerAction, + FooControllerEvent, + typeof messenger >({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], + namespace: 'FooController', + parent: messenger, }); - const fooController = new FooController(fooMessenger); - const composableControllerMessenger = messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: ['FooController:stateChange'], + const fooController = new FooController(fooControllerMessenger); + const composableControllerMessenger = new Messenger< + 'ComposableController', + ComposableControllerActions, + | ComposableControllerEvents + | FooControllerEvent, + typeof messenger + >({ + namespace: 'ComposableController', + parent: messenger, + }); + messenger.delegate({ + messenger: composableControllerMessenger, + events: ['FooController:stateChange'], }); const controller = new ComposableController< ComposableControllerState, @@ -588,24 +659,35 @@ describe('ComposableController', () => { FooController: FooControllerState; }; const messenger = new Messenger< - never, + MockAnyNamespace, + | ComposableControllerActions + | FooControllerAction, | ComposableControllerEvents | FooControllerEvent - >(); - const fooMessenger = messenger.getRestricted< + >({ namespace: MOCK_ANY_NAMESPACE }); + const fooControllerMessenger = new Messenger< 'FooController', - never, - never + FooControllerAction, + FooControllerEvent, + typeof messenger >({ - name: 'FooController', - allowedActions: [], - allowedEvents: [], + namespace: 'FooController', + parent: messenger, }); - const fooController = new FooController(fooMessenger); - const composableControllerMessenger = messenger.getRestricted({ - name: 'ComposableController', - allowedActions: [], - allowedEvents: ['FooController:stateChange'], + const fooController = new FooController(fooControllerMessenger); + const composableControllerMessenger = new Messenger< + 'ComposableController', + ComposableControllerActions, + | ComposableControllerEvents + | FooControllerEvent, + typeof messenger + >({ + namespace: 'ComposableController', + parent: messenger, + }); + messenger.delegate({ + messenger: composableControllerMessenger, + events: ['FooController:stateChange'], }); const controller = new ComposableController< ComposableControllerState, diff --git a/packages/composable-controller/src/ComposableController.ts b/packages/composable-controller/src/ComposableController.ts index e46fa4870a0..03bdb317341 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. @@ -51,12 +70,10 @@ export type ChildControllerStateChangeEvents< ComposableControllerState extends ComposableControllerStateConstraint, > = ComposableControllerState extends Record< - infer ControllerName extends string, - infer ControllerState + infer ChildControllerName extends string, + infer ChildControllerState extends StateConstraint > - ? ControllerState extends StateConstraint - ? ControllerStateChangeEvent - : never + ? ControllerStateChangeEvent : never; /** @@ -75,13 +92,11 @@ export type AllowedEvents< */ export type ComposableControllerMessenger< ComposableControllerState extends ComposableControllerStateConstraint, -> = RestrictedMessenger< +> = Messenger< typeof controllerName, - never, + ComposableControllerActions, | ComposableControllerEvents - | AllowedEvents, - never, - AllowedEvents['type'] + | AllowedEvents >; /** @@ -106,7 +121,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, @@ -128,7 +143,7 @@ export class ComposableController< (metadata as StateMetadataConstraint)[name] = { includeInStateLogs: false, persist: true, - anonymous: true, + includeInDebugSnapshot: true, usedInUi: false, }; return metadata; @@ -163,24 +178,22 @@ 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 - `${name}:stateChange`, - (childState: StateConstraint) => { - this.update((state) => { - // Type assertion is necessary for property assignment to a generic type. This does not pollute or widen the type of the asserted variable. - // @ts-expect-error "Type instantiation is excessively deep" - (state as ComposableControllerStateConstraint)[name] = childState; - }); - }, - ); + this.messenger.subscribe< + // The type intersection with "ComposableController:stateChange" is added by one of the `Messenger.subscribe` overloads, but that constraint is unnecessary here, + // since this method only subscribes the messenger to child controller `stateChange` events. + // @ts-expect-error "Type '`${string}:stateChange`' is not assignable to parameter of type '"ComposableController:stateChange" & ChildControllerStateChangeEvents["type"]'." + ChildControllerStateChangeEvents['type'] + >(`${name}:stateChange`, (childState: StateConstraint) => { + this.update((state) => { + // Type assertion is necessary for property assignment to a generic type. This does not pollute or widen the type of the asserted variable. + // @ts-expect-error "Type instantiation is excessively deep" + (state as ComposableControllerStateConstraint)[name] = childState; + }); + }); } catch (error: unknown) { // False negative. `name` is a string type. // eslint-disable-next-line @typescript-eslint/restrict-template-expressions diff --git a/packages/composable-controller/tsconfig.build.json b/packages/composable-controller/tsconfig.build.json index 779d385a6ab..249f327913d 100644 --- a/packages/composable-controller/tsconfig.build.json +++ b/packages/composable-controller/tsconfig.build.json @@ -8,6 +8,9 @@ "references": [ { "path": "../base-controller/tsconfig.build.json" + }, + { + "path": "../messenger/tsconfig.build.json" } ], "include": ["../../types", "./src"] diff --git a/packages/composable-controller/tsconfig.json b/packages/composable-controller/tsconfig.json index cc814f313b7..0d608a82545 100644 --- a/packages/composable-controller/tsconfig.json +++ b/packages/composable-controller/tsconfig.json @@ -7,6 +7,9 @@ { "path": "../base-controller" }, + { + "path": "../messenger" + }, { "path": "../json-rpc-engine" } diff --git a/yarn.lock b/yarn.lock index afdd7caec80..5efc630fa83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3031,6 +3031,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.4.1" "@metamask/json-rpc-engine": "npm:^10.1.1" + "@metamask/messenger": "npm:^0.3.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" immer: "npm:^9.0.6"