1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-25 20:02:58 +01:00
metamask-extension/app/scripts/lib/ComposableObservableStore.js
Mark Stacey 4cf886f710
Initialize composable observable store after update (#20468)
* Initialize composable observable store after update

The composable observable store now updates state immediately when the
structure is updated. Previously each store would only be updated after
the first state change. This ensures that the composable observable
store state is always complete.

* SUpport falsy controller state

We now use the nullish coalescing operator when checkint store.state, so that we don't accidentally ignore falsy state.

Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com>

* Add test for falsy controller state

* Update state snapshots

A change on `develop` required another state update.

---------

Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com>
2023-08-17 12:22:37 -02:30

103 lines
3.2 KiB
JavaScript

import { ObservableStore } from '@metamask/obs-store';
import { getPersistentState } from '@metamask/base-controller';
/**
* @typedef {import('@metamask/base-controller').ControllerMessenger} ControllerMessenger
*/
/**
* An ObservableStore that can composes a flat
* structure of child stores based on configuration
*/
export default class ComposableObservableStore extends ObservableStore {
/**
* Describes which stores are being composed. The key is the name of the
* store, and the value is either an ObserableStore, or a controller that
* extends one of the two base controllers in the `@metamask/base-controller`
* package.
*
* @type {Record<string, object>}
*/
config = {};
/**
* Create a new store
*
* @param {object} options
* @param {object} [options.config] - Map of internal state keys to child stores
* @param {ControllerMessenger} options.controllerMessenger - The controller
* messenger, used for subscribing to events from BaseControllerV2-based
* controllers.
* @param {object} [options.state] - The initial store state
* @param {boolean} [options.persist] - Whether or not to apply the persistence for v2 controllers
*/
constructor({ config, controllerMessenger, state, persist }) {
super(state);
this.persist = persist;
this.controllerMessenger = controllerMessenger;
if (config) {
this.updateStructure(config);
}
}
/**
* Composes a new internal store subscription structure
*
* @param {Record<string, object>} config - Describes which stores are being
* composed. The key is the name of the store, and the value is either an
* ObserableStore, or a controller that extends one of the two base
* controllers in the `@metamask/base-controller` package.
*/
updateStructure(config) {
this.config = config;
this.removeAllListeners();
const initialState = {};
for (const key of Object.keys(config)) {
if (!config[key]) {
throw new Error(`Undefined '${key}'`);
}
const store = config[key];
if (store.subscribe) {
config[key].subscribe((state) => {
this.updateState({ [key]: state });
});
} else {
this.controllerMessenger.subscribe(
`${store.name}:stateChange`,
(state) => {
let updatedState = state;
if (this.persist) {
updatedState = getPersistentState(state, config[key].metadata);
}
this.updateState({ [key]: updatedState });
},
);
}
initialState[key] = store.state ?? store.getState?.();
}
this.updateState(initialState);
}
/**
* Merges all child store state into a single object rather than
* returning an object keyed by child store class name
*
* @returns {object} Object containing merged child store state
*/
getFlatState() {
if (!this.config) {
return {};
}
let flatState = {};
for (const key of Object.keys(this.config)) {
const controller = this.config[key];
const state = controller.getState
? controller.getState()
: controller.state;
flatState = { ...flatState, ...state };
}
return flatState;
}
}