UNPKG

@metamask/base-controller

Version:

Provides scaffolding for controllers as well a communication system for all controllers

187 lines 8.79 kB
"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _BaseController_internalState; Object.defineProperty(exports, "__esModule", { value: true }); exports.getPersistentState = exports.getAnonymizedState = exports.BaseController = exports.isBaseController = void 0; const immer_1 = require("immer"); (0, immer_1.enablePatches)(); /** * Determines if the given controller is an instance of `BaseController` * * @param controller - Controller instance to check * @returns True if the controller is an instance of `BaseController` */ function isBaseController(controller) { return (typeof controller === 'object' && controller !== null && 'name' in controller && typeof controller.name === 'string' && 'state' in controller && typeof controller.state === 'object' && 'metadata' in controller && typeof controller.metadata === 'object'); } exports.isBaseController = isBaseController; /** * Controller class that provides state management, subscriptions, and state metadata */ class BaseController { /** * Creates a BaseController instance. * * @param options - Controller options. * @param options.messenger - Controller messaging system. * @param options.metadata - ControllerState metadata, describing how to "anonymize" the state, and which * parts should be persisted. * @param options.name - The name of the controller, used as a namespace for events and actions. * @param options.state - Initial controller state. */ constructor({ messenger, metadata, name, state, }) { _BaseController_internalState.set(this, void 0); this.messagingSystem = messenger; this.name = name; // Here we use `freeze` from Immer to enforce that the state is deeply // immutable. Note that this is a runtime check, not a compile-time check. // That is, unlike `Object.freeze`, this does not narrow the type // recursively to `Readonly`. The equivalent in Immer is `Immutable`, but // `Immutable` does not handle recursive types such as our `Json` type. __classPrivateFieldSet(this, _BaseController_internalState, (0, immer_1.freeze)(state, true), "f"); this.metadata = metadata; this.messagingSystem.registerActionHandler(`${name}:getState`, () => this.state); this.messagingSystem.registerInitialEventPayload({ eventType: `${name}:stateChange`, getPayload: () => [this.state, []], }); } /** * Retrieves current controller state. * * @returns The current state. */ get state() { return __classPrivateFieldGet(this, _BaseController_internalState, "f"); } set state(_) { throw new Error(`Controller state cannot be directly mutated; use 'update' method instead.`); } /** * Updates controller state. Accepts a callback that is passed a draft copy * of the controller state. If a value is returned, it is set as the new * state. Otherwise, any changes made within that callback to the draft are * applied to the controller state. * * @param callback - Callback for updating state, passed a draft state * object. Return a new state object or mutate the draft to update state. * @returns An object that has the next state, patches applied in the update and inverse patches to * rollback the update. */ update(callback) { // We run into ts2589, "infinite type depth", if we don't cast // produceWithPatches here. const [nextState, patches, inversePatches] = immer_1.produceWithPatches(__classPrivateFieldGet(this, _BaseController_internalState, "f"), callback); // Protect against unnecessary state updates when there is no state diff. if (patches.length > 0) { __classPrivateFieldSet(this, _BaseController_internalState, nextState, "f"); this.messagingSystem.publish(`${this.name}:stateChange`, nextState, patches); } return { nextState, patches, inversePatches }; } /** * Applies immer patches to the current state. The patches come from the * update function itself and can either be normal or inverse patches. * * @param patches - An array of immer patches that are to be applied to make * or undo changes. */ applyPatches(patches) { const nextState = (0, immer_1.applyPatches)(__classPrivateFieldGet(this, _BaseController_internalState, "f"), patches); __classPrivateFieldSet(this, _BaseController_internalState, nextState, "f"); this.messagingSystem.publish(`${this.name}:stateChange`, nextState, patches); } /** * Prepares the controller for garbage collection. This should be extended * by any subclasses to clean up any additional connections or events. * * The only cleanup performed here is to remove listeners. While technically * this is not required to ensure this instance is garbage collected, it at * least ensures this instance won't be responsible for preventing the * listeners from being garbage collected. */ destroy() { this.messagingSystem.clearEventSubscriptions(`${this.name}:stateChange`); } } exports.BaseController = BaseController; _BaseController_internalState = new WeakMap(); /** * Returns an anonymized representation of the controller state. * * By "anonymized" we mean that it should not contain any information that could be personally * identifiable. * * @param state - The controller state. * @param metadata - The controller state metadata, which describes how to derive the * anonymized state. * @returns The anonymized controller state. */ function getAnonymizedState(state, metadata) { return deriveStateFromMetadata(state, metadata, 'anonymous'); } exports.getAnonymizedState = getAnonymizedState; /** * Returns the subset of state that should be persisted. * * @param state - The controller state. * @param metadata - The controller state metadata, which describes which pieces of state should be persisted. * @returns The subset of controller state that should be persisted. */ function getPersistentState(state, metadata) { return deriveStateFromMetadata(state, metadata, 'persist'); } exports.getPersistentState = getPersistentState; /** * Use the metadata to derive state according to the given metadata property. * * @param state - The full controller state. * @param metadata - The controller metadata. * @param metadataProperty - The metadata property to use to derive state. * @returns The metadata-derived controller state. */ function deriveStateFromMetadata(state, metadata, metadataProperty) { return Object.keys(state).reduce((derivedState, key) => { try { const stateMetadata = metadata[key]; if (!stateMetadata) { throw new Error(`No metadata found for '${String(key)}'`); } const propertyMetadata = stateMetadata[metadataProperty]; const stateProperty = state[key]; if (typeof propertyMetadata === 'function') { derivedState[key] = propertyMetadata(stateProperty); } else if (propertyMetadata) { derivedState[key] = stateProperty; } return derivedState; } catch (error) { // Throw error after timeout so that it is captured as a console error // (and by Sentry) without interrupting state-related operations setTimeout(() => { throw error; }); return derivedState; } }, {}); } //# sourceMappingURL=BaseController.cjs.map