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

[composable-controller] Make class and messenger generic upon child controllers #3627

Closed
MajorLift opened this issue Dec 6, 2023 · 3 comments · Fixed by #3952
Closed

[composable-controller] Make class and messenger generic upon child controllers #3627

MajorLift opened this issue Dec 6, 2023 · 3 comments · Fixed by #3952
Assignees

Comments

@MajorLift
Copy link
Contributor

MajorLift commented Dec 6, 2023

Explanation

Currently, the allow lists of ComposableControllerMessenger are set to string, which is too permissive. Each ComposableController instance needs typing and allow lists that are specific to its set of input child controllers.

However, there's no simple way to achieve this using BaseController, as controller-messenger allow lists are defined for the controller class singleton, not controller instances.

Instead, ComposableController and ComposableControllerMessenger need to be made polymorphic upon the controllers constructor property, which represents the list of child controllers to be composed.

Requirements

  • A generic parameter ControllerList (or maybe ChildControllers) needs to be added to the ComposableController class and ComposableControllerMessenger type.
  • ControllerList should be constrained, but not defined by the least supertype for all base-controller instances (currently ControllerInstance).
  • ControllerList should be used to derive the return type of flatState() at the type level.
  • ControllerList should be used to derive the state type for ComposableController instances.
  • ControllerList should be used to derive the allow lists of ComposableControllerMessenger.
    • The allow lists should at minimum include the stateChange events for all child controllers that are subscribed to in the #updateChildController() method.

References

@MajorLift MajorLift self-assigned this Dec 6, 2023
@MajorLift MajorLift removed their assignment Jan 11, 2024
@MajorLift MajorLift changed the title Improve ComposableController typing for child controllers and allow lists Make ComposableController polymorphic upon child controllers Jan 22, 2024
@MajorLift MajorLift changed the title Make ComposableController polymorphic upon child controllers Make ComposableController polymorphic upon its child controllers, and improve typing for messenger allow lists Jan 22, 2024
@MajorLift MajorLift changed the title Make ComposableController polymorphic upon its child controllers, and improve typing for messenger allow lists [composable-controller] Define child controllers list as class generic parameter, and use it to derive typing and allow lists Jan 22, 2024
@MajorLift MajorLift changed the title [composable-controller] Define child controllers list as class generic parameter, and use it to derive typing and allow lists [composable-controller] Make class generic upon its child controllers list, and use the list to derive typing and messenger allow-lists Jan 22, 2024
@MajorLift MajorLift changed the title [composable-controller] Make class generic upon its child controllers list, and use the list to derive typing and messenger allow-lists [composable-controller] Make class and allow-lists generic upon child controllers Jan 22, 2024
@MajorLift MajorLift changed the title [composable-controller] Make class and allow-lists generic upon child controllers [composable-controller] Make class and messenger generic upon child controllers Jan 22, 2024
@desi
Copy link
Contributor

desi commented Jan 22, 2024

@MajorLift
Copy link
Contributor Author

MajorLift commented Feb 8, 2024

One approach we could take is to add the types listed in requirements as generic parameters to the ComposableController class and compute them in place:

export class ComposableController<
  ChildControllers extends ControllerInstance[],
    
  ComposedState extends ComposableControllerState = T1<ChildControllers>,
    
  ComposedStateChangeEvent = ControllerStateChangeEvent<'ComposableController', ComposedState>,
    
  ChildStateChangeEvents extends EventConstraint & { type: `${string}:stateChange` } = T2<ChildControllers>,
    
  ComposedControllerMessenger = RestrictedControllerMessenger<
    'ComposableController',
    never,
    ComposedStateChangeEvent | ChildStateChangeEvents,
    never,
    ChildStateChangeEvents['type']
  >,
> extends BaseController<
  'ComposableController',
  ComposedState,
  ComposedControllerMessenger
> {
   constructor({
    controllers,
    messenger,
  }: {
    controllers: ControllerList;
    messenger: ComposedControllerMessenger;
  }) {}
}
  • Reduces the only independent variable to the list of child controllers.
  • The generic constraints validate the constructor inputs.
    • Probably the most important feature to be introduced by this ticket.
  • Implementing T1 and T2 would be a good starting point for tackling this ticket.
Another approach we could take is to write a factory class for ComposableControllers:
abstract class ComposableControllerFactory {
  static compose<
    ChildControllers extends ControllerInstance[],
    
    ComposedState extends ComposableControllerState = T1<ChildControllers>,
    
    ComposedStateChangeEvent = ControllerStateChangeEvent<'ComposableController', ComposedState>
    
    ChildStateChangeEvents extends EventConstraint & { type: `${string}:stateChange` } = T2<ChildControllers>,
    
