{"version":3,"file":"BaseControllerV2.mjs","sourceRoot":"","sources":["../src/BaseControllerV2.ts"],"names":[],"mappings":";;;;;;;;;;;;AACA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc;AAShF,aAAa,EAAE,CAAC;AAEhB;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAmB;IAEnB,OAAO,CACL,OAAO,UAAU,KAAK,QAAQ;QAC9B,UAAU,KAAK,IAAI;QACnB,MAAM,IAAI,UAAU;QACpB,OAAO,UAAU,CAAC,IAAI,KAAK,QAAQ;QACnC,OAAO,IAAI,UAAU;QACrB,OAAO,UAAU,CAAC,KAAK,KAAK,QAAQ;QACpC,UAAU,IAAI,UAAU;QACxB,OAAO,UAAU,CAAC,QAAQ,KAAK,QAAQ,CACxC,CAAC;AACJ,CAAC;AAiID;;GAEG;AACH,MAAM,OAAO,cAAc;IA0BzB;;;;;;;;;OASG;IACH,YAAY,EACV,SAAS,EACT,QAAQ,EACR,IAAI,EACJ,KAAK,GAMN;QAjCD,gDAAgC;QAkC9B,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,sEAAsE;QACtE,0EAA0E;QAC1E,iEAAiE;QACjE,yEAAyE;QACzE,uEAAuE;QACvE,uBAAA,IAAI,iCAAkB,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,MAAA,CAAC;QAC1C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAEzB,IAAI,CAAC,eAAe,CAAC,qBAAqB,CACxC,GAAG,IAAI,WAAW,EAClB,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CACjB,CAAC;QAEF,IAAI,CAAC,eAAe,CAAC,2BAA2B,CAAC;YAC/C,SAAS,EAAE,GAAG,IAAI,cAAc;YAChC,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;SACnC,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,IAAI,KAAK;QACP,OAAO,uBAAA,IAAI,qCAAe,CAAC;IAC7B,CAAC;IAED,IAAI,KAAK,CAAC,CAAC;QACT,MAAM,IAAI,KAAK,CACb,2EAA2E,CAC5E,CAAC;IACJ,CAAC;IAED;;;;;;;;;;OAUG;IACO,MAAM,CACd,QAAmE;QAMnE,8DAA8D;QAC9D,2BAA2B;QAC3B,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,cAAc,CAAC,GACxC,kBAID,CAAC,uBAAA,IAAI,qCAAe,EAAE,QAAQ,CAAC,CAAC;QAEjC,uBAAA,IAAI,iCAAkB,SAAS,MAAA,CAAC;QAChC,IAAI,CAAC,eAAe,CAAC,OAAO,CAC1B,GAAG,IAAI,CAAC,IAAI,cAAc,EAC1B,SAAS,EACT,OAAO,CACR,CAAC;QAEF,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;IAChD,CAAC;IAED;;;;;;OAMG;IACO,YAAY,CAAC,OAAgB;QACrC,MAAM,SAAS,GAAG,YAAY,CAAC,uBAAA,IAAI,qCAAe,EAAE,OAAO,CAAC,CAAC;QAC7D,uBAAA,IAAI,iCAAkB,SAAS,MAAA,CAAC;QAChC,IAAI,CAAC,eAAe,CAAC,OAAO,CAC1B,GAAG,IAAI,CAAC,IAAI,cAAc,EAC1B,SAAS,EACT,OAAO,CACR,CAAC;IACJ,CAAC;IAED;;;;;;;;OAQG;IACO,OAAO;QACf,IAAI,CAAC,eAAe,CAAC,uBAAuB,CAAC,GAAG,IAAI,CAAC,IAAI,cAAc,CAAC,CAAC;IAC3E,CAAC;CACF;;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAsB,EACtB,QAAwC;IAExC,OAAO,uBAAuB,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAsB,EACtB,QAAwC;IAExC,OAAO,uBAAuB,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,uBAAuB,CAC9B,KAAsB,EACtB,QAAwC,EACxC,gBAAyC;IAEzC,OAAQ,MAAM,CAAC,IAAI,CAAC,KAAK,CAA+B,CAAC,MAAM,CAE7D,CAAC,YAAY,EAAE,GAAG,EAAE,EAAE;QACtB,IAAI;YACF,MAAM,aAAa,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;YACpC,IAAI,CAAC,aAAa,EAAE;gBAClB,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;aAC3D;YACD,MAAM,gBAAgB,GAAG,aAAa,CAAC,gBAAgB,CAAC,CAAC;YACzD,MAAM,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,OAAO,gBAAgB,KAAK,UAAU,EAAE;gBAC1C,YAAY,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,aAAa,CAAC,CAAC;aACrD;iBAAM,IAAI,gBAAgB,EAAE;gBAC3B,YAAY,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC;aACnC;YACD,OAAO,YAAY,CAAC;SACrB;QAAC,OAAO,KAAK,EAAE;YACd,sEAAsE;YACtE,gEAAgE;YAChE,UAAU,CAAC,GAAG,EAAE;gBACd,MAAM,KAAK,CAAC;YACd,CAAC,CAAC,CAAC;YACH,OAAO,YAAY,CAAC;SACrB;IACH,CAAC,EAAE,EAAW,CAAC,CAAC;AAClB,CAAC","sourcesContent":["import type { Json, PublicInterface } from '@metamask/utils';\nimport { enablePatches, produceWithPatches, applyPatches, freeze } from 'immer';\nimport type { Draft, Patch } from 'immer';\n\nimport type { ActionConstraint, EventConstraint } from './Messenger';\nimport type {\n  RestrictedMessenger,\n  RestrictedMessengerConstraint,\n} from './RestrictedMessenger';\n\nenablePatches();\n\n/**\n * Determines if the given controller is an instance of `BaseController`\n *\n * @param controller - Controller instance to check\n * @returns True if the controller is an instance of `BaseController`\n */\nexport function isBaseController(\n  controller: unknown,\n): controller is BaseControllerInstance {\n  return (\n    typeof controller === 'object' &&\n    controller !== null &&\n    'name' in controller &&\n    typeof controller.name === 'string' &&\n    'state' in controller &&\n    typeof controller.state === 'object' &&\n    'metadata' in controller &&\n    typeof controller.metadata === 'object'\n  );\n}\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 */\n// TODO: Either fix this lint violation or explain why it's necessary to ignore.\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type Listener<T> = (state: T, 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 */\n// TODO: Either fix this lint violation or explain why it's necessary to ignore.\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type StateDeriver<T extends Json> = (value: T) => 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 */\n// TODO: Either fix this lint violation or explain why it's necessary to ignore.\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type StateMetadata<T extends StateConstraint> = {\n  [P in keyof T]-?: StatePropertyMetadata<T[P]>;\n};\n\n/**\n * Metadata for a single state property\n *\n * @property persist - Indicates whether this property should be persisted\n * (`true` for persistent, `false` for transient), or is set to a function\n * that derives the persistent state from the state.\n * @property anonymous - Indicates whether this property is already anonymous,\n * (`true` for anonymous, `false` if it has potential to be personally\n * identifiable), or is set to a function that returns an anonymized\n * representation of this state.\n */\n// TODO: Either fix this lint violation or explain why it's necessary to ignore.\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type StatePropertyMetadata<T extends Json> = {\n  persist: boolean | StateDeriver<T>;\n  anonymous: boolean | StateDeriver<T>;\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  [P in 'anonymous' | 'persist']: boolean | StateDeriverConstraint;\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<string, StateConstraint, RestrictedMessengerConstraint>\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\nexport type ControllerStateChangeEvent<\n  ControllerName extends string,\n  ControllerState extends StateConstraint,\n> = {\n  type: `${ControllerName}:stateChange`;\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> = ControllerStateChangeEvent<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  // TODO: Either fix this lint violation or explain why it's necessary to ignore.\n  // eslint-disable-next-line @typescript-eslint/naming-convention\n  messenger extends RestrictedMessenger<\n    ControllerName,\n    ActionConstraint | ControllerActions<ControllerName, ControllerState>,\n    EventConstraint | ControllerEvents<ControllerName, ControllerState>,\n    string,\n    string\n  >,\n> {\n  #internalState: ControllerState;\n\n  protected messagingSystem: messenger;\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 - Controller messaging system.\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: messenger;\n    metadata: StateMetadata<ControllerState>;\n    name: ControllerName;\n    state: ControllerState;\n  }) {\n    this.messagingSystem = 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.messagingSystem.registerActionHandler(\n      `${name}:getState`,\n      () => this.state,\n    );\n\n    this.messagingSystem.registerInitialEventPayload({\n      eventType: `${name}:stateChange`,\n      getPayload: () => [this.state, []],\n    });\n  }\n\n  /**\n   * Retrieves current controller state.\n   *\n   * @returns The current state.\n   */\n  get state() {\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        cb: typeof callback,\n      ) => [ControllerState, Patch[], Patch[]]\n    )(this.#internalState, callback);\n\n    this.#internalState = nextState;\n    this.messagingSystem.publish(\n      `${this.name}:stateChange`,\n      nextState,\n      patches,\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[]) {\n    const nextState = applyPatches(this.#internalState, patches);\n    this.#internalState = nextState;\n    this.messagingSystem.publish(\n      `${this.name}:stateChange`,\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() {\n    this.messagingSystem.clearEventSubscriptions(`${this.name}:stateChange`);\n  }\n}\n\n/**\n * Returns an anonymized representation of the controller state.\n *\n * By \"anonymized\" we mean that it should not contain any information that could be personally\n * identifiable.\n *\n * @param state - The controller state.\n * @param metadata - The controller state metadata, which describes how to derive the\n * anonymized state.\n * @returns The anonymized controller state.\n */\nexport function getAnonymizedState<ControllerState extends StateConstraint>(\n  state: ControllerState,\n  metadata: StateMetadata<ControllerState>,\n): Record<keyof ControllerState, Json> {\n  return deriveStateFromMetadata(state, metadata, 'anonymous');\n}\n\n/**\n * Returns the subset of state that should be persisted.\n *\n * @param state - The controller state.\n * @param metadata - The controller state metadata, which describes which pieces of state should be persisted.\n * @returns The subset of controller state that should be persisted.\n */\nexport function getPersistentState<ControllerState extends StateConstraint>(\n  state: ControllerState,\n  metadata: StateMetadata<ControllerState>,\n): Record<keyof ControllerState, Json> {\n  return deriveStateFromMetadata(state, metadata, 'persist');\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 * @returns The metadata-derived controller state.\n */\nfunction deriveStateFromMetadata<ControllerState extends StateConstraint>(\n  state: ControllerState,\n  metadata: StateMetadata<ControllerState>,\n  metadataProperty: 'anonymous' | 'persist',\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      // Throw error after timeout so that it is captured as a console error\n      // (and by Sentry) without interrupting state-related operations\n      setTimeout(() => {\n        throw error;\n      });\n      return derivedState;\n    }\n  }, {} as never);\n}\n"]}