123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- // @flow
-
- import Bourne from '@hapi/bourne';
- import { jitsiLocalStorage } from '@jitsi/js-utils';
- import md5 from 'js-md5';
-
- import logger from './logger';
-
- declare var __DEV__;
-
- /**
- * The name of the {@code localStorage} store where the app persists its values.
- */
- const PERSISTED_STATE_NAME = 'jitsi-state';
-
- /**
- * Mixed type of the element (subtree) config. If it's a {@code boolean} (and is
- * {@code true}), we persist the entire subtree. If it's an {@code Object}, we
- * perist a filtered subtree based on the properties of the config object.
- */
- declare type ElementConfig = boolean | Object;
-
- /**
- * The type of the name-config pairs stored in {@code PersistenceRegistry}.
- */
- declare type PersistencyConfigMap = { [name: string]: ElementConfig };
-
- /**
- * A registry to allow features to register their redux store subtree to be
- * persisted and also handles the persistency calls too.
- */
- class PersistenceRegistry {
- _checksum: string;
- _defaultStates: { [name: string ]: ?Object} = {};
- _elements: PersistencyConfigMap = {};
-
- /**
- * Returns the persisted redux state. Takes the {@link #_elements} into
- * account as we may have persisted something in the past that we don't want
- * to retrieve anymore. The next {@link #persistState} will remove such
- * values.
- *
- * @returns {Object}
- */
- getPersistedState() {
- let filteredPersistedState = {};
-
- // localStorage key per feature
- for (const subtreeName of Object.keys(this._elements)) {
- // Assumes that the persisted value is stored under the same key as
- // the feature's redux state name.
- // TODO We'll need to introduce functions later that can control the
- // persist key's name. Similar to control serialization and
- // deserialization. But that should be a straightforward change.
- const persistedSubtree
- = this._getPersistedSubtree(
- subtreeName,
- this._elements[subtreeName],
- this._defaultStates[subtreeName]);
-
- if (persistedSubtree !== undefined) {
- filteredPersistedState[subtreeName] = persistedSubtree;
- }
- }
-
- // legacy
- if (Object.keys(filteredPersistedState).length === 0) {
- let persistedState = jitsiLocalStorage.getItem(PERSISTED_STATE_NAME);
-
- if (persistedState) {
- try {
- persistedState = Bourne.parse(persistedState);
- } catch (error) {
- logger.error(
- 'Error parsing persisted state',
- persistedState,
- error);
- persistedState = {};
- }
-
- filteredPersistedState = this._getFilteredState(persistedState);
-
- // Store into the new format and delete the old format so that
- // it's not used again.
- this.persistState(filteredPersistedState);
- jitsiLocalStorage.removeItem(PERSISTED_STATE_NAME);
- }
- }
-
- // Initialize the checksum.
- this._checksum = this._calculateChecksum(filteredPersistedState);
-
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
- logger.info('redux state rehydrated as', filteredPersistedState);
- }
-
- return filteredPersistedState;
- }
-
- /**
- * Initiates a persist operation, but its execution will depend on the
- * current checksums (checks changes).
- *
- * @param {Object} state - The redux state.
- * @returns {void}
- */
- persistState(state: Object) {
- const filteredState = this._getFilteredState(state);
- const checksum = this._calculateChecksum(filteredState);
-
- if (checksum !== this._checksum) {
- for (const subtreeName of Object.keys(filteredState)) {
- try {
- jitsiLocalStorage.setItem(subtreeName, JSON.stringify(filteredState[subtreeName]));
- } catch (error) {
- logger.error('Error persisting redux subtree', subtreeName, error);
- }
- }
- logger.info(`redux state persisted. ${this._checksum} -> ${checksum}`);
- this._checksum = checksum;
- }
- }
-
- /**
- * Registers a new subtree config to be used for the persistency.
- *
- * @param {string} name - The name of the subtree the config belongs to.
- * @param {ElementConfig} config - The config {@code Object}, or
- * {@code boolean} if the entire subtree needs to be persisted.
- * @param {Object} defaultState - The default state of the component. If
- * it's provided, the rehydrated state will be merged with it before it gets
- * pushed into Redux.
- * @returns {void}
- */
- register(
- name: string,
- config?: ElementConfig = true,
- defaultState?: Object) {
- this._elements[name] = config;
- this._defaultStates[name] = defaultState;
- }
-
- /**
- * Calculates the checksum of a specific state.
- *
- * @param {Object} state - The redux state to calculate the checksum of.
- * @private
- * @returns {string} The checksum of the specified {@code state}.
- */
- _calculateChecksum(state: Object) {
- try {
- return md5.hex(JSON.stringify(state) || '');
- } catch (error) {
- logger.error('Error calculating checksum for state', error);
-
- return '';
- }
- }
-
- /**
- * Prepares a filtered state from the actual or the persisted redux state,
- * based on this registry.
- *
- * @param {Object} state - The actual or persisted redux state.
- * @private
- * @returns {Object}
- */
- _getFilteredState(state: Object) {
- const filteredState = {};
-
- for (const name of Object.keys(this._elements)) {
- if (state[name]) {
- filteredState[name]
- = this._getFilteredSubtree(
- state[name],
- this._elements[name]);
- }
- }
-
- return filteredState;
- }
-
- /**
- * Prepares a filtered subtree based on the config for persisting or for
- * retrieval.
- *
- * @param {Object} subtree - The redux state subtree.
- * @param {ElementConfig} subtreeConfig - The related config.
- * @private
- * @returns {Object}
- */
- _getFilteredSubtree(subtree, subtreeConfig) {
- let filteredSubtree;
-
- if (typeof subtreeConfig === 'object') {
- // Only a filtered subtree gets persisted as specified by
- // subtreeConfig.
- filteredSubtree = {};
- for (const persistedKey of Object.keys(subtree)) {
- if (subtreeConfig[persistedKey]) {
- filteredSubtree[persistedKey] = subtree[persistedKey];
- }
- }
- } else if (subtreeConfig) {
- // Persist the entire subtree.
- filteredSubtree = subtree;
- }
-
- return filteredSubtree;
- }
-
- /**
- * Retrieves a persisted subtree from the storage.
- *
- * @param {string} subtreeName - The name of the subtree.
- * @param {Object} subtreeConfig - The config of the subtree from
- * {@link #_elements}.
- * @param {Object} subtreeDefaults - The defaults of the persisted subtree.
- * @private
- * @returns {Object}
- */
- _getPersistedSubtree(subtreeName, subtreeConfig, subtreeDefaults) {
- let persistedSubtree = jitsiLocalStorage.getItem(subtreeName);
-
- if (persistedSubtree) {
- try {
- persistedSubtree = Bourne.parse(persistedSubtree);
-
- const filteredSubtree
- = this._getFilteredSubtree(persistedSubtree, subtreeConfig);
-
- if (filteredSubtree !== undefined) {
- return this._mergeDefaults(
- filteredSubtree, subtreeDefaults);
- }
- } catch (error) {
- logger.error(
- 'Error parsing persisted subtree',
- subtreeName,
- persistedSubtree,
- error);
- }
- }
-
- return undefined;
- }
-
- /**
- * Merges the persisted subtree with its defaults before rehydrating the
- * values.
- *
- * @private
- * @param {Object} subtree - The Redux subtree.
- * @param {?Object} defaults - The defaults, if any.
- * @returns {Object}
- */
- _mergeDefaults(subtree: Object, defaults: ?Object) {
- if (!defaults) {
- return subtree;
- }
-
- // If the subtree is an array, we don't need to merge it with the
- // defaults, because if it has a value, it will overwrite it, and if
- // it's undefined, it won't be even returned, and Redux will natively
- // use the default values instead.
- if (!Array.isArray(subtree)) {
- return {
- ...defaults,
- ...subtree
- };
- }
- }
- }
-
- export default new PersistenceRegistry();
|