ngrx-store-localstorage
Version:
State and local storage syncing for @ngrx/store
248 lines (244 loc) • 10.1 kB
JavaScript
import deepmerge from 'deepmerge';
// Cannot import from the @ngrx/store package due to a module resolution issue.
// See Issue #206.
const INIT_ACTION = '@ngrx/store/init';
const UPDATE_ACTION = '@ngrx/store/update-reducers';
const detectDate = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/;
// correctly parse dates from local storage
const dateReviver = (_key, value) => {
if (typeof value === 'string' && detectDate.test(value)) {
return new Date(value);
}
return value;
};
const dummyReviver = (_key, value) => value;
const checkIsBrowserEnv = () => {
return typeof window !== 'undefined';
};
const validateStateKeys = (keys) => {
return keys.map((key) => {
let attr = key;
if (typeof key === 'object') {
attr = Object.keys(key)[0];
}
if (typeof attr !== 'string') {
throw new TypeError(`localStorageSync Unknown Parameter Type: ` + `Expected type of string, got ${typeof attr}`);
}
return key;
});
};
const rehydrateApplicationState = (keys, storage, storageKeySerializer, restoreDates) => {
return keys.reduce((acc, curr) => {
let key = curr;
let reviver = restoreDates ? dateReviver : dummyReviver;
let deserialize;
let decrypt;
if (typeof key === 'object') {
key = Object.keys(key)[0];
// use the custom reviver function
if (typeof curr[key] === 'function') {
reviver = curr[key];
}
else {
// use custom reviver function if available
if (curr[key].reviver) {
reviver = curr[key].reviver;
}
// use custom serialize function if available
if (curr[key].deserialize) {
deserialize = curr[key].deserialize;
}
}
// Ensure that encrypt and decrypt functions are both present
if (curr[key].encrypt && curr[key].decrypt) {
if (typeof curr[key].encrypt === 'function' && typeof curr[key].decrypt === 'function') {
decrypt = curr[key].decrypt;
}
else {
console.error(`Either encrypt or decrypt is not a function on '${curr[key]}' key object.`);
}
}
else if (curr[key].encrypt || curr[key].decrypt) {
// Let know that one of the encryption functions is not provided
console.error(`Either encrypt or decrypt function is not present on '${curr[key]}' key object.`);
}
}
if (storage !== undefined) {
let stateSlice = storage.getItem(storageKeySerializer(key));
if (stateSlice) {
// Use provided decrypt function
if (decrypt) {
stateSlice = decrypt(stateSlice);
}
const isObjectRegex = new RegExp('{|\\[');
let raw = stateSlice;
if (stateSlice === 'null' || stateSlice === 'true' || stateSlice === 'false' || isObjectRegex.test(stateSlice.charAt(0))) {
raw = JSON.parse(stateSlice, reviver);
}
return Object.assign({}, acc, {
[key]: deserialize ? deserialize(raw) : raw,
});
}
}
return acc;
}, {});
};
// Recursively traverse all properties of the existing slice as defined by the `filter` argument,
// and output the new object with extraneous properties removed.
function createStateSlice(existingSlice, filter) {
return filter.reduce((memo, attr) => {
if (typeof attr === 'string' || typeof attr === 'number') {
const value = existingSlice?.[attr];
if (value !== undefined) {
memo[attr] = value;
}
}
else {
for (const key in attr) {
if (Object.prototype.hasOwnProperty.call(attr, key)) {
const element = attr[key];
memo[key] = createStateSlice(existingSlice[key], element);
}
}
}
return memo;
}, {});
}
const syncStateUpdate = (state, keys, storage, storageKeySerializer, removeOnUndefined, syncCondition) => {
if (syncCondition) {
try {
if (syncCondition(state) !== true) {
return;
}
}
catch (e) {
// Treat TypeError as do not sync
if (e instanceof TypeError) {
return;
}
throw e;
}
}
keys.forEach((key) => {
let stateSlice = state[key];
let replacer;
let space;
let encrypt;
if (typeof key === 'object') {
let name = Object.keys(key)[0];
stateSlice = state[name];
if (typeof stateSlice !== 'undefined' && key[name]) {
// use serialize function if specified.
if (key[name].serialize) {
stateSlice = key[name].serialize(stateSlice);
}
else {
// if serialize function is not specified filter on fields if an array has been provided.
let filter;
if (key[name].reduce) {
filter = key[name];
}
else if (key[name].filter) {
filter = key[name].filter;
}
if (filter) {
stateSlice = createStateSlice(stateSlice, filter);
}
// Check if encrypt and decrypt are present, also checked at this#rehydrateApplicationState()
if (key[name].encrypt && key[name].decrypt) {
if (typeof key[name].encrypt === 'function') {
encrypt = key[name].encrypt;
}
}
else if (key[name].encrypt || key[name].decrypt) {
// If one of those is not present, then let know that one is missing
console.error(`Either encrypt or decrypt function is not present on '${key[name]}' key object.`);
}
}
/*
Replacer and space arguments to pass to JSON.stringify.
If these fields don't exist, undefined will be passed.
*/
replacer = key[name].replacer;
space = key[name].space;
}
key = name;
}
if (typeof stateSlice !== 'undefined' && storage !== undefined) {
try {
if (encrypt) {
// ensure that a string message is passed
stateSlice = encrypt(typeof stateSlice === 'string' ? stateSlice : JSON.stringify(stateSlice, replacer, space));
}
storage.setItem(storageKeySerializer(key), typeof stateSlice === 'string' ? stateSlice : JSON.stringify(stateSlice, replacer, space));
}
catch (e) {
console.warn('Unable to save state to localStorage:', e);
}
}
else if (typeof stateSlice === 'undefined' && removeOnUndefined && storage !== undefined) {
try {
storage.removeItem(storageKeySerializer(key));
}
catch (e) {
console.warn(`Exception on removing/cleaning undefined '${key}' state`, e);
}
}
});
};
// Default merge strategy is a full deep merge.
const defaultMergeReducer = (state, rehydratedState, action) => {
if ((action.type === INIT_ACTION || action.type === UPDATE_ACTION) && rehydratedState) {
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray;
const options = {
arrayMerge: overwriteMerge,
};
state = deepmerge(state, rehydratedState, options);
}
return state;
};
const localStorageSync = (config) => (reducer) => {
if ((config.storage === undefined && !config.checkStorageAvailability) ||
(config.checkStorageAvailability && checkIsBrowserEnv())) {
config.storage = localStorage || window.localStorage;
}
if (config.storageKeySerializer === undefined) {
config.storageKeySerializer = (key) => key;
}
if (config.restoreDates === undefined) {
config.restoreDates = true;
}
// Use default merge reducer.
let mergeReducer = config.mergeReducer;
if (mergeReducer === undefined || typeof mergeReducer !== 'function') {
mergeReducer = defaultMergeReducer;
}
const stateKeys = validateStateKeys(config.keys);
const rehydratedState = config.rehydrate
? rehydrateApplicationState(stateKeys, config.storage, config.storageKeySerializer, config.restoreDates)
: undefined;
return function (state, action) {
let nextState;
// If state arrives undefined, we need to let it through the supplied reducer
// in order to get a complete state as defined by user
if (action.type === INIT_ACTION && !state) {
nextState = reducer(state, action);
}
else {
nextState = { ...state };
}
// Merge the store state with the rehydrated state using
// either a user-defined reducer or the default.
nextState = mergeReducer(nextState, rehydratedState, action);
nextState = reducer(nextState, action);
if (action.type !== INIT_ACTION) {
syncStateUpdate(nextState, stateKeys, config.storage, config.storageKeySerializer, config.removeOnUndefined, config.syncCondition);
}
return nextState;
};
};
/**
* Generated bundle index. Do not edit.
*/
export { dateReviver, defaultMergeReducer, localStorageSync, rehydrateApplicationState, syncStateUpdate };
//# sourceMappingURL=ngrx-store-localstorage.mjs.map