2018-01-17 11:19:10 +00:00
|
|
|
// @flow
|
2018-01-29 22:20:38 +00:00
|
|
|
|
2018-01-17 11:19:10 +00:00
|
|
|
import md5 from 'js-md5';
|
|
|
|
|
2018-02-27 20:21:28 +00:00
|
|
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
2018-01-17 11:19:10 +00:00
|
|
|
|
|
|
|
/**
|
2018-02-27 20:21:28 +00:00
|
|
|
* The name of the {@code localStorage} store where the app persists its values.
|
2018-01-17 11:19:10 +00:00
|
|
|
*/
|
|
|
|
const PERSISTED_STATE_NAME = 'jitsi-state';
|
|
|
|
|
2018-02-06 09:43:06 +00:00
|
|
|
/**
|
2018-02-27 20:21:28 +00:00
|
|
|
* 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.
|
2018-02-06 09:43:06 +00:00
|
|
|
*/
|
2018-02-27 20:21:28 +00:00
|
|
|
declare type ElementConfig = boolean | Object;
|
2018-02-06 09:43:06 +00:00
|
|
|
|
2018-01-17 11:19:10 +00:00
|
|
|
/**
|
2018-02-27 20:21:28 +00:00
|
|
|
* The type of the name-config pairs stored in {@code PersistenceRegistry}.
|
2018-01-17 11:19:10 +00:00
|
|
|
*/
|
2018-02-06 09:43:06 +00:00
|
|
|
declare type PersistencyConfigMap = { [name: string]: ElementConfig };
|
2018-01-17 11:19:10 +00:00
|
|
|
|
|
|
|
/**
|
2018-01-29 22:20:38 +00:00
|
|
|
* A registry to allow features to register their redux store subtree to be
|
|
|
|
* persisted and also handles the persistency calls too.
|
2018-01-17 11:19:10 +00:00
|
|
|
*/
|
2018-02-02 19:35:49 +00:00
|
|
|
class PersistenceRegistry {
|
2018-01-17 11:19:10 +00:00
|
|
|
_checksum: string;
|
2018-07-06 18:03:16 +00:00
|
|
|
_defaultStates: { [name: string ]: ?Object} = {};
|
2018-02-27 20:21:28 +00:00
|
|
|
_elements: PersistencyConfigMap = {};
|
2018-01-17 11:19:10 +00:00
|
|
|
|
|
|
|
/**
|
2018-02-27 20:21:28 +00:00
|
|
|
* 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 retreive anymore. The next {@link #persistState} will remove such
|
|
|
|
* values.
|
2018-01-17 11:19:10 +00:00
|
|
|
*
|
|
|
|
* @returns {Object}
|
|
|
|
*/
|
|
|
|
getPersistedState() {
|
|
|
|
let filteredPersistedState = {};
|
|
|
|
|
2018-02-27 20:21:28 +00:00
|
|
|
// 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,
|
2018-07-06 18:03:16 +00:00
|
|
|
this._elements[subtreeName],
|
|
|
|
this._defaultStates[subtreeName]);
|
2018-02-27 20:21:28 +00:00
|
|
|
|
|
|
|
if (persistedSubtree !== undefined) {
|
|
|
|
filteredPersistedState[subtreeName] = persistedSubtree;
|
2018-01-17 11:19:10 +00:00
|
|
|
}
|
2018-02-27 20:21:28 +00:00
|
|
|
}
|
2018-01-17 11:19:10 +00:00
|
|
|
|
2018-02-27 20:21:28 +00:00
|
|
|
// legacy
|
|
|
|
if (Object.keys(filteredPersistedState).length === 0) {
|
|
|
|
const { localStorage } = window;
|
|
|
|
let persistedState = localStorage.getItem(PERSISTED_STATE_NAME);
|
2018-02-12 22:03:05 +00:00
|
|
|
|
2018-02-27 20:21:28 +00:00
|
|
|
if (persistedState) {
|
|
|
|
try {
|
|
|
|
persistedState = JSON.parse(persistedState);
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(
|
|
|
|
'Error parsing persisted state',
|
|
|
|
persistedState,
|
|
|
|
error);
|
|
|
|
persistedState = {};
|
2018-02-12 22:03:05 +00:00
|
|
|
}
|
2018-02-27 20:21:28 +00:00
|
|
|
|
|
|
|
filteredPersistedState = this._getFilteredState(persistedState);
|
|
|
|
|
|
|
|
// Store into the new format and delete the old format so that
|
|
|
|
// it's not used again.
|
|
|
|
this.persistState(filteredPersistedState);
|
|
|
|
localStorage.removeItem(PERSISTED_STATE_NAME);
|
2018-02-12 22:03:05 +00:00
|
|
|
}
|
2018-01-17 11:19:10 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 20:21:28 +00:00
|
|
|
// Initialize the checksum.
|
2018-02-12 22:03:05 +00:00
|
|
|
this._checksum = this._calculateChecksum(filteredPersistedState);
|
|
|
|
|
2018-01-29 22:20:38 +00:00
|
|
|
logger.info('redux state rehydrated as', filteredPersistedState);
|
2018-01-17 11:19:10 +00:00
|
|
|
|
|
|
|
return filteredPersistedState;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-01-29 22:20:38 +00:00
|
|
|
* Initiates a persist operation, but its execution will depend on the
|
|
|
|
* current checksums (checks changes).
|
2018-01-17 11:19:10 +00:00
|
|
|
*
|
|
|
|
* @param {Object} state - The redux state.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
persistState(state: Object) {
|
|
|
|
const filteredState = this._getFilteredState(state);
|
2018-02-27 20:21:28 +00:00
|
|
|
const checksum = this._calculateChecksum(filteredState);
|
2018-01-17 11:19:10 +00:00
|
|
|
|
2018-02-27 20:21:28 +00:00
|
|
|
if (checksum !== this._checksum) {
|
2018-02-12 22:03:05 +00:00
|
|
|
for (const subtreeName of Object.keys(filteredState)) {
|
|
|
|
try {
|
|
|
|
window.localStorage.setItem(
|
|
|
|
subtreeName,
|
|
|
|
JSON.stringify(filteredState[subtreeName]));
|
|
|
|
} catch (error) {
|
2018-02-27 20:21:28 +00:00
|
|
|
logger.error(
|
|
|
|
'Error persisting redux subtree',
|
2018-02-12 22:03:05 +00:00
|
|
|
subtreeName,
|
|
|
|
filteredState[subtreeName],
|
2018-02-27 20:21:28 +00:00
|
|
|
error);
|
2018-02-12 22:03:05 +00:00
|
|
|
}
|
2018-01-17 11:19:10 +00:00
|
|
|
}
|
2018-02-12 22:03:05 +00:00
|
|
|
logger.info(
|
2018-02-27 20:21:28 +00:00
|
|
|
`redux state persisted. ${this._checksum} -> ${checksum}`);
|
|
|
|
this._checksum = checksum;
|
2018-01-17 11:19:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Registers a new subtree config to be used for the persistency.
|
|
|
|
*
|
|
|
|
* @param {string} name - The name of the subtree the config belongs to.
|
2018-02-27 20:21:28 +00:00
|
|
|
* @param {ElementConfig} config - The config {@code Object}, or
|
|
|
|
* {@code boolean} if the entire subtree needs to be persisted.
|
2018-07-06 18:03:16 +00:00
|
|
|
* @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.
|
2018-01-17 11:19:10 +00:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-07-06 18:03:16 +00:00
|
|
|
register(
|
|
|
|
name: string,
|
|
|
|
config?: ElementConfig = true,
|
|
|
|
defaultState?: Object) {
|
2018-01-17 11:19:10 +00:00
|
|
|
this._elements[name] = config;
|
2018-07-06 18:03:16 +00:00
|
|
|
this._defaultStates[name] = defaultState;
|
2018-01-17 11:19:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-02-27 20:21:28 +00:00
|
|
|
* Calculates the checksum of a specific state.
|
2018-01-17 11:19:10 +00:00
|
|
|
*
|
2018-02-27 20:21:28 +00:00
|
|
|
* @param {Object} state - The redux state to calculate the checksum of.
|
2018-01-17 11:19:10 +00:00
|
|
|
* @private
|
2018-02-27 20:21:28 +00:00
|
|
|
* @returns {string} The checksum of the specified {@code state}.
|
2018-01-17 11:19:10 +00:00
|
|
|
*/
|
2018-02-27 20:21:28 +00:00
|
|
|
_calculateChecksum(state: Object) {
|
2018-01-17 11:19:10 +00:00
|
|
|
try {
|
2018-02-27 20:21:28 +00:00
|
|
|
return md5.hex(JSON.stringify(state) || '');
|
2018-01-17 11:19:10 +00:00
|
|
|
} catch (error) {
|
2018-02-27 20:21:28 +00:00
|
|
|
logger.error('Error calculating checksum for state', state, error);
|
2018-01-17 11:19:10 +00:00
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-01-29 22:20:38 +00:00
|
|
|
* Prepares a filtered state from the actual or the persisted redux state,
|
|
|
|
* based on this registry.
|
2018-01-17 11:19:10 +00:00
|
|
|
*
|
|
|
|
* @param {Object} state - The actual or persisted redux state.
|
2018-02-27 20:21:28 +00:00
|
|
|
* @private
|
2018-01-17 11:19:10 +00:00
|
|
|
* @returns {Object}
|
|
|
|
*/
|
|
|
|
_getFilteredState(state: Object) {
|
|
|
|
const filteredState = {};
|
|
|
|
|
|
|
|
for (const name of Object.keys(this._elements)) {
|
|
|
|
if (state[name]) {
|
2018-02-27 20:21:28 +00:00
|
|
|
filteredState[name]
|
|
|
|
= this._getFilteredSubtree(
|
|
|
|
state[name],
|
|
|
|
this._elements[name]);
|
2018-01-17 11:19:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return filteredState;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-01-29 22:20:38 +00:00
|
|
|
* Prepares a filtered subtree based on the config for persisting or for
|
|
|
|
* retrieval.
|
2018-01-17 11:19:10 +00:00
|
|
|
*
|
|
|
|
* @param {Object} subtree - The redux state subtree.
|
2018-02-06 09:43:06 +00:00
|
|
|
* @param {ElementConfig} subtreeConfig - The related config.
|
2018-02-27 20:21:28 +00:00
|
|
|
* @private
|
2018-01-17 11:19:10 +00:00
|
|
|
* @returns {Object}
|
|
|
|
*/
|
|
|
|
_getFilteredSubtree(subtree, subtreeConfig) {
|
2018-02-06 09:43:06 +00:00
|
|
|
let filteredSubtree;
|
|
|
|
|
2018-02-27 20:21:28 +00:00
|
|
|
if (typeof subtreeConfig === 'object') {
|
|
|
|
// Only a filtered subtree gets persisted as specified by
|
|
|
|
// subtreeConfig.
|
2018-02-06 09:43:06 +00:00
|
|
|
filteredSubtree = {};
|
|
|
|
for (const persistedKey of Object.keys(subtree)) {
|
|
|
|
if (subtreeConfig[persistedKey]) {
|
|
|
|
filteredSubtree[persistedKey] = subtree[persistedKey];
|
|
|
|
}
|
2018-01-17 11:19:10 +00:00
|
|
|
}
|
2018-02-27 20:21:28 +00:00
|
|
|
} else if (subtreeConfig) {
|
|
|
|
// Persist the entire subtree.
|
|
|
|
filteredSubtree = subtree;
|
2018-01-17 11:19:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return filteredSubtree;
|
|
|
|
}
|
2018-02-27 20:21:28 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Retreives 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}.
|
2018-07-06 18:03:16 +00:00
|
|
|
* @param {Object} subtreeDefaults - The defaults of the persisted subtree.
|
2018-02-27 20:21:28 +00:00
|
|
|
* @private
|
|
|
|
* @returns {Object}
|
|
|
|
*/
|
2018-07-06 18:03:16 +00:00
|
|
|
_getPersistedSubtree(subtreeName, subtreeConfig, subtreeDefaults) {
|
2018-02-27 20:21:28 +00:00
|
|
|
let persistedSubtree = window.localStorage.getItem(subtreeName);
|
|
|
|
|
|
|
|
if (persistedSubtree) {
|
|
|
|
try {
|
|
|
|
persistedSubtree = JSON.parse(persistedSubtree);
|
|
|
|
|
|
|
|
const filteredSubtree
|
|
|
|
= this._getFilteredSubtree(persistedSubtree, subtreeConfig);
|
|
|
|
|
|
|
|
if (filteredSubtree !== undefined) {
|
2018-07-06 18:03:16 +00:00
|
|
|
return this._mergeDefaults(
|
|
|
|
filteredSubtree, subtreeDefaults);
|
2018-02-27 20:21:28 +00:00
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(
|
|
|
|
'Error parsing persisted subtree',
|
|
|
|
subtreeName,
|
|
|
|
persistedSubtree,
|
|
|
|
error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
2018-07-06 18:03:16 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2018-01-17 11:19:10 +00:00
|
|
|
}
|
|
|
|
|
2018-02-02 19:35:49 +00:00
|
|
|
export default new PersistenceRegistry();
|