@larscom/ngrx-store-storagesync
Version:
Highly configurable state sync library between localStorage/sessionStorage and @ngrx/store (Angular)
226 lines (218 loc) • 7.67 kB
JavaScript
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