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

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