    ComposedControllerMessenger = RestrictedControllerMessenger<
      'ComposableController',
      never,
      ComposedStateChangeEvent | ChildStateChangeEvents,
      never,
      ChildStateChangeEvents['type']
    >,
  >({
    controllers: ChildControllers, 
    messenger: ComposedControllerMessenger, 
  }){}
}
  • Decouples "factory" and type-level logic vs. internal methods and BaseController-inherited properties of each controller instance.
  • ComposableController arguably already functions as a factory that creates an arbitrary number of unique controller instances, which is distinct from the behavior of other singleton controllers. We need a new abstraction to encapsulate this distinction.
  • Narrows the scope of the problem to typing each ComposableController instance only once -- upon instantiation.
    • Preserves current ComposableController API which doesn't allow users to update child controller list state using side effects (i.e. this.update()).

The API of the ComposableController class would change as follows:

  • Before:
const composableControllerMessenger = controllerMessenger.getRestricted({
  name: 'ComposableController',
  allowedEvents: ['FooController:stateChange'],
});
const composableController = new ComposableController({
  controllers: [fooController],
  messenger: composableControllerMessenger,
});
  • After:
const composableControllerMessenger = controllerMessenger.getRestricted({
  name: 'ComposableController',
  allowedEvents: ['FooController:stateChange', 'FooController:stateChange'],
});
const { composedController } = ComposableControllerFactory.compose<[fooController, barController]>({
  controllers: [fooController, barController], 
  messenger: composableControllerMessenger,
});

This would require the consumer to supply a controllerMessenger instance with the stateChange events of all child controllers specified as allowed events. This seems unavoidable with the current ControllerMessenger implementation, which does not support allow lists to be altered after instantiation.

  • ComposableController does need to be typed according to its child controllers list, but it will still be used as a singleton. Applying the factory pattern will therefore be unnecessary.

MajorLift added a commit that referenced this issue Feb 21, 2024
…pe checks for inputs, remove `#controllers` field (#3904)

## Explanation

There are several issues with the current ComposableController
implementation that should be addressed before further updates are made
to the controller (e.g. #3627).

- Removes `#controllers` class field, which is not being updated by
`#updateChildController` or anywhere else.
- Removing this makes it clear that the list of child controllers to be
composed is determined at class instantiation and cannot be altered
later.
- This behavior is consistent with `#updateChildController` being a
private method.
- Types BaseController, ComposableController state with `Record<string,
Json>`
- Opted to use `any` to disable BaseController state type constraint, as
there is no straightforward way to type `BaseControllerV1` state to be
compatible with `Json`.
- Adds a `isBaseController` type guard, ~and removes the deprecated
`subscribed` property from `BaseController`~.
- Removes `ControllerList` type in anticipation of
#3627. Internally,
`ControllerInstance` will be used to type child controllers or unions
and tuples thereof.
- Adds and exports `ControllerInstance`, `BaseControllerV{1,2}Instance` types.
- Populates state metadata object in constructor with child controllers.

## References

- Blocks #3627 
- Closes #3716
- Replaces some TODOs with comment explaining necessity of `any` usage.
- Closes #3907

## Changelog

Recorded under "Unreleased" heading in CHANGELOG files.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate

---------

Co-authored-by: Elliot Winkler <[email protected]>
MajorLift added a commit that referenced this issue Mar 3, 2024
@MajorLift MajorLift assigned MajorLift and unassigned cryptodev-2s Mar 6, 2024
MajorLift added a commit that referenced this issue Mar 6, 2024
MajorLift added a commit that referenced this issue Mar 8, 2024
@desi
Copy link
Contributor

desi commented Apr 24, 2024

@MajorLift How would you feel about changing the estimate on this reflect the actual complexity? And just wondering what the next steps on this one are?

MajorLift added a commit that referenced this issue Apr 25, 2024
MajorLift added a commit that referenced this issue May 30, 2024
…izing over child controllers list (#3952)

## Explanation

Currently, the allow lists of `ComposableControllerMessenger` are set to
`string`, which is too permissive. Each `ComposableController` instance
needs typing and allow lists that are specific to its set of input child
controllers.

To achieve this, the `ComposableController` class and its messenger are
made polymorphic upon the `ControllerState` type, which defines the
shape of the composed state.

## References

- Closes #3627

## Changelog

###
[`@metamask/composable-controller`](https://github.com/MetaMask/core/pull/3952/files#diff-9745631a8a7023c408b4f9b96dc8c0eaa9a41599e8d93eb470bb268f1fcc75ff)

### Added

- Adds and exports new types:
([#3952](#3952))
- `RestrictedControllerMessengerConstraint`, which is the narrowest
supertype of all controller-messenger instances.
- `LegacyControllerStateConstraint`, a universal supertype for the
controller state object, encompassing both BaseControllerV1 and
BaseControllerV2 state.
- `ComposableControllerStateConstraint`, the narrowest supertype for the
composable controller state object.

### Changed

- **BREAKING:** The `ComposableController` class is now a generic class
that expects one generic argument `ComposableControllerState`
([#3952](#3952)).
- **BREAKING:** For the `ComposableController` class to be typed
correctly, any of its child controllers that extend `BaseControllerV1`
must have an overridden `name` property that is defined using the `as
const` assertion.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants