Skip to content

Commit 8fb745a

Browse files
committed
stash
1 parent d747651 commit 8fb745a

File tree

3 files changed

+125
-24
lines changed

3 files changed

+125
-24
lines changed

packages/composable-controller/CHANGELOG.md

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

1717
- **BREAKING:** Passing a non-controller into `controllers` constructor option now throws an error ([#3904](https://github.com/MetaMask/core/pull/3904))
1818
- **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))
19+
- **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))
20+
- 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.
21+
- 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.
22+
- 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.
1923
- Add `@metamask/utils` ^8.3.0 as a dependency. ([#3904](https://github.com/MetaMask/core/pull/3904))
2024

2125
### Removed

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: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
RestrictedControllerMessenger,
55
BaseState,
66
BaseConfig,
7+
EventConstraint,
78
StateMetadata,
89
} from '@metamask/base-controller';
910
import { isValidJson, type Json } from '@metamask/utils';
@@ -112,13 +113,43 @@ export type ComposableControllerMessenger = RestrictedControllerMessenger<
112113
AllowedEvents['type']
113114
>;
114115

116+
type GetStateChangeEventsUnion<Controller extends ControllerInstance> =
117+
Controller extends ControllerInstance
118+
? Controller extends BaseControllerV2Instance
119+
? ControllerStateChangeEvent<Controller['name'], Controller['state']>
120+
: never
121+
: never;
122+
115123
/**
116124
* Controller that can be used to compose multiple controllers together.
117125
*/
118-
export class ComposableController extends BaseController<
126+
export class ComposableController<
127+
ChildControllers extends ControllerInstance,
128+
ComposedControllerState extends ComposableControllerState = {
129+
[P in ChildControllers as P['name']]: P extends ControllerInstance
130+
? P['state']
131+
: never;
132+
},
133+
ComposedControllerStateChangeEvent extends EventConstraint & {
134+
type: `${typeof controllerName}:stateChange`;
135+
} = ControllerStateChangeEvent<
136+
typeof controllerName,
137+
ComposedControllerState
138+
>,
139+
ChildControllersStateChangeEvents extends EventConstraint & {
140+
type: `${string}:stateChange`;
141+
} = GetStateChangeEventsUnion<ChildControllers>,
142+
ComposedControllerMessenger extends ComposableControllerMessenger = RestrictedControllerMessenger<
143+
typeof controllerName,
144+
never,
145+
ComposedControllerStateChangeEvent | ChildControllersStateChangeEvents,
146+
never,
147+
ChildControllersStateChangeEvents['type']
148+
>,
149+
> extends BaseController<
119150
typeof controllerName,
120-
ComposableControllerState,
121-
ComposableControllerMessenger
151+
ComposedControllerState,
152+
ComposedControllerMessenger
122153
> {
123154
/**
124155
* Creates a ComposableController instance.
@@ -132,29 +163,29 @@ export class ComposableController extends BaseController<
132163
controllers,
133164
messenger,
134165
}: {
135-
controllers: ControllerInstance[];
136-
messenger: ComposableControllerMessenger;
166+
controllers: ChildControllers[];
167+
messenger: ComposedControllerMessenger;
137168
}) {
138169
if (messenger === undefined) {
139170
throw new Error(`Messaging system is required`);
140171
}
141172

142173
super({
143174
name: controllerName,
144-
metadata: controllers.reduce<StateMetadata<ComposableControllerState>>(
175+
metadata: controllers.reduce<StateMetadata<ComposedControllerState>>(
145176
(metadata, controller) => ({
146177
...metadata,
147178
[controller.name]: isBaseController(controller)
148179
? controller.metadata
149180
: { persist: true, anonymous: true },
150181
}),
151-
{},
182+
{} as never,
152183
),
153-
state: controllers.reduce<ComposableControllerState>(
184+
state: controllers.reduce<ComposedControllerState>(
154185
(state, controller) => {
155186
return { ...state, [controller.name]: controller.state };
156187
},
157-
{},
188+
{} as never,
158189
),
159190
messenger,
160191
});
@@ -173,18 +204,24 @@ export class ComposableController extends BaseController<
173204
const { name } = controller;
174205
if (isBaseControllerV1(controller)) {
175206
controller.subscribe((childState) => {
176-
this.update((state) => ({
177-
...state,
178-
[name]: childState,
179-
}));
207+
this.update(
208+
(state) =>
209+
({
210+
...state,
211+
[name]: childState,
212+
} as ComposedControllerState),
213+
);
180214
});
181215
} else if (isBaseController(controller)) {
182216
this.messagingSystem.subscribe(`${name}:stateChange`, (childState) => {
183217
if (isValidJson(childState)) {
184-
this.update((state) => ({
185-
...state,
186-
[name]: childState,
187-
}));
218+
this.update(
219+
(state) =>
220+
({
221+
...state,
222+
[name]: childState,
223+
} as ComposedControllerState),
224+
);
188225
}
189226
});
190227
} else {

0 commit comments

Comments
 (0)