Skip to content

Commit

Permalink
stash
Browse files Browse the repository at this point in the history
  • Loading branch information
MajorLift committed Jul 1, 2024
1 parent a3a060c commit 6457a28
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 72 deletions.
72 changes: 55 additions & 17 deletions packages/composable-controller/src/ComposableController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,21 @@ class BazController extends BaseControllerV1<never, BazControllerState> {
}
}

type ControllersMap = {
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
FooController: FooController;
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
QuzController: QuzController;
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
BarController: BarController;
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
BazController: BazController;
};

describe('ComposableController', () => {
afterEach(() => {
sinon.restore();
Expand All @@ -159,6 +174,7 @@ describe('ComposableController', () => {
// eslint-disable-next-line @typescript-eslint/naming-convention
BazController: BazControllerState;
};

const composableMessenger = new ControllerMessenger<
never,
ComposableControllerEvents<ComposableControllerState>
Expand All @@ -167,7 +183,10 @@ describe('ComposableController', () => {
allowedActions: [],
allowedEvents: [],
});
const controller = new ComposableController({
const controller = new ComposableController<
ComposableControllerState,
ControllersMap[keyof ComposableControllerState]
>({
controllers: [new BarController(), new BazController()],
messenger: composableMessenger,
});
Expand All @@ -194,7 +213,10 @@ describe('ComposableController', () => {
allowedEvents: [],
});
const barController = new BarController();
new ComposableController({
new ComposableController<
ComposableControllerState,
ControllersMap[keyof ComposableControllerState]
>({
controllers: [barController],
messenger: composableMessenger,
});
Expand Down Expand Up @@ -255,11 +277,13 @@ describe('ComposableController', () => {
'QuzController:stateChange',
],
});
const composableController =
new ComposableController<ComposableControllerState>({
controllers: [fooController, quzController],
messenger: composableControllerMessenger,
});
const composableController = new ComposableController<
ComposableControllerState,
ControllersMap[keyof ComposableControllerState]
>({
controllers: [fooController, quzController],
messenger: composableControllerMessenger,
});
expect(composableController.state).toStrictEqual({
FooController: { foo: 'foo' },
QuzController: { quz: 'quz' },
Expand Down Expand Up @@ -288,7 +312,10 @@ describe('ComposableController', () => {
allowedActions: [],
allowedEvents: ['FooController:stateChange'],
});
new ComposableController<ComposableControllerState>({
new ComposableController<
ComposableControllerState,
ControllersMap[keyof ComposableControllerState]
>({
controllers: [fooController],
messenger: composableControllerMessenger,
});
Expand Down Expand Up @@ -336,11 +363,13 @@ describe('ComposableController', () => {
allowedActions: [],
allowedEvents: ['FooController:stateChange'],
});
const composableController =
new ComposableController<ComposableControllerState>({
controllers: [barController, fooController],
messenger: composableControllerMessenger,
});
const composableController = new ComposableController<
ComposableControllerState,
ControllersMap[keyof ComposableControllerState]
>({
controllers: [barController, fooController],
messenger: composableControllerMessenger,
});
expect(composableController.state).toStrictEqual({
BarController: { bar: 'bar' },
FooController: { foo: 'foo' },
Expand Down Expand Up @@ -373,7 +402,10 @@ describe('ComposableController', () => {
allowedActions: [],
allowedEvents: ['FooController:stateChange'],
});
new ComposableController<ComposableControllerState>({
new ComposableController<
ComposableControllerState,
ControllersMap[keyof ComposableControllerState]
>({
controllers: [barController, fooController],
messenger: composableControllerMessenger,
});
Expand Down Expand Up @@ -421,7 +453,10 @@ describe('ComposableController', () => {
allowedActions: [],
allowedEvents: ['FooController:stateChange'],
});
new ComposableController<ComposableControllerState>({
new ComposableController<
ComposableControllerState,
ControllersMap[keyof ComposableControllerState]
>({
controllers: [barController, fooController],
messenger: composableControllerMessenger,
});
Expand Down Expand Up @@ -490,13 +525,16 @@ describe('ComposableController', () => {
});
expect(
() =>
new ComposableController({
new ComposableController<
ComposableControllerState,
ControllersMap[keyof ComposableControllerState]
>({
// @ts-expect-error - Suppressing type error to test for runtime error handling
controllers: [notController, fooController],
messenger: composableControllerMessenger,
}),
).toThrow(
'Invalid controller: controller must extend from BaseController or BaseControllerV1',
'Invalid component: component must be a MessengerConsumer or a controller inheriting from BaseControllerV1.',
);
});
});
Expand Down
128 changes: 73 additions & 55 deletions packages/composable-controller/src/ComposableController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { BaseController, BaseControllerV1 } from '@metamask/base-controller';
import type {
ActionConstraint,
BaseConfig,
Expand All @@ -8,22 +7,34 @@ import type {
StateConstraint,
StateMetadata,
ControllerStateChangeEvent,
Listener,
} from '@metamask/base-controller';
import { BaseController, BaseControllerV1 } from '@metamask/base-controller';
import type { Patch } from 'immer';

export const controllerName = 'ComposableController';

type MessengerConsumerInstance = {
name: string;
messagingSystem: RestrictedControllerMessengerConstraint;
};

/**
* A universal subtype of all controller instances that extend from `BaseControllerV1`.
* Any `BaseControllerV1` instance can be assigned to this type.
*
* Note that this type is not the widest subtype or narrowest supertype of all `BaseControllerV1` instances.
* This type is therefore unsuitable for general use as a type constraint, and is only intended for use within the ComposableController.
*/
export type BaseControllerV1Instance =
// `any` is used so that all `BaseControllerV1` instances are assignable to this type.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
BaseControllerV1<any, any>;
export type BaseControllerV1Instance = {
name: string;
config: BaseConfig & object;
defaultConfig: BaseConfig & object;
state: BaseState & object;
defaultState: BaseState & object;
subscribe: (listener: Listener<BaseState & object>) => void;
disabled: boolean;
} & Partial<Pick<MessengerConsumerInstance, 'messagingSystem'>>;

/**
* A universal subtype of all controller instances that extend from `BaseController` (formerly `BaseControllerV2`).
Expand All @@ -34,9 +45,12 @@ export type BaseControllerV1Instance =
*
* For this reason, we only look for `BaseController` properties that we use in the ComposableController (name and state).
*/
export type BaseControllerInstance = {
export type BaseControllerInstance<
State extends StateConstraint = StateConstraint,
> = {
name: string;
state: StateConstraint;
state: State;
metadata: Record<string, unknown>;
};

/**
Expand All @@ -46,42 +60,63 @@ export type BaseControllerInstance = {
* Note that this type is not the widest subtype or narrowest supertype of all `BaseController` and `BaseControllerV1` instances.
* This type is therefore unsuitable for general use as a type constraint, and is only intended for use within the ComposableController.
*/
export type ControllerInstance =
export type WalletComponentInstance =
| BaseControllerV1Instance
| BaseControllerInstance;
| BaseControllerInstance
| MessengerConsumerInstance;

export type ControllerInstance = Exclude<
WalletComponentInstance,
MessengerConsumerInstance
>;

/**
* The narrowest supertype of all `RestrictedControllerMessenger` instances.
*/
export type RestrictedControllerMessengerConstraint =
RestrictedControllerMessenger<
string,
ActionConstraint,
EventConstraint,
string,
string
>;
export type RestrictedControllerMessengerConstraint<
ControllerName extends string = string,
> = RestrictedControllerMessenger<
ControllerName,
ActionConstraint,
EventConstraint,
string,
string
>;

/**
* Determines if the given class has a messaging system.
* @param component - Component instance to check
* @returns True if the component is an instance of `MessengerConsumerInstance`
*/
export function isMessengerConsumer(
component: WalletComponentInstance,
): component is MessengerConsumerInstance {
return 'name' in component && 'messagingSystem' in component;
}

/**
* Determines if the given controller is an instance of `BaseControllerV1`
* @param controller - Controller instance to check
* @returns True if the controller is an instance of `BaseControllerV1`
*/
export function isBaseControllerV1(
controller: ControllerInstance,
): controller is BaseControllerV1<
BaseConfig & Record<string, unknown>,
BaseState & Record<string, unknown>
> {
controller: WalletComponentInstance,
): controller is BaseControllerV1Instance {
return (
'name' in controller &&
typeof controller.name === 'string' &&
'config' in controller &&
typeof controller.config === 'object' &&
'defaultConfig' in controller &&
typeof controller.defaultConfig === 'object' &&
'state' in controller &&
typeof controller.state === 'object' &&
'defaultState' in controller &&
typeof controller.defaultState === 'object' &&
'disabled' in controller &&
typeof controller.disabled === 'boolean' &&
'subscribe' in controller &&
typeof controller.subscribe === 'function' &&
controller instanceof BaseControllerV1
);
}
Expand All @@ -92,17 +127,15 @@ export function isBaseControllerV1(
* @returns True if the controller is an instance of `BaseController`
*/
export function isBaseController(
controller: ControllerInstance,
): controller is BaseController<
string,
StateConstraint,
RestrictedControllerMessengerConstraint
> {
controller: WalletComponentInstance,
): controller is BaseControllerInstance {
return (
'name' in controller &&
typeof controller.name === 'string' &&
'state' in controller &&
typeof controller.state === 'object' &&
'metadata' in controller &&
typeof controller.metadata === 'object' &&
controller instanceof BaseController
);
}
Expand Down Expand Up @@ -186,25 +219,13 @@ export type ComposableControllerMessenger<
AllowedEvents<ComposableControllerState>['type']
>;

type GetChildControllers<
ComposableControllerState,
ControllerName extends keyof ComposableControllerState = keyof ComposableControllerState,
> = ControllerName extends string
? ComposableControllerState[ControllerName] extends StateConstraint
? { name: ControllerName; state: ComposableControllerState[ControllerName] }
: BaseControllerV1<
BaseConfig & Record<string, unknown>,
BaseState & ComposableControllerState[ControllerName]
>
: never;

/**
* Controller that can be used to compose multiple controllers together.
* @template ChildControllerState - The composed state of the child controllers that are being used to instantiate the composable controller.
*/
export class ComposableController<
ComposableControllerState extends LegacyComposableControllerStateConstraint,
ChildControllers extends ControllerInstance = GetChildControllers<ComposableControllerState>,
ChildControllers extends WalletComponentInstance,
> extends BaseController<
typeof controllerName,
ComposableControllerState,
Expand Down Expand Up @@ -242,7 +263,9 @@ export class ComposableController<
),
state: controllers.reduce<ComposableControllerState>(
(state, controller) => {
return { ...state, [controller.name]: controller.state };
return 'state' in controller
? { ...state, [controller.name]: controller.state }
: state;
},
{} as never,
),
Expand All @@ -258,34 +281,29 @@ export class ComposableController<
* Constructor helper that subscribes to child controller state changes.
* @param controller - Controller instance to update
*/
#updateChildController(controller: ControllerInstance): void {
if (!isBaseController(controller) && !isBaseControllerV1(controller)) {
throw new Error(
'Invalid controller: controller must extend from BaseController or BaseControllerV1',
);
}

#updateChildController(controller: WalletComponentInstance): void {
const { name } = controller;
if (
(isBaseControllerV1(controller) && 'messagingSystem' in controller) ||
isBaseController(controller)
) {
if (isBaseController(controller) || isMessengerConsumer(controller)) {
this.messagingSystem.subscribe(
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${name}:stateChange`,
(childState: Record<string, unknown>) => {
(childState: object) => {
this.update((state) => {
Object.assign(state, { [name]: childState });
});
},
);
} else if (isBaseControllerV1(controller)) {
controller.subscribe((childState) => {
controller.subscribe((childState: BaseState & object) => {
this.update((state) => {
Object.assign(state, { [name]: childState });
});
});
} else {
throw new Error(
'Invalid component: component must be a MessengerConsumer or a controller inheriting from BaseControllerV1.',
);
}
}
}
Expand Down

0 comments on commit 6457a28

Please sign in to comment.