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 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. // @ts-ignore
  2. import { jitsiLocalStorage } from '@jitsi/js-utils';
  3. // eslint-disable-next-line lines-around-comment
  4. // @ts-ignore
  5. import { safeJsonParse } from '@jitsi/js-utils/json';
  6. import md5 from 'js-md5';
  7. import logger from './logger';
  8. declare let __DEV__: any;
  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 = '';
  25. _defaultStates: { [name: string ]: Object | undefined; } = {};
  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 retrieve anymore. The next {@link #persistState} will remove such
  31. * values.
  32. *
  33. * @returns {Object}
  34. */
  35. getPersistedState() {
  36. const filteredPersistedState: any = {};
  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. const persistedSubtree
  42. = this._getPersistedSubtree(
  43. subtreeName,
  44. this._elements[subtreeName],
  45. this._defaultStates[subtreeName]);
  46. if (persistedSubtree !== undefined) {
  47. filteredPersistedState[subtreeName] = persistedSubtree;
  48. }
  49. }
  50. // Initialize the checksum.
  51. this._checksum = this._calculateChecksum(filteredPersistedState);
  52. if (typeof __DEV__ !== 'undefined' && __DEV__) {
  53. logger.info('redux state rehydrated as', filteredPersistedState);
  54. }
  55. return filteredPersistedState;
  56. }
  57. /**
  58. * Initiates a persist operation, but its execution will depend on the
  59. * current checksums (checks changes).
  60. *
  61. * @param {Object} state - The redux state.
  62. * @returns {void}
  63. */
  64. persistState(state: Object) {
  65. const filteredState = this._getFilteredState(state);
  66. const checksum = this._calculateChecksum(filteredState);
  67. if (checksum !== this._checksum) {
  68. for (const subtreeName of Object.keys(filteredState)) {
  69. try {
  70. jitsiLocalStorage.setItem(subtreeName, JSON.stringify(filteredState[subtreeName]));
  71. } catch (error) {
  72. logger.error('Error persisting redux subtree', subtreeName, error);
  73. }
  74. }
  75. logger.info(`redux state persisted. ${this._checksum} -> ${checksum}`);
  76. this._checksum = checksum;
  77. }
  78. }
  79. /**
  80. * Registers a new subtree config to be used for the persistency.
  81. *
  82. * @param {string} name - The name of the subtree the config belongs to.
  83. * @param {ElementConfig} config - The config {@code Object}, or
  84. * {@code boolean} if the entire subtree needs to be persisted.
  85. * @param {Object} defaultState - The default state of the component. If
  86. * it's provided, the rehydrated state will be merged with it before it gets
  87. * pushed into Redux.
  88. * @returns {void}
  89. */
  90. register(
  91. name: string,
  92. config: ElementConfig = true,
  93. defaultState?: Object) {
  94. this._elements[name] = config;
  95. this._defaultStates[name] = defaultState;
  96. }
  97. /**
  98. * Calculates the checksum of a specific state.
  99. *
  100. * @param {Object} state - The redux state to calculate the checksum of.
  101. * @private
  102. * @returns {string} The checksum of the specified {@code state}.
  103. */
  104. _calculateChecksum(state: Object) {
  105. try {
  106. return md5.hex(JSON.stringify(state) || '');
  107. } catch (error) {
  108. logger.error('Error calculating checksum for state', error);
  109. return '';
  110. }
  111. }
  112. /**
  113. * Prepares a filtered state from the actual or the persisted redux state,
  114. * based on this registry.
  115. *
  116. * @param {Object} state - The actual or persisted redux state.
  117. * @private
  118. * @returns {Object}
  119. */
  120. _getFilteredState(state: any): any {
  121. const filteredState: any = {};
  122. for (const name of Object.keys(this._elements)) {
  123. if (state[name]) {
  124. filteredState[name]
  125. = this._getFilteredSubtree(
  126. state[name],
  127. this._elements[name]);
  128. }
  129. }
  130. return filteredState;
  131. }
  132. /**
  133. * Prepares a filtered subtree based on the config for persisting or for
  134. * retrieval.
  135. *
  136. * @param {Object} subtree - The redux state subtree.
  137. * @param {ElementConfig} subtreeConfig - The related config.
  138. * @private
  139. * @returns {Object}
  140. */
  141. _getFilteredSubtree(subtree: any, subtreeConfig: any) {
  142. let filteredSubtree: any;
  143. if (typeof subtreeConfig === 'object') {
  144. // Only a filtered subtree gets persisted as specified by
  145. // subtreeConfig.
  146. filteredSubtree = {};
  147. for (const persistedKey of Object.keys(subtree)) {
  148. if (subtreeConfig[persistedKey]) {
  149. filteredSubtree[persistedKey] = subtree[persistedKey];
  150. }
  151. }
  152. } else if (subtreeConfig) {
  153. // Persist the entire subtree.
  154. filteredSubtree = subtree;
  155. }
  156. return filteredSubtree;
  157. }
  158. /**
  159. * Retrieves a persisted subtree from the storage.
  160. *
  161. * @param {string} subtreeName - The name of the subtree.
  162. * @param {Object} subtreeConfig - The config of the subtree from
  163. * {@link #_elements}.
  164. * @param {Object} subtreeDefaults - The defaults of the persisted subtree.
  165. * @private
  166. * @returns {Object}
  167. */
  168. _getPersistedSubtree(subtreeName: string, subtreeConfig: Object, subtreeDefaults?: Object) {
  169. let persistedSubtree = jitsiLocalStorage.getItem(subtreeName);
  170. if (persistedSubtree) {
  171. try {
  172. persistedSubtree = safeJsonParse(persistedSubtree);
  173. const filteredSubtree
  174. = this._getFilteredSubtree(persistedSubtree, subtreeConfig);
  175. if (filteredSubtree !== undefined) {
  176. return this._mergeDefaults(
  177. filteredSubtree, subtreeDefaults);
  178. }
  179. } catch (error) {
  180. logger.error(
  181. 'Error parsing persisted subtree',
  182. subtreeName,
  183. persistedSubtree,
  184. error);
  185. }
  186. }
  187. return undefined;
  188. }
  189. /**
  190. * Merges the persisted subtree with its defaults before rehydrating the
  191. * values.
  192. *
  193. * @private
  194. * @param {Object} subtree - The Redux subtree.
  195. * @param {?Object} defaults - The defaults, if any.
  196. * @returns {Object}
  197. */
  198. _mergeDefaults(subtree: Object, defaults?: Object) {
  199. if (!defaults) {
  200. return subtree;
  201. }
  202. // If the subtree is an array, we don't need to merge it with the
  203. // defaults, because if it has a value, it will overwrite it, and if
  204. // it's undefined, it won't be even returned, and Redux will natively
  205. // use the default values instead.
  206. if (!Array.isArray(subtree)) {
  207. return {
  208. ...defaults,
  209. ...subtree
  210. };
  211. }
  212. }
  213. }
  214. export default new PersistenceRegistry();