UNPKG

@larscom/ngrx-store-storagesync

Version:

Highly configurable state sync library between localStorage/sessionStorage and @ngrx/store (Angular)

226 lines (218 loc) 7.67 kB
import { clone, mergeDeepRight } from 'ramda'; const INIT_ACTION = '@ngrx/store/init'; const INIT_ACTION_EFFECTS = '@ngrx/effects/init'; const UPDATE_ACTION = '@ngrx/store/update-reducers'; const isObjectLike = (value) => typeof value === 'object' && value !== null; const isPlainObject = (value) => value?.constructor === Object; const isPlainObjectAndEmpty = (value) => isPlainObject(value) && Object.keys(value).length === 0; const dateMatcher = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/; /** * @internal Restores the resolved state from a storage location */ const rehydrateState = ({ storage, storageKeySerializer, features, storageError }) => { const rehydratedState = features.reduce((acc, curr) => { const { storageKeySerializerForFeature, stateKey, deserialize, storageForFeature } = curr; const key = storageKeySerializerForFeature ? storageKeySerializerForFeature(stateKey) : storageKeySerializer(stateKey); try { const featureState = storageForFeature ? storageForFeature.getItem(key) : storage.getItem(key); return featureState ? { ...acc, ...{ [stateKey]: deserialize ? deserialize(featureState) : JSON.parse(featureState, (_, value) => { return dateMatcher.test(String(value)) && !isNaN(Date.parse(value)) ? new Date(value) : value; }) } } : acc; } catch (e) { if (storageError) { storageError(e); } else { throw e; } } }, Object()); return !isPlainObjectAndEmpty(rehydratedState) ? rehydratedState : undefined; }; /** * @internal Remove empty objects */ const removeEmptyObjects = (object) => { for (const key in object) { if (!isPlainObject(object[key])) { continue; } if (!isPlainObjectAndEmpty(object[key])) { removeEmptyObjects(object[key]); } if (isPlainObjectAndEmpty(object[key])) { delete object[key]; } } return object; }; /** * @internal Exclude properties from featureState */ const excludePropsFromState = (featureState, excludeKeys) => { if (!excludeKeys || !excludeKeys.length) { return featureState; } const keyPairs = excludeKeys.map((key) => ({ leftKey: key.split('.')[0], rightKey: key.split('.')[1] })); for (const key in featureState) { const keyPair = keyPairs.find((pair) => pair.leftKey === key); const leftKey = keyPair?.leftKey; const rightKey = keyPair?.rightKey; if (isObjectLike(featureState[key])) { if (leftKey && rightKey) { excludePropsFromState(featureState[key], [...excludeKeys, rightKey]); } else if (leftKey) { delete featureState[key]; } else { excludePropsFromState(featureState[key], excludeKeys); } } else if (leftKey) { delete featureState[key]; } } return removeEmptyObjects(featureState); }; /** * @internal Sync state with storage */ const syncWithStorage = (state, { features, storage, storageKeySerializer, storageError }) => { features .filter(({ stateKey }) => state[stateKey] !== undefined) .filter(({ stateKey, shouldSync }) => (shouldSync ? shouldSync(state[stateKey], state) : true)) .forEach(({ stateKey, excludeKeys, storageKeySerializerForFeature, serialize, storageForFeature }) => { const featureStateClone = clone(state[stateKey]); const featureState = excludePropsFromState(featureStateClone, excludeKeys); if (isPlainObjectAndEmpty(featureState)) { return; } const key = storageKeySerializerForFeature ? storageKeySerializerForFeature(stateKey) : storageKeySerializer(stateKey); const value = serialize ? serialize(featureState) : JSON.stringify(featureState); try { if (storageForFeature) { storageForFeature.setItem(key, value); } else { storage.setItem(key, value); } } catch (e) { if (storageError) { storageError(e); } else { throw e; } } }); }; /** * The StorageSync Meta Reducer for @ngrx/store. * * @param options The configuration for the meta reducer * * Check out github for more information. * @see https://github.com/larscom/ngrx-store-storagesync * * @returns the meta reducer function */ const storageSync = (options) => (reducer) => { const config = { rehydrate: true, storageKeySerializer: (key) => key, rehydrateStateMerger: (nextState, rehydratedState) => mergeDeepRight(nextState, rehydratedState), ...options }; const { rehydrate, rehydrateStateMerger } = config; const shouldRehydrate = rehydrate && isCompatibleVersion(config); const rehydratedState = shouldRehydrate ? rehydrateState(config) : undefined; return (state, action) => { const nextState = action.type === INIT_ACTION ? reducer(state, action) : { ...state }; const shouldMerge = rehydratedState !== undefined && [INIT_ACTION, UPDATE_ACTION].includes(action.type); const mergedState = reducer(shouldMerge ? rehydrateStateMerger(nextState, rehydratedState) : nextState, action); if (![INIT_ACTION, INIT_ACTION_EFFECTS].includes(action.type)) { updateNewVersion(config); syncWithStorage(mergedState, config); } return mergedState; }; }; /** * @internal Load version from storage to see if it matches the * version from the config * * @examples * Storage.version = 1 and Config.version = 2 --> incompatible, skip hydration * * Storage.version = undefined and Config.version = 1 --> incompatible, skip hydration * * Storage.version = 1 and Config.version = undefined --> unknown, incompatible, skip hydration * * Storage.version = 1 and Config.version = 1 --> compatible, hydrate */ const isCompatibleVersion = ({ storage, storageError, storageKeySerializer, version, versionKey = 'ngrx-store-storagesync.version' }) => { const key = storageKeySerializer(versionKey); try { const item = storage.getItem(key); if (item == null && version == null) { return true; } return Number(item) === version; } catch (e) { if (storageError) { storageError(e); } else { throw e; } } return false; }; /** * @internal Update Storage with new config version * Remove item from Storage if version from config is undefined */ const updateNewVersion = ({ storage, storageError, storageKeySerializer, version, versionKey = 'ngrx-store-storagesync.version' }) => { const key = storageKeySerializer(versionKey); try { if (version) { storage.setItem(key, String(version)); } else { storage.removeItem(key); } } catch (e) { if (storageError) { storageError(e); } else { throw e; } } }; /** * Generated bundle index. Do not edit. */ export { storageSync }; //# sourceMappingURL=larscom-ngrx-store-storagesync.mjs.map