{"version":3,"file":"BaseController.mjs","sourceRoot":"","sources":["../src/BaseController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n  ActionConstraint,\n  EventConstraint,\n  Messenger,\n  MessengerActions,\n  MessengerEvents,\n} from '@metamask/messenger';\nimport type { Json, PublicInterface } from '@metamask/utils';\nimport { enablePatches, produceWithPatches, applyPatches, freeze } from 'immer';\nimport type { Draft, Patch } from 'immer';\n\nenablePatches();\n\n/**\n * A type that constrains the state of all controllers.\n *\n * In other words, the narrowest supertype encompassing all controller state.\n */\nexport type StateConstraint = Record<string, Json>;\n\n/**\n * A state change listener.\n *\n * This function will get called for each state change, and is given a copy of\n * the new state along with a set of patches describing the changes since the\n * last update.\n *\n * @param state - The new controller state.\n * @param patches - A list of patches describing any changes (see here for more\n * information: https://immerjs.github.io/immer/docs/patches)\n */\nexport type StateChangeListener<Type> = (state: Type, patches: Patch[]) => void;\n\n/**\n * An function to derive state.\n *\n * This function will accept one piece of the controller state (one property),\n * and will return some derivation of that state.\n *\n * @param value - A piece of controller state.\n * @returns Something derived from controller state.\n */\nexport type StateDeriver<Type extends Json> = (value: Type) => Json;\n\n/**\n * State metadata.\n *\n * This metadata describes which parts of state should be persisted, and how to\n * get an anonymized representation of the state.\n */\nexport type StateMetadata<Type extends StateConstraint> = {\n  [Key in keyof Type]-?: StatePropertyMetadata<Type[Key]>;\n};\n\n/**\n * Metadata for a single state property\n */\nexport type StatePropertyMetadata<ControllerState extends Json> = {\n  /**\n   * Indicates whether this property should be included in debug snapshots attached to Sentry\n   * errors.\n   *\n   * Set this to false if the state may contain personally identifiable information, or if it's\n   * too large to include in a debug snapshot.\n   */\n  includeInDebugSnapshot: boolean | StateDeriver<ControllerState>;\n  /**\n   * Indicates whether this property should be included in state logs.\n   *\n   * Set this to false if the data should be kept hidden from support agents (e.g. if it contains\n   * secret keys, or personally-identifiable information that is not useful for debugging).\n   *\n   * We do allow state logs to contain some personally identifiable information to assist with\n   * diagnosing errors (e.g. transaction hashes, addresses), but we still attempt to limit the\n   * data we expose to what is most useful for helping users.\n   */\n  includeInStateLogs: boolean | StateDeriver<ControllerState>;\n  /**\n   * Indicates whether this property should be persisted.\n   *\n   * If true, the property will be persisted and saved between sessions.\n   * If false, the property will not be saved between sessions, and it will always be missing from the `state` constructor parameter.\n   */\n  persist: boolean | StateDeriver<ControllerState>;\n  /**\n   * Indicates whether this property is used by the UI.\n   *\n   * If true, the property will be accessible from the UI.\n   * If false, it will be inaccessible from the UI.\n   *\n   * Making a property accessible to the UI has a performance overhead, so it's better to set this\n   * to `false` if it's not used in the UI, especially for properties that can be large in size.\n   *\n   * Note that we disallow the use of a state derivation function here to preserve type information\n   * for the UI (the state deriver type always returns `Json`).\n   */\n  usedInUi: boolean;\n};\n\n/**\n * A universal supertype of `StateDeriver` types.\n * This type can be assigned to any `StateDeriver` type.\n */\nexport type StateDeriverConstraint = (value: never) => Json;\n\n/**\n * A universal supertype of `StatePropertyMetadata` types.\n * This type can be assigned to any `StatePropertyMetadata` type.\n */\nexport type StatePropertyMetadataConstraint = {\n  includeInDebugSnapshot: boolean | StateDeriverConstraint;\n  includeInStateLogs: boolean | StateDeriverConstraint;\n  persist: boolean | StateDeriverConstraint;\n  usedInUi: boolean;\n};\n\n/**\n * A universal supertype of `StateMetadata` types.\n * This type can be assigned to any `StateMetadata` type.\n */\nexport type StateMetadataConstraint = Record<\n  string,\n  StatePropertyMetadataConstraint\n>;\n\n/**\n * The widest subtype of all controller instances that inherit from `BaseController` (formerly `BaseControllerV2`).\n * Any `BaseController` subclass instance can be assigned to this type.\n */\nexport type BaseControllerInstance = Omit<\n  PublicInterface<\n    BaseController<\n      string,\n      StateConstraint,\n      // Use `any` to allow any parent to be set.\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      Messenger<string, ActionConstraint, EventConstraint, any>\n    >\n  >,\n  'metadata'\n> & {\n  metadata: StateMetadataConstraint;\n};\n\nexport type ControllerGetStateAction<\n  ControllerName extends string,\n  ControllerState extends StateConstraint,\n> = {\n  type: `${ControllerName}:getState`;\n  handler: () => ControllerState;\n};\n\n/**\n * @deprecated This event type is deprecated. Please use\n * `ControllerStateChangedEvent` instead.\n */\nexport type ControllerStateChangeEvent<\n  ControllerName extends string,\n  ControllerState extends StateConstraint,\n> = {\n  type: `${ControllerName}:stateChange`;\n  payload: [ControllerState, Patch[]];\n};\n\nexport type ControllerStateChangedEvent<\n  ControllerName extends string,\n  ControllerState extends StateConstraint,\n> = {\n  type: `${ControllerName}:stateChanged`;\n  payload: [ControllerState, Patch[]];\n};\n\nexport type ControllerActions<\n  ControllerName extends string,\n  ControllerState extends StateConstraint,\n> = ControllerGetStateAction<ControllerName, ControllerState>;\n\nexport type ControllerEvents<\n  ControllerName extends string,\n  ControllerState extends StateConstraint,\n> =\n  | ControllerStateChangeEvent<ControllerName, ControllerState>\n  | ControllerStateChangedEvent<ControllerName, ControllerState>;\n\n/**\n * Controller class that provides state management, subscriptions, and state metadata\n */\nexport class BaseController<\n  ControllerName extends string,\n  ControllerState extends StateConstraint,\n  ControllerMessenger extends Messenger<\n    ControllerName,\n    ActionConstraint,\n    EventConstraint,\n    // Use `any` to allow any parent to be set. `any` is harmless in a type constraint anyway,\n    // it's the one totally safe place to use it.\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    any\n  >,\n> {\n  /**\n   * The controller state.\n   */\n  #internalState: ControllerState;\n\n  /**\n   * The controller messenger. This is used to interact with other parts of the application.\n   */\n  protected messenger: ControllerMessenger;\n\n  /**\n   * The controller messenger.\n   *\n   * This is the same as the `messenger` property, but has a type that only lets us use\n   * actions and events that are part of the `BaseController` class.\n   */\n  readonly #messenger: Messenger<\n    ControllerName,\n    ControllerActions<ControllerName, ControllerState>,\n    ControllerEvents<ControllerName, ControllerState>\n  >;\n\n  /**\n   * The name of the controller.\n   *\n   * This is used by the ComposableController to construct a composed application state.\n   */\n  public readonly name: ControllerName;\n\n  public readonly metadata: StateMetadata<ControllerState>;\n\n  /**\n   * Creates a BaseController instance.\n   *\n   * @param options - Controller options.\n   * @param options.messenger - The controller messenger.\n   * @param options.metadata - ControllerState metadata, describing how to \"anonymize\" the state, and which\n   * parts should be persisted.\n   * @param options.name - The name of the controller, used as a namespace for events and actions.\n   * @param options.state - Initial controller state.\n   */\n  constructor({\n    messenger,\n    metadata,\n    name,\n    state,\n  }: {\n    messenger: ControllerActions<\n      ControllerName,\n      ControllerState\n    >['type'] extends MessengerActions<ControllerMessenger>['type']\n      ? ControllerStateChangeEvent<\n          ControllerName,\n          ControllerState\n        >['type'] extends MessengerEvents<ControllerMessenger>['type']\n        ? ControllerMessenger\n        : ControllerStateChangedEvent<\n              ControllerName,\n              ControllerState\n            >['type'] extends MessengerEvents<ControllerMessenger>['type']\n          ? ControllerMessenger\n          : never\n      : never;\n    metadata: StateMetadata<ControllerState>;\n    name: ControllerName;\n    state: ControllerState;\n  }) {\n    // The parameter type validates that the expected actions/events are present\n    // We don't have a way to validate the type property because the type is invariant\n    this.#messenger = messenger as unknown as Messenger<\n      ControllerName,\n      ControllerActions<ControllerName, ControllerState>,\n      ControllerEvents<ControllerName, ControllerState>\n    >;\n    this.messenger = messenger;\n    this.name = name;\n    // Here we use `freeze` from Immer to enforce that the state is deeply\n    // immutable. Note that this is a runtime check, not a compile-time check.\n    // That is, unlike `Object.freeze`, this does not narrow the type\n    // recursively to `Readonly`. The equivalent in Immer is `Immutable`, but\n    // `Immutable` does not handle recursive types such as our `Json` type.\n    this.#internalState = freeze(state, true);\n    this.metadata = metadata;\n\n    this.#messenger.registerActionHandler(`${name}:getState`, () => this.state);\n\n    this.#messenger.registerInitialEventPayload({\n      eventType: `${name}:stateChange`,\n      getPayload: () => [this.state, []],\n    });\n    this.#messenger.registerInitialEventPayload({\n      eventType: `${name}:stateChanged`,\n      getPayload: () => [this.state, []],\n    });\n  }\n\n  /**\n   * Retrieves current controller state.\n   *\n   * @returns The current state.\n   */\n  get state(): ControllerState {\n    return this.#internalState;\n  }\n\n  set state(_) {\n    throw new Error(\n      `Controller state cannot be directly mutated; use 'update' method instead.`,\n    );\n  }\n\n  /**\n   * Updates controller state. Accepts a callback that is passed a draft copy\n   * of the controller state. If a value is returned, it is set as the new\n   * state. Otherwise, any changes made within that callback to the draft are\n   * applied to the controller state.\n   *\n   * @param callback - Callback for updating state, passed a draft state\n   * object. Return a new state object or mutate the draft to update state.\n   * @returns An object that has the next state, patches applied in the update and inverse patches to\n   * rollback the update.\n   */\n  protected update(\n    callback: (state: Draft<ControllerState>) => void | ControllerState,\n  ): {\n    nextState: ControllerState;\n    patches: Patch[];\n    inversePatches: Patch[];\n  } {\n    // We run into ts2589, \"infinite type depth\", if we don't cast\n    // produceWithPatches here.\n    const [nextState, patches, inversePatches] = (\n      produceWithPatches as unknown as (\n        state: ControllerState,\n        callbackFn: typeof callback,\n      ) => [ControllerState, Patch[], Patch[]]\n    )(this.#internalState, callback);\n\n    // Protect against unnecessary state updates when there is no state diff.\n    if (patches.length > 0) {\n      this.#internalState = nextState;\n      this.#messenger.publish(\n        `${this.name}:stateChange` as const,\n        nextState,\n        patches,\n      );\n      this.#messenger.publish(\n        `${this.name}:stateChanged` as const,\n        nextState,\n        patches,\n      );\n    }\n\n    return { nextState, patches, inversePatches };\n  }\n\n  /**\n   * Applies immer patches to the current state. The patches come from the\n   * update function itself and can either be normal or inverse patches.\n   *\n   * @param patches - An array of immer patches that are to be applied to make\n   * or undo changes.\n   */\n  protected applyPatches(patches: Patch[]): void {\n    const nextState = applyPatches(this.#internalState, patches);\n    this.#internalState = nextState;\n    this.#messenger.publish(\n      `${this.name}:stateChange` as const,\n      nextState,\n      patches,\n    );\n    this.#messenger.publish(\n      `${this.name}:stateChanged` as const,\n      nextState,\n      patches,\n    );\n  }\n\n  /**\n   * Prepares the controller for garbage collection. This should be extended\n   * by any subclasses to clean up any additional connections or events.\n   *\n   * The only cleanup performed here is to remove listeners. While technically\n   * this is not required to ensure this instance is garbage collected, it at\n   * least ensures this instance won't be responsible for preventing the\n   * listeners from being garbage collected.\n   */\n  protected destroy(): void {\n    this.messenger.clearEventSubscriptions(`${this.name}:stateChange`);\n    this.messenger.clearEventSubscriptions(`${this.name}:stateChanged`);\n  }\n}\n\n/**\n * Use the metadata to derive state according to the given metadata property.\n *\n * @param state - The full controller state.\n * @param metadata - The controller metadata.\n * @param metadataProperty - The metadata property to use to derive state.\n * @param captureException - Reports an error to an error monitoring service.\n * @returns The metadata-derived controller state.\n */\nexport function deriveStateFromMetadata<\n  ControllerState extends StateConstraint,\n>(\n  state: ControllerState,\n  metadata: StateMetadata<ControllerState>,\n  metadataProperty: keyof StatePropertyMetadata<Json>,\n  captureException?: (error: Error) => void,\n): Record<keyof ControllerState, Json> {\n  return (Object.keys(state) as (keyof ControllerState)[]).reduce<\n    Record<keyof ControllerState, Json>\n  >((derivedState, key) => {\n    try {\n      const stateMetadata = metadata[key];\n      if (!stateMetadata) {\n        throw new Error(`No metadata found for '${String(key)}'`);\n      }\n      const propertyMetadata = stateMetadata[metadataProperty];\n      const stateProperty = state[key];\n      if (typeof propertyMetadata === 'function') {\n        derivedState[key] = propertyMetadata(stateProperty);\n      } else if (propertyMetadata) {\n        derivedState[key] = stateProperty;\n      }\n      return derivedState;\n    } catch (error) {\n      // Capture error without interrupting state-related operations\n      // See [ADR core#0016](https://github.com/MetaMask/decisions/blob/main/decisions/core/0016-core-classes-error-reporting.md)\n      if (captureException) {\n        try {\n          captureException(\n            error instanceof Error ? error : new Error(String(error)),\n          );\n        } catch (captureExceptionError) {\n          console.error(\n            new Error(`Error thrown when calling 'captureException'`),\n            captureExceptionError,\n          );\n          console.error(error);\n        }\n      } else {\n        console.error(error);\n      }\n      return derivedState;\n    }\n  }, {} as never);\n}\n"]}