You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PersistenceRegistry.js 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. // @flow
  2. import Bourne from '@hapi/bourne';
  3. import { jitsiLocalStorage } from '@jitsi/js-utils';
  4. import md5 from 'js-md5';
  5. import logger from './logger';
  6. declare var __DEV__;
  7. /**
  8. * The name of the {@code localStorage} store where the app persists its values.
  9. */
  10. const PERSISTED_STATE_NAME = 'jitsi-state';
  11. /**
  12. * Mixed type of the element (subtree) config. If it's a {@code boolean} (and is
  13. * {@code true}), we persist the entire subtree. If it's an {@code Object}, we
  14. * perist a filtered subtree based on the properties of the config object.
  15. */
  16. declare type ElementConfig = boolean | Object;
  17. /**
  18. * The type of the name-config pairs stored in {@code PersistenceRegistry}.
  19. */
  20. declare type PersistencyConfigMap = { [name: string]: ElementConfig };
  21. /**
  22. * A registry to allow features to register their redux store subtree to be
  23. * persisted and also handles the persistency calls too.
  24. */
  25. class PersistenceRegistry {
  26. _checksum: string;
  27. _defaultStates: { [name: string ]: ?Object} = {};
  28. _elements: PersistencyConfigMap = {};
  29. /**
  30. * Returns the persisted redux state. Takes the {@link #_elements} into
  31. * account as we may have persisted something in the past that we don't want
  32. * to retrieve anymore. The next {@link #persistState} will remove such
  33. * values.
  34. *
  35. * @returns {Object}
  36. */
  37. getPersistedState() {
  38. let filteredPersistedState = {};
  39. // localStorage key per feature
  40. for (const subtreeName of Object.keys(this._elements)) {
  41. // Assumes that the persisted value is stored under the same key as
  42. // the feature's redux state name.
  43. // TODO We'll need to introduce functions later that can control the
  44. // persist key's name. Similar to control serialization and
  45. // deserialization. But that should be a straightforward change.
  46. const persistedSubtree
  47. = this._getPersistedSubtree(
  48. subtreeName,
  49. this._elements[subtreeName],
  50. this._defaultStates[subtreeName]);
  51. if (persistedSubtree !== undefined) {
  52. filteredPersistedState[subtreeName] = persistedSubtree;
  53. }
  54. }
  55. // legacy
  56. if (Object.keys(filteredPersistedState).length === 0) {
  57. let persistedState = jitsiLocalStorage.getItem(PERSISTED_STATE_NAME);
  58. if (persistedState) {
  59. try {
  60. persistedState = Bourne.parse(persistedState);
  61. } catch (error) {
  62. logger.error(
  63. 'Error parsing persisted state',
  64. persistedState,
  65. error);
  66. persistedState = {};
  67. }
  68. filteredPersistedState = this._getFilteredState(persistedState);
  69. // Store into the new format and delete the old format so that
  70. // it's not used again.
  71. this.persistState(filteredPersistedState);
  72. jitsiLocalStorage.removeItem(PERSISTED_STATE_NAME);
  73. }
  74. }
  75. // Initialize the checksum.
  76. this._checksum = this._calculateChecksum(filteredPersistedState);
  77. if (typeof __DEV__ !== 'undefined' && __DEV__) {
  78. logger.info('redux state rehydrated as', filteredPersistedState);
  79. }
  80. return filteredPersistedState;
  81. }
  82. /**
  83. * Initiates a persist operation, but its execution will depend on the
  84. * current checksums (checks changes).
  85. *
  86. * @param {Object} state - The redux state.
  87. * @returns {void}
  88. */
  89. persistState(state: Object) {
  90. const filteredState = this._getFilteredState(state);
  91. const checksum = this._calculateChecksum(filteredState);
  92. if (checksum !== this._checksum) {
  93. for (const subtreeName of Object.keys(filteredState)) {
  94. try {
  95. jitsiLocalStorage.setItem(subtreeName, JSON.stringify(filteredState[subtreeName]));
  96. } catch (error) {
  97. logger.error('Error persisting redux subtree', subtreeName, error);
  98. }
  99. }
  100. logger.info(`redux state persisted. ${this._checksum} -> ${checksum}`);
  101. this._checksum = checksum;
  102. }
  103. }
  104. /**
  105. * Registers a new subtree config to be used for the persistency.
  106. *
  107. * @param {string} name - The name of the subtree the config belongs to.
  108. * @param {ElementConfig} config - The config {@code Object}, or
  109. * {@code boolean} if the entire subtree needs to be persisted.
  110. * @param {Object} defaultState - The default state of the component. If
  111. * it's provided, the rehydrated state will be merged with it before it gets
  112. * pushed into Redux.
  113. * @returns {void}
  114. */
  115. register(
  116. name: string,
  117. config?: ElementConfig = true,
  118. defaultState?: Object) {
  119. this._elements[name] = config;
  120. this._defaultStates[name] = defaultState;
  121. }
  122. /**
  123. * Calculates the checksum of a specific state.
  124. *
  125. * @param {Object} state - The redux state to calculate the checksum of.
  126. * @private
  127. * @returns {string} The checksum of the specified {@code state}.
  128. */
  129. _calculateChecksum(state: Object) {
  130. try {
  131. return md5.hex(JSON.stringify(state) || '');
  132. } catch (error) {
  133. logger.error('Error calculating checksum for state', error);
  134. return '';
  135. }
  136. }
  137. /**
  138. * Prepares a filtered state from the actual or the persisted redux state,
  139. * based on this registry.
  140. *
  141. * @param {Object} state - The actual or persisted redux state.
  142. * @private
  143. * @returns {Object}
  144. */
  145. _getFilteredState(state: Object) {
  146. const filteredState = {};
  147. for (const name of Object.keys(this._elements)) {
  148. if (state[name]) {
  149. filteredState[name]
  150. = this._getFilteredSubtree(
  151. state[name],
  152. this._elements[name]);
  153. }
  154. }
  155. return filteredState;
  156. }
  157. /**
  158. * Prepares a filtered subtree based on the config for persisting or for
  159. * retrieval.
  160. *
  161. * @param {Object} subtree - The redux state subtree.
  162. * @param {ElementConfig} subtreeConfig - The related config.
  163. * @private
  164. * @returns {Object}
  165. */
  166. _getFilteredSubtree(subtree, subtreeConfig) {
  167. let filteredSubtree;
  168. if (typeof subtreeConfig === 'object') {
  169. // Only a filtered subtree gets persisted as specified by
  170. // subtreeConfig.
  171. filteredSubtree = {};
  172. for (const persistedKey of Object.keys(subtree)) {
  173. if (subtreeConfig[persistedKey]) {
  174. filteredSubtree[persistedKey] = subtree[persistedKey];
  175. }
  176. }
  177. } else if (subtreeConfig) {
  178. // Persist the entire subtree.
  179. filteredSubtree = subtree;
  180. }
  181. return filteredSubtree;
  182. }
  183. /**
  184. * Retrieves a persisted subtree from the storage.
  185. *
  186. * @param {string} subtreeName - The name of the subtree.
  187. * @param {Object} subtreeConfig - The config of the subtree from
  188. * {@link #_elements}.
  189. * @param {Object} subtreeDefaults - The defaults of the persisted subtree.
  190. * @private
  191. * @returns {Object}
  192. */
  193. _getPersistedSubtree(subtreeName, subtreeConfig, subtreeDefaults) {
  194. let persistedSubtree = jitsiLocalStorage.getItem(subtreeName);
  195. if (persistedSubtree) {
  196. try {
  197. persistedSubtree = Bourne.parse(persistedSubtree);
  198. const filteredSubtree
  199. = this._getFilteredSubtree(persistedSubtree, subtreeConfig);
  200. if (filteredSubtree !== undefined) {
  201. return this._mergeDefaults(
  202. filteredSubtree, subtreeDefaults);
  203. }
  204. } catch (error) {
  205. logger.error(
  206. 'Error parsing persisted subtree',
  207. subtreeName,
  208. persistedSubtree,
  209. error);
  210. }
  211. }
  212. return undefined;
  213. }
  214. /**
  215. * Merges the persisted subtree with its defaults before rehydrating the
  216. * values.
  217. *
  218. * @private
  219. * @param {Object} subtree - The Redux subtree.
  220. * @param {?Object} defaults - The defaults, if any.
  221. * @returns {Object}
  222. */
  223. _mergeDefaults(subtree: Object, defaults: ?Object) {
  224. if (!defaults) {
  225. return subtree;
  226. }
  227. // If the subtree is an array, we don't need to merge it with the
  228. // defaults, because if it has a value, it will overwrite it, and if
  229. // it's undefined, it won't be even returned, and Redux will natively
  230. // use the default values instead.
  231. if (!Array.isArray(subtree)) {
  232. return {
  233. ...defaults,
  234. ...subtree
  235. };
  236. }
  237. }
  238. }
  239. export default new PersistenceRegistry();