// @flow import Logger from 'jitsi-meet-logger'; import md5 from 'js-md5'; const logger = Logger.getLogger(__filename); /** * The name of the localStorage store where the app persists its values to. */ const PERSISTED_STATE_NAME = 'jitsi-state'; /** * Mixed type of the element (subtree) config. If it's a boolean, * (and is true) we persist the entire subtree. If it's an object, * we perist a filtered subtree based on the properties in the * config object. */ declare type ElementConfig = Object | boolean; /** * The type of the name-config pairs stored in this reducer. */ 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; _elements: PersistencyConfigMap; /** * Initializes a new {@ code PersistenceRegistry} instance. */ constructor() { this._elements = {}; } /** * Returns the persisted redux state. This function takes the * {@link #_elements} into account as we may have persisted something in the * past that we don't want to retreive anymore. The next * {@link #persistState} will remove those values. * * @returns {Object} */ getPersistedState() { let filteredPersistedState = {}; let persistedState = window.localStorage.getItem(PERSISTED_STATE_NAME); if (persistedState) { try { persistedState = JSON.parse(persistedState); } catch (error) { logger.error( 'Error parsing persisted state', persistedState, error); persistedState = {}; } filteredPersistedState = this._getFilteredState(persistedState); } this._checksum = this._calculateChecksum(filteredPersistedState); 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 newCheckSum = this._calculateChecksum(filteredState); if (newCheckSum !== this._checksum) { try { window.localStorage.setItem( PERSISTED_STATE_NAME, JSON.stringify(filteredState)); logger.info( `redux state persisted. ${this._checksum} -> ${ newCheckSum}`); this._checksum = newCheckSum; } catch (error) { logger.error('Error persisting redux state', error); } } } /** * 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 object, or boolean * if the entire subtree needs to be persisted. * @returns {void} */ register(name: string, config?: ElementConfig = true) { this._elements[name] = config; } /** * Calculates the checksum of the current or the new values of the state. * * @private * @param {Object} filteredState - The filtered/persisted redux state. * @returns {string} */ _calculateChecksum(filteredState: Object) { try { return md5.hex(JSON.stringify(filteredState) || ''); } catch (error) { logger.error( 'Error calculating checksum for state', filteredState, error); return ''; } } /** * Prepares a filtered state from the actual or the persisted redux state, * based on this registry. * * @private * @param {Object} state - The actual or persisted redux state. * @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. * * @private * @param {Object} subtree - The redux state subtree. * @param {ElementConfig} subtreeConfig - The related config. * @returns {Object} */ _getFilteredSubtree(subtree, subtreeConfig) { let filteredSubtree; if (subtreeConfig === true) { // we persist the entire subtree filteredSubtree = subtree; } else if (typeof subtreeConfig === 'object') { // only a filtered subtree gets persisted, based on the // subtreeConfig object. filteredSubtree = {}; for (const persistedKey of Object.keys(subtree)) { if (subtreeConfig[persistedKey]) { filteredSubtree[persistedKey] = subtree[persistedKey]; } } } return filteredSubtree; } } export default new PersistenceRegistry();