index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import deepmerge from 'deepmerge';
  2. /**
  3. * Created by championswimmer on 22/07/17.
  4. */
  5. let MockStorage;
  6. // @ts-ignore
  7. {
  8. MockStorage = class {
  9. get length() {
  10. return Object.keys(this).length;
  11. }
  12. key(index) {
  13. return Object.keys(this)[index];
  14. }
  15. setItem(key, data) {
  16. this[key] = data.toString();
  17. }
  18. getItem(key) {
  19. return this[key];
  20. }
  21. removeItem(key) {
  22. delete this[key];
  23. }
  24. clear() {
  25. for (let key of Object.keys(this)) {
  26. delete this[key];
  27. }
  28. }
  29. };
  30. }
  31. // tslint:disable: variable-name
  32. class SimplePromiseQueue {
  33. constructor() {
  34. this._queue = [];
  35. this._flushing = false;
  36. }
  37. enqueue(promise) {
  38. this._queue.push(promise);
  39. if (!this._flushing) {
  40. return this.flushQueue();
  41. }
  42. return Promise.resolve();
  43. }
  44. flushQueue() {
  45. this._flushing = true;
  46. const chain = () => {
  47. const nextTask = this._queue.shift();
  48. if (nextTask) {
  49. return nextTask.then(chain);
  50. }
  51. else {
  52. this._flushing = false;
  53. }
  54. };
  55. return Promise.resolve(chain());
  56. }
  57. }
  58. const options = {
  59. replaceArrays: {
  60. arrayMerge: (destinationArray, sourceArray, options) => sourceArray
  61. },
  62. concatArrays: {
  63. arrayMerge: (target, source, options) => target.concat(...source)
  64. }
  65. };
  66. function merge(into, from, mergeOption) {
  67. return deepmerge(into, from, options[mergeOption]);
  68. }
  69. let FlattedJSON = JSON;
  70. /**
  71. * A class that implements the vuex persistence.
  72. * @type S type of the 'state' inside the store (default: any)
  73. */
  74. class VuexPersistence {
  75. /**
  76. * Create a {@link VuexPersistence} object.
  77. * Use the <code>plugin</code> function of this class as a
  78. * Vuex plugin.
  79. * @param {PersistOptions} options
  80. */
  81. constructor(options) {
  82. // tslint:disable-next-line:variable-name
  83. this._mutex = new SimplePromiseQueue();
  84. /**
  85. * Creates a subscriber on the store. automatically is used
  86. * when this is used a vuex plugin. Not for manual usage.
  87. * @param store
  88. */
  89. this.subscriber = (store) => (handler) => store.subscribe(handler);
  90. if (typeof options === 'undefined')
  91. options = {};
  92. this.key = ((options.key != null) ? options.key : 'vuex');
  93. this.subscribed = false;
  94. this.supportCircular = options.supportCircular || false;
  95. if (this.supportCircular) {
  96. FlattedJSON = require('flatted');
  97. }
  98. this.mergeOption = options.mergeOption || 'replaceArrays';
  99. let localStorageLitmus = true;
  100. try {
  101. window.localStorage.getItem('');
  102. }
  103. catch (err) {
  104. localStorageLitmus = false;
  105. }
  106. /**
  107. * 1. First, prefer storage sent in optinos
  108. * 2. Otherwise, use window.localStorage if available
  109. * 3. Finally, try to use MockStorage
  110. * 4. None of above? Well we gotta fail.
  111. */
  112. if (options.storage) {
  113. this.storage = options.storage;
  114. }
  115. else if (localStorageLitmus) {
  116. this.storage = window.localStorage;
  117. }
  118. else if (MockStorage) {
  119. this.storage = new MockStorage();
  120. }
  121. else {
  122. throw new Error("Neither 'window' is defined, nor 'MockStorage' is available");
  123. }
  124. /**
  125. * How this works is -
  126. * 1. If there is options.reducer function, we use that, if not;
  127. * 2. We check options.modules;
  128. * 1. If there is no options.modules array, we use entire state in reducer
  129. * 2. Otherwise, we create a reducer that merges all those state modules that are
  130. * defined in the options.modules[] array
  131. * @type {((state: S) => {}) | ((state: S) => S) | ((state: any) => {})}
  132. */
  133. this.reducer = ((options.reducer != null)
  134. ? options.reducer
  135. : ((options.modules == null)
  136. ? ((state) => state)
  137. : ((state) => options.modules.reduce((a, i) => merge(a, { [i]: state[i] }, this.mergeOption), { /* start empty accumulator*/}))));
  138. this.filter = options.filter || ((mutation) => true);
  139. this.strictMode = options.strictMode || false;
  140. this.RESTORE_MUTATION = function RESTORE_MUTATION(state, savedState) {
  141. const mergedState = merge(state, savedState || {}, this.mergeOption);
  142. for (const propertyName of Object.keys(mergedState)) {
  143. this._vm.$set(state, propertyName, mergedState[propertyName]);
  144. }
  145. };
  146. this.asyncStorage = options.asyncStorage || false;
  147. if (this.asyncStorage) {
  148. /**
  149. * Async {@link #VuexPersistence.restoreState} implementation
  150. * @type {((key: string, storage?: Storage) =>
  151. * (Promise<S> | S)) | ((key: string, storage: AsyncStorage) => Promise<any>)}
  152. */
  153. this.restoreState = ((options.restoreState != null)
  154. ? options.restoreState
  155. : ((key, storage) => (storage).getItem(key)
  156. .then((value) => typeof value === 'string' // If string, parse, or else, just return
  157. ? (this.supportCircular
  158. ? FlattedJSON.parse(value || '{}')
  159. : JSON.parse(value || '{}'))
  160. : (value || {}))));
  161. /**
  162. * Async {@link #VuexPersistence.saveState} implementation
  163. * @type {((key: string, state: {}, storage?: Storage) =>
  164. * (Promise<void> | void)) | ((key: string, state: {}, storage?: Storage) => Promise<void>)}
  165. */
  166. this.saveState = ((options.saveState != null)
  167. ? options.saveState
  168. : ((key, state, storage) => (storage).setItem(key, // Second argument is state _object_ if asyc storage, stringified otherwise
  169. // do not stringify the state if the storage type is async
  170. (this.asyncStorage
  171. ? merge({}, state || {}, this.mergeOption)
  172. : (this.supportCircular
  173. ? FlattedJSON.stringify(state)
  174. : JSON.stringify(state))))));
  175. /**
  176. * Async version of plugin
  177. * @param {Store<S>} store
  178. */
  179. this.plugin = (store) => {
  180. /**
  181. * For async stores, we're capturing the Promise returned
  182. * by the `restoreState()` function in a `restored` property
  183. * on the store itself. This would allow app developers to
  184. * determine when and if the store's state has indeed been
  185. * refreshed. This approach was suggested by GitHub user @hotdogee.
  186. * See https://github.com/championswimmer/vuex-persist/pull/118#issuecomment-500914963
  187. * @since 2.1.0
  188. */
  189. store.restored = (this.restoreState(this.key, this.storage)).then((savedState) => {
  190. /**
  191. * If in strict mode, do only via mutation
  192. */
  193. if (this.strictMode) {
  194. store.commit('RESTORE_MUTATION', savedState);
  195. }
  196. else {
  197. store.replaceState(merge(store.state, savedState || {}, this.mergeOption));
  198. }
  199. this.subscriber(store)((mutation, state) => {
  200. if (this.filter(mutation)) {
  201. this._mutex.enqueue(this.saveState(this.key, this.reducer(state), this.storage));
  202. }
  203. });
  204. this.subscribed = true;
  205. });
  206. };
  207. }
  208. else {
  209. /**
  210. * Sync {@link #VuexPersistence.restoreState} implementation
  211. * @type {((key: string, storage?: Storage) =>
  212. * (Promise<S> | S)) | ((key: string, storage: Storage) => (any | string | {}))}
  213. */
  214. this.restoreState = ((options.restoreState != null)
  215. ? options.restoreState
  216. : ((key, storage) => {
  217. const value = (storage).getItem(key);
  218. if (typeof value === 'string') { // If string, parse, or else, just return
  219. return (this.supportCircular
  220. ? FlattedJSON.parse(value || '{}')
  221. : JSON.parse(value || '{}'));
  222. }
  223. else {
  224. return (value || {});
  225. }
  226. }));
  227. /**
  228. * Sync {@link #VuexPersistence.saveState} implementation
  229. * @type {((key: string, state: {}, storage?: Storage) =>
  230. * (Promise<void> | void)) | ((key: string, state: {}, storage?: Storage) => Promise<void>)}
  231. */
  232. this.saveState = ((options.saveState != null)
  233. ? options.saveState
  234. : ((key, state, storage) => (storage).setItem(key, // Second argument is state _object_ if localforage, stringified otherwise
  235. (this.supportCircular
  236. ? FlattedJSON.stringify(state)
  237. : JSON.stringify(state)))));
  238. /**
  239. * Sync version of plugin
  240. * @param {Store<S>} store
  241. */
  242. this.plugin = (store) => {
  243. const savedState = this.restoreState(this.key, this.storage);
  244. if (this.strictMode) {
  245. store.commit('RESTORE_MUTATION', savedState);
  246. }
  247. else {
  248. store.replaceState(merge(store.state, savedState || {}, this.mergeOption));
  249. }
  250. this.subscriber(store)((mutation, state) => {
  251. if (this.filter(mutation)) {
  252. this.saveState(this.key, this.reducer(state), this.storage);
  253. }
  254. });
  255. this.subscribed = true;
  256. };
  257. }
  258. }
  259. }
  260. export default VuexPersistence;
  261. export { MockStorage, VuexPersistence };
  262. //# sourceMappingURL=index.js.map