Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[base-controller] Fix all any usage, apply universal supertype for functions #1890

Merged
merged 42 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9b47929
Improved typing for `deriveStateFromMetadata`
MajorLift Oct 24, 2023
9e70e09
Remove `any` from `Action`, `Event` typing in `BaseController`
MajorLift Oct 24, 2023
df6e36d
Fix errors and remove `any` in `RateLimitController`
MajorLift Oct 24, 2023
07fa0fa
Remove unnecessary `extends unknown`
MajorLift Oct 26, 2023
356ffd3
Merge branch '231031-typing-fixes-base-controller' into 231023-queued…
MajorLift Nov 1, 2023
b5ec9b1
Fix single-letter type param names
MajorLift Nov 1, 2023
bf31301
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 2, 2023
841c8f3
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 2, 2023
29f4026
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 7, 2023
cb6621c
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 7, 2023
043e3ee
Fix `RateLimitController` types
MajorLift Nov 7, 2023
b184980
Fix type for selector function
MajorLift Nov 7, 2023
13f3693
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 7, 2023
ae04604
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 8, 2023
1fbbb59
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 8, 2023
08413a9
Fix single-letter type names: `N`, `S`, `T`, `E`, `V`
MajorLift Nov 9, 2023
f1cc123
Fix outdated comment
MajorLift Nov 9, 2023
b32a7ed
Improve `deriveStateFromMetadata` typing
MajorLift Nov 9, 2023
3b0ab93
Use `as Namespaced<ControllerName, any>` pattern for namespaced actio…
MajorLift Nov 9, 2023
35b55dd
Fix generic param names
MajorLift Nov 10, 2023
1fa24a8
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 10, 2023
7baebd7
Improve `EventSubscriptionMap` typing
MajorLift Nov 13, 2023
ee2467d
More rigorous `reduce` typing
MajorLift Nov 13, 2023
f2dbbf5
Constrain `SelectorFunction` with `Event` type
MajorLift Nov 13, 2023
ce08095
Define `GetStateAction`, `StateChangeEvent` as properties globally av…
MajorLift Nov 13, 2023
ae291da
Define `NamespacedName` type
MajorLift Nov 13, 2023
d474b25
Constrain `RateLimitedApi['method']` return type to `boolean`
MajorLift Nov 13, 2023
5eb3906
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 13, 2023
37769c2
Prepend global controller action and event with `Controller`, widen s…
MajorLift Nov 13, 2023
2d604bb
Remove unnecessary type constraint
MajorLift Nov 13, 2023
df81424
Revert "Fix single-letter type names: `N`, `S`, `T`, `E`, `V`"
MajorLift Nov 13, 2023
e8826ef
Revert "Constrain `RateLimitedApi['method']` return type to `boolean`"
MajorLift Nov 13, 2023
af244c5
Revert single-letter type param rename for 'HandlerReturnValue`
MajorLift Nov 14, 2023
01eb2a0
Better typing for `call` method for `rate-limit-controller`
MajorLift Nov 14, 2023
465a6a8
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 14, 2023
d84ade9
Add namespace constraint to `registerActionHandler`
MajorLift Nov 14, 2023
ca8b791
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 14, 2023
bd3c81d
Define wider and more accurate universal supertype for functions that…
MajorLift Nov 14, 2023
08695f5
Remove now redundant type constraint on `CallApi['handler']` in `rate…
MajorLift Nov 14, 2023
097ed5b
Use `infer _` to avoid confusion with the universal function supertyp…
MajorLift Nov 14, 2023
a7c657e
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 15, 2023
d23613c
Merge branch 'main' into 231023-queued-allowed-actions
MajorLift Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 46 additions & 15 deletions packages/base-controller/src/BaseControllerV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { enablePatches, produceWithPatches, applyPatches } from 'immer';
import type { Draft, Patch } from 'immer';

import type {
ActionConstraint,
EventConstraint,
RestrictedControllerMessenger,
Namespaced,
} from './ControllerMessenger';

enablePatches();
Expand Down Expand Up @@ -62,13 +63,45 @@ export interface StatePropertyMetadata<T extends Json> {
anonymous: boolean | StateDeriver<T>;
}

export type ControllerGetStateAction<
ControllerName extends string,
ControllerState extends Record<string, unknown>,
> = {
type: `${ControllerName}:getState`;
handler: () => ControllerState;
};

export type ControllerStateChangeEvent<
ControllerName extends string,
ControllerState extends Record<string, unknown>,
> = {
type: `${ControllerName}:stateChange`;
payload: [ControllerState, Patch[]];
};

export type ControllerActions<
ControllerName extends string,
ControllerState extends Record<string, unknown>,
> = ControllerGetStateAction<ControllerName, ControllerState>;

export type ControllerEvents<
ControllerName extends string,
ControllerState extends Record<string, unknown>,
> = ControllerStateChangeEvent<ControllerName, ControllerState>;

/**
* Controller class that provides state management, subscriptions, and state metadata
*/
export class BaseController<
N extends string,
S extends Record<string, Json>,
messenger extends RestrictedControllerMessenger<N, any, any, string, string>,
messenger extends RestrictedControllerMessenger<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. This would be a breaking change, and so I am wary of changing this. That said, I don't understand why we chose to use any here. I would be curious to hear if Mark can remember any context around this.

Copy link
Contributor Author

@MajorLift MajorLift Oct 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActionConstraint and EventConstraint in this PR are written to be the universal supertypes of their respective categories, so I don't think we'd be adding a meaningful constraint by moving away from any. Also no tests are being broken by this change, although I guess we'll have to check whether any package outside of the monorepo is using incompatible Action or Event types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, using any in this context is not harmful in the same way that it is in other contexts. Types used in a generic constraints don't get applied to any specific variables, they're just constraints (i.e. by using any here, absolutely nothing is actually given the type any, even indirectly). For this reason I've sometimes put less effort into avoiding the use of any for generic constraints.

That said, more detailed constraints don't hurt either. And in this case, these constraints are already present in the RestrictedControllerMessenger type so this change seems like it would not affect anything functionally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic constraints are also helpful for intellisense hints and self-documenting code. If finding the right constraints isn't a significant blocker for actual feature development, I think it's worth the extra effort, even if it's not strictly necessary in terms of functionality.

N,
MajorLift marked this conversation as resolved.
Show resolved Hide resolved
ActionConstraint | ControllerActions<N, S>,
EventConstraint | ControllerEvents<N, S>,
string,
string
>,
> {
private internalState: S;

Expand Down Expand Up @@ -164,7 +197,7 @@ export class BaseController<

this.internalState = nextState;
this.messagingSystem.publish(
`${this.name}:stateChange` as Namespaced<N, any>,
`${this.name}:stateChange`,
nextState,
patches,
);
Expand All @@ -183,7 +216,7 @@ export class BaseController<
const nextState = applyPatches(this.internalState, patches);
this.internalState = nextState;
this.messagingSystem.publish(
`${this.name}:stateChange` as Namespaced<N, any>,
`${this.name}:stateChange`,
nextState,
patches,
);
Expand All @@ -199,9 +232,7 @@ export class BaseController<
* listeners from being garbage collected.
*/
protected destroy() {
this.messagingSystem.clearEventSubscriptions(
`${this.name}:stateChange` as Namespaced<N, any>,
);
this.messagingSystem.clearEventSubscriptions(`${this.name}:stateChange`);
}
}

Expand Down Expand Up @@ -250,20 +281,20 @@ function deriveStateFromMetadata<S extends Record<string, Json>>(
metadata: StateMetadata<S>,
metadataProperty: 'anonymous' | 'persist',
): Record<string, Json> {
return Object.keys(state).reduce((persistedState, key) => {
return (Object.keys(state) as (keyof S)[]).reduce<
Partial<Record<keyof S, Json>>
>((persistedState, key) => {
try {
const stateMetadata = metadata[key as keyof S];
const stateMetadata = metadata[key];
if (!stateMetadata) {
throw new Error(`No metadata found for '${key}'`);
throw new Error(`No metadata found for '${String(key)}'`);
}
const propertyMetadata = stateMetadata[metadataProperty];
const stateProperty = state[key];
if (typeof propertyMetadata === 'function') {
persistedState[key as string] = propertyMetadata(
stateProperty as S[keyof S],
);
persistedState[key] = propertyMetadata(stateProperty);
} else if (propertyMetadata) {
persistedState[key as string] = stateProperty;
persistedState[key] = stateProperty;
}
return persistedState;
} catch (error) {
Expand All @@ -274,5 +305,5 @@ function deriveStateFromMetadata<S extends Record<string, Json>>(
});
return persistedState;
}
}, {} as Record<string, Json>);
}, {}) as Record<keyof S, Json>;
}
84 changes: 58 additions & 26 deletions packages/base-controller/src/ControllerMessenger.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,58 @@
export type ActionHandler<Action, ActionType> = (
export type ActionHandler<
Action extends ActionConstraint,
ActionType = Action['type'],
> = (
...args: ExtractActionParameters<Action, ActionType>
) => ExtractActionResponse<Action, ActionType>;
export type ExtractActionParameters<Action, T> = Action extends {

export type ExtractActionParameters<
Action extends ActionConstraint,
T = Action['type'],
> = Action extends {
type: T;
handler: (...args: infer H) => any;
handler: (...args: infer H) => unknown;
}
? H
: never;
export type ExtractActionResponse<Action, T> = Action extends {
mcmire marked this conversation as resolved.
Show resolved Hide resolved

export type ExtractActionResponse<
Action extends ActionConstraint,
T = Action['type'],
> = Action extends {
type: T;
handler: (...args: any) => infer H;
handler: (...args: infer _) => infer H;
}
? H
: never;

export type ExtractEventHandler<Event, T> = Event extends {
export type ExtractEventHandler<
Event extends EventConstraint,
T = Event['type'],
> = Event extends {
type: T;
payload: infer P;
}
? P extends unknown[]
? (...payload: P) => void
: never
: never;
export type ExtractEventPayload<Event, T> = Event extends {

export type ExtractEventPayload<
Event extends EventConstraint,
T = Event['type'],
> = Event extends {
type: T;
payload: infer P;
}
? P
? P extends unknown[]
? P
: never
: never;

export type GenericEventHandler = (...args: unknown[]) => void;

export type SelectorFunction<Args extends unknown[], ReturnValue> = (
...args: Args
export type SelectorFunction<Event extends EventConstraint, ReturnValue> = (
...args: ExtractEventPayload<Event>
) => ReturnValue;
Comment on lines +54 to +55
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Highlighting for closer review

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, that does seem like a much more appropriate type

export type SelectorEventHandler<SelectorReturnValue> = (
newValue: SelectorReturnValue,
Expand All @@ -41,13 +61,19 @@ export type SelectorEventHandler<SelectorReturnValue> = (

export type ActionConstraint = {
type: string;
handler: (...args: any) => unknown;
handler: ((...args: never) => unknown) | ((...args: never[]) => unknown);
};
export type EventConstraint = {
Comment on lines 62 to +65
Copy link
Contributor Author

@MajorLift MajorLift Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it turns out that (...args: never[]) => unknown probably isn't the universal supertype for functions.

The error messages @Gudahtt linked here #1890 (comment) reflect the fact that in general, array types of arbitrary length are not assignable to tuple types with independent typing for some or all of its index positions (the array is wider than the tuple since it has less constraints e.g. the tuple has a minimum length whereas the array could be empty).

This means that there's a whole subcategory of functions that (...args: never[]) => unknown doesn't cover: functions with at least some parameters that are positionally typed, making their argument list evaluate as a fixed-length or variadic tuple type, instead of a freely expandable (or contractable) array type.

To illustrate, (...args: never[]) => unknown covers the cases where all of the params can be typed together as a group e.g. (...args: (string | number)[]) => T. But to cover cases like (a: string, b: number, ...rest: unknown[]) => T, where positional arguments are typed independently, we need another top type with an even narrower params type: (...args: never) => unknown.

In short, I think the top type for functions that includes both of these categories is: ((...args: never) => unknown) | ((...args: never[]) => unknown).

(| never in general is analogous to + 0, so (...args: (never[] | never)) => unknown doesn't work.)

With this change, CallApi['handler'] is now accepted by RestrictedControllerMessenger as extending ActionConstraint['handler'] without needing the explicit assertion, and running git checkout origin/main ./packages/rate-limit-controller/ on this PR branch no longer results in any type errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great sleuthing! 👏🏻

type: string;
payload: unknown[];
};
export type EventConstraint = { type: string; payload: unknown[] };

type EventSubscriptionMap = Map<
GenericEventHandler | SelectorEventHandler<unknown>,
SelectorFunction<any, unknown> | undefined
type EventSubscriptionMap<
Event extends EventConstraint,
ReturnValue = unknown,
> = Map<
GenericEventHandler | SelectorEventHandler<ReturnValue>,
SelectorFunction<ExtractEventPayload<Event>, ReturnValue> | undefined
>;

/**
Expand All @@ -62,6 +88,9 @@ export type Namespaced<Name extends string, T> = T extends `${Name}:${string}`
? T
: never;

export type NamespacedName<Namespace extends string = string> =
`${Namespace}:${string}`;

type NarrowToNamespace<T, Namespace extends string> = T extends {
type: `${Namespace}:${string}`;
}
Expand Down Expand Up @@ -151,10 +180,10 @@ export class RestrictedControllerMessenger<
* @param action - The action type. This is a unqiue identifier for this action.
* @param handler - The action handler. This function gets called when the `call` method is
* invoked with the given action type.
* @throws Will throw when a handler has been registered for this action type already.
* @throws Will throw if an action handler that is not in the current namespace is being registered.
* @template T - A type union of Action type strings that are namespaced by N.
*/
MajorLift marked this conversation as resolved.
Show resolved Hide resolved
registerActionHandler<T extends Namespaced<N, Action['type']>>(
registerActionHandler<T extends NamespacedName<N>>(
action: T,
handler: ActionHandler<Action, T>,
) {
Expand All @@ -177,7 +206,7 @@ export class RestrictedControllerMessenger<
* @param action - The action type. This is a unqiue identifier for this action.
* @template T - A type union of Action type strings that are namespaced by N.
*/
unregisterActionHandler<T extends Namespaced<N, Action['type']>>(action: T) {
unregisterActionHandler<T extends NamespacedName<N>>(action: T) {
/* istanbul ignore if */ // Branch unreachable with valid types
if (!action.startsWith(`${this.controllerName}:`)) {
throw new Error(
Expand All @@ -202,7 +231,7 @@ export class RestrictedControllerMessenger<
* @template T - A type union of allowed Action type strings.
* @returns The action return value.
*/
call<T extends AllowedAction & string>(
call<T extends AllowedAction & NamespacedName>(
action: T,
...params: ExtractActionParameters<Action, T>
): ExtractActionResponse<Action, T> {
Expand All @@ -227,7 +256,7 @@ export class RestrictedControllerMessenger<
* match the type of this payload.
* @template E - A type union of Event type strings that are namespaced by N.
*/
publish<E extends Namespaced<N, Event['type']>>(
publish<E extends NamespacedName<N>>(
event: E,
...payload: ExtractEventPayload<Event, E>
) {
Expand All @@ -252,7 +281,7 @@ export class RestrictedControllerMessenger<
* match the type of the payload for this event type.
* @template E - A type union of Event type strings.
*/
subscribe<E extends AllowedEvent & string>(
subscribe<E extends AllowedEvent & NamespacedName>(
eventType: E,
handler: ExtractEventHandler<Event, E>,
): void;
Expand All @@ -276,13 +305,13 @@ export class RestrictedControllerMessenger<
* @template E - A type union of Event type strings.
* @template V - The selector return value.
*/
subscribe<E extends AllowedEvent & string, V>(
subscribe<E extends AllowedEvent & NamespacedName, V>(
eventType: E,
handler: SelectorEventHandler<V>,
selector: SelectorFunction<ExtractEventPayload<Event, E>, V>,
): void;

subscribe<E extends AllowedEvent & string, V>(
subscribe<E extends AllowedEvent & NamespacedName, V>(
event: E,
handler: ExtractEventHandler<Event, E>,
selector?: SelectorFunction<ExtractEventPayload<Event, E>, V>,
Expand Down Expand Up @@ -312,7 +341,7 @@ export class RestrictedControllerMessenger<
* @throws Will throw when the given event handler is not registered for this event.
* @template T - A type union of allowed Event type strings.
*/
unsubscribe<E extends AllowedEvent & string>(
unsubscribe<E extends AllowedEvent & NamespacedName>(
event: E,
handler: ExtractEventHandler<Event, E>,
) {
Expand All @@ -335,7 +364,7 @@ export class RestrictedControllerMessenger<
* @param event - The event type. This is a unique identifier for this event.
* @template E - A type union of Event type strings that are namespaced by N.
*/
clearEventSubscriptions<E extends Namespaced<N, Event['type']>>(event: E) {
clearEventSubscriptions<E extends NamespacedName<N>>(event: E) {
/* istanbul ignore if */ // Branch unreachable with valid types
if (!event.startsWith(`${this.controllerName}:`)) {
throw new Error(
Expand All @@ -362,7 +391,10 @@ export class ControllerMessenger<
> {
private readonly actions = new Map<Action['type'], unknown>();

private readonly events = new Map<Event['type'], EventSubscriptionMap>();
private readonly events = new Map<
Event['type'],
EventSubscriptionMap<Event>
>();

/**
* A cache of selector return values for their respective handlers.
Expand Down
16 changes: 10 additions & 6 deletions packages/rate-limit-controller/src/RateLimitController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { RestrictedControllerMessenger } from '@metamask/base-controller';
import type {
ActionConstraint,
RestrictedControllerMessenger,
} from '@metamask/base-controller';
import { BaseControllerV2 as BaseController } from '@metamask/base-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import type { Patch } from 'immer';
Expand All @@ -10,7 +13,7 @@ import type { Patch } from 'immer';
* @property rateLimitCount - The amount of calls an origin can make in the rate limit time window.
*/
export type RateLimitedApi = {
method: (...args: any[]) => any;
method: ActionConstraint['handler'];
rateLimitTimeout?: number;
rateLimitCount?: number;
};
Expand Down Expand Up @@ -121,11 +124,11 @@ export class RateLimitController<

this.messagingSystem.registerActionHandler(
`${name}:call` as const,
((
(
origin: string,
type: keyof RateLimitedApis,
...args: Parameters<RateLimitedApis[keyof RateLimitedApis]['method']>
) => this.call(origin, type, ...args)) as any,
) => this.call(origin, type, ...args),
);
}

Expand All @@ -135,7 +138,6 @@ export class RateLimitController<
* @param origin - The requesting origin.
* @param type - The type of API call to make.
* @param args - Arguments for the API call.
* @returns `false` if rate-limited, and `true` otherwise.
*/
async call<ApiType extends keyof RateLimitedApis>(
origin: string,
Expand All @@ -155,7 +157,9 @@ export class RateLimitController<
throw new Error('Invalid api type');
}

return implementation(...args);
return implementation(...args) as ReturnType<
RateLimitedApis[ApiType]['method']
>;
}
MajorLift marked this conversation as resolved.
Show resolved Hide resolved

/**
Expand Down
Loading