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.ts 9.1KB

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