Skip to content

Commit 923d77d

Browse files
committed
stash
1 parent bdc167c commit 923d77d

File tree

3 files changed

+130
-25
lines changed

3 files changed

+130
-25
lines changed

packages/composable-controller/CHANGELOG.md

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

1616
- **BREAKING:** Passing a non-controller into `controllers` constructor option now throws an error ([#3904](https://github.com/MetaMask/core/pull/3904))
1717
- **BREAKING:** The `AllowedActions` parameter of the `ComposableControllerMessenger` type is narrowed from `string` to `never`, as `ComposableController` does not use any external controller actions. ([#3904](https://github.com/MetaMask/core/pull/3904))
18+
- **BREAKING:** The `ComposableController` class is now a generic class that expects one generic argument `ChildControllers` in the form of a union of the controllers passed into the `controllers` array constructor option ([#3941](https://github.com/MetaMask/core/pull/3941))
19+
- Child controllers that extend `BaseControllerV1` must have an overridden `name` property that is defined using the `as const` assertion for the `ComposableController` class to be typed correctly.
20+
- The controller state is now constrained by a `ComposedControllerState` generic argument, which is automaticallly derived from the `ChildControllers` argument without needing to be passed in by the user.
21+
- The `messenger` constructor option is constrained by a `ComposedControllerMessenger` generic argument which is also automatically derived, and is required to contain the `stateChange` events for all child controllers in its `Events` and `AllowedEvents` parameters, as well as the composable controller instance in its `Events` parameter.
1822

1923
### Removed
2024

packages/composable-controller/src/ComposableController.test.ts

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,50 @@ class FooController extends BaseController<
6060
}
6161
}
6262

63+
type QuzControllerState = {
64+
quz: string;
65+
};
66+
type QuzControllerEvent = {
67+
type: `QuzController:stateChange`;
68+
payload: [QuzControllerState, Patch[]];
69+
};
70+
71+
type QuzMessenger = RestrictedControllerMessenger<
72+
'QuzController',
73+
never,
74+
QuzControllerEvent,
75+
never,
76+
never
77+
>;
78+
79+
const quzControllerStateMetadata = {
80+
quz: {
81+
persist: true,
82+
anonymous: true,
83+
},
84+
};
85+
86+
class QuzController extends BaseController<
87+
'QuzController',
88+
QuzControllerState,
89+
QuzMessenger
90+
> {
91+
constructor(messagingSystem: QuzMessenger) {
92+
super({
93+
messenger: messagingSystem,
94+
metadata: quzControllerStateMetadata,
95+
name: 'QuzController',
96+
state: { quz: 'quz' },
97+
});
98+
}
99+
100+
updateQuz(quz: string) {
101+
super.update((state) => {
102+
state.quz = quz;
103+
});
104+
}
105+
}
106+
63107
// Mock BaseControllerV1 classes
64108

65109
type BarControllerState = BaseState & {
@@ -71,7 +115,7 @@ class BarController extends BaseControllerV1<never, BarControllerState> {
71115
bar: 'bar',
72116
};
73117

74-
override name = 'BarController';
118+
override name = 'BarController' as const;
75119

76120
constructor() {
77121
super();
@@ -92,7 +136,7 @@ class BazController extends BaseControllerV1<never, BazControllerState> {
92136
baz: 'baz',
93137
};
94138

95-
override name = 'BazController';
139+
override name = 'BazController' as const;
96140

97141
constructor() {
98142
super();
@@ -157,23 +201,39 @@ describe('ComposableController', () => {
157201
it('should compose controller state', () => {
158202
const controllerMessenger = new ControllerMessenger<
159203
never,
160-
FooControllerEvent
204+
FooControllerEvent | QuzControllerEvent
161205
>();
162-
const fooControllerMessenger = controllerMessenger.getRestricted({
206+
const fooMessenger = controllerMessenger.getRestricted<
207+
'FooController',
208+
never,
209+
never
210+
>({
163211
name: 'FooController',
164212
});
165-
const fooController = new FooController(fooControllerMessenger);
213+
const quzMessenger = controllerMessenger.getRestricted<
214+
'QuzController',
215+
never,
216+
never
217+
>({
218+
name: 'QuzController',
219+
});
220+
const fooController = new FooController(fooMessenger);
221+
const quzController = new QuzController(quzMessenger);
166222

167223
const composableControllerMessenger = controllerMessenger.getRestricted({
168224
name: 'ComposableController',
169-
allowedEvents: ['FooController:stateChange'],
225+
allowedEvents: [
226+
'FooController:stateChange',
227+
'QuzController:stateChange',
228+
],
170229
});
171230
const composableController = new ComposableController({
172-
controllers: [fooController],
231+
controllers: [fooController, quzController],
173232
messenger: composableControllerMessenger,
174233
});
175234
expect(composableController.state).toStrictEqual({
176235
FooController: { foo: 'foo' },
236+
QuzController: { quz: 'quz' },
177237
});
178238
});
179239

packages/composable-controller/src/ComposableController.ts

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { BaseController, BaseControllerV1 } from '@metamask/base-controller';
22
import type {
33
BaseConfig,
44
BaseState,
5+
ControllerStateChangeEvent,
56
RestrictedControllerMessenger,
7+
EventConstraint,
68
StateMetadata,
79
StateConstraint,
810
} from '@metamask/base-controller';
@@ -24,7 +26,7 @@ export type BaseControllerV1Instance =
2426
* The `BaseController` class itself can't be used directly as a type representing all of its subclasses,
2527
* because the generic parameters it expects require knowing the exact shape of the controller's state and messenger.
2628
*
27-
* Instead, we look for an object with the `BaseController` properties that we use in the ComposableController (name and state).
29+
* Instead, we look for an object with the `BaseController` properties that we use in the ComposableController (name, state).
2830
*/
2931
export type BaseControllerV2Instance = {
3032
name: string;
@@ -109,13 +111,46 @@ export type ComposableControllerMessenger = RestrictedControllerMessenger<
109111
AllowedEvents['type']
110112
>;
111113

114+
type GetStateChangeEvents<Controller extends ControllerInstance> =
115+
Controller extends ControllerInstance
116+
? Controller extends BaseControllerV1Instance
117+
? {
118+
type: `${Controller['name']}:stateChange`;
119+
payload: [Controller['state'], Patch[]];
120+
}
121+
: ControllerStateChangeEvent<Controller['name'], Controller['state']>
122+
: never;
123+
112124
/**
113125
* Controller that can be used to compose multiple controllers together.
114126
*/
115-
export class ComposableController extends BaseController<
127+
export class ComposableController<
128+
ChildControllers extends ControllerInstance,
129+
ComposedControllerState extends ComposableControllerState = {
130+
[P in ChildControllers as P['name']]: P extends ControllerInstance
131+
? P['state']
132+
: never;
133+
},
134+
ComposedControllerStateChangeEvent extends EventConstraint & {
135+
type: `${typeof controllerName}:stateChange`;
136+
} = ControllerStateChangeEvent<
137+
typeof controllerName,
138+
ComposedControllerState
139+
>,
140+
ChildControllersStateChangeEvents extends EventConstraint & {
141+
type: `${string}:stateChange`;
142+
} = GetStateChangeEvents<ChildControllers>,
143+
ComposedControllerMessenger extends ComposableControllerMessenger = RestrictedControllerMessenger<
144+
typeof controllerName,
145+
never,
146+
ComposedControllerStateChangeEvent | ChildControllersStateChangeEvents,
147+
never,
148+
ChildControllersStateChangeEvents['type']
149+
>,
150+
> extends BaseController<
116151
typeof controllerName,
117-
ComposableControllerState,
118-
ComposableControllerMessenger
152+
ComposedControllerState,
153+
ComposedControllerMessenger
119154
> {
120155
/**
121156
* Creates a ComposableController instance.
@@ -129,29 +164,29 @@ export class ComposableController extends BaseController<
129164
controllers,
130165
messenger,
131166
}: {
132-
controllers: ControllerInstance[];
133-
messenger: ComposableControllerMessenger;
167+
controllers: ChildControllers[];
168+
messenger: ComposedControllerMessenger;
134169
}) {
135170
if (messenger === undefined) {
136171
throw new Error(`Messaging system is required`);
137172
}
138173

139174
super({
140175
name: controllerName,
141-
metadata: controllers.reduce<StateMetadata<ComposableControllerState>>(
176+
metadata: controllers.reduce<StateMetadata<ComposedControllerState>>(
142177
(metadata, controller) => ({
143178
...metadata,
144179
[controller.name]: isBaseController(controller)
145180
? (controller as BaseController<never, never, never>).metadata
146181
: { persist: true, anonymous: true },
147182
}),
148-
{},
183+
{} as never,
149184
),
150-
state: controllers.reduce<ComposableControllerState>(
185+
state: controllers.reduce<ComposedControllerState>(
151186
(state, controller) => {
152187
return { ...state, [controller.name]: controller.state };
153188
},
154-
{},
189+
{} as never,
155190
),
156191
messenger,
157192
});
@@ -175,10 +210,13 @@ export class ComposableController extends BaseController<
175210
const { name } = controller;
176211
if (isBaseControllerV1(controller)) {
177212
controller.subscribe((childState) => {
178-
this.update((state) => ({
179-
...state,
180-
[name]: childState,
181-
}));
213+
this.update(
214+
(state) =>
215+
({
216+
...state,
217+
[name]: childState,
218+
} as ComposedControllerState),
219+
);
182220
});
183221
}
184222
if (
@@ -188,10 +226,13 @@ export class ComposableController extends BaseController<
188226
this.messagingSystem.subscribe(
189227
`${name}:stateChange`,
190228
(childState: StateConstraint) => {
191-
this.update((state) => ({
192-
...state,
193-
[name]: childState,
194-
}));
229+
this.update(
230+
(state) =>
231+
({
232+
...state,
233+
[name]: childState,
234+
} as ComposedControllerState),
235+
);
195236
},
196237
);
197238
}

0 commit comments

Comments
 (0)