UNPKG

easy-peasy

Version:

Vegetarian friendly state for React

346 lines (312 loc) 9.66 kB
import { clone, get, isPlainObject, isPromise, set, pSeries } from './lib'; import { migrate } from './migrations'; const noopStorage = { getItem: () => undefined, setItem: () => undefined, removeItem: () => undefined, }; const getBrowserStorage = (storageName) => { let storageCache; return () => { if (!storageCache) { try { if ( typeof window !== 'undefined' && typeof window[storageName] !== 'undefined' ) { storageCache = window[storageName]; } } catch (_) { // swallow the failure } if (!storageCache) { storageCache = noopStorage; } } return storageCache; }; }; const localStorage = getBrowserStorage('localStorage'); const sessionStorage = getBrowserStorage('sessionStorage'); function createStorageWrapper(storage, transformers = [], migrations = {}) { if (storage == null) { storage = sessionStorage(); } if (typeof storage === 'string') { if (storage === 'localStorage') { storage = localStorage(); } else if (storage === 'sessionStorage') { storage = sessionStorage(); } else { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.warn(`Invalid storage provider`); } storage = noopStorage; } } const outTransformers = [...transformers].reverse(); const serialize = (data) => { if (transformers.length > 0 && data != null && typeof data === 'object') { Object.keys(data).forEach((key) => { data[key] = transformers.reduce( (acc, cur) => cur.in(acc, key, data), data[key], ); }); } return storage === localStorage() || storage === sessionStorage() ? JSON.stringify({ data }) : data; }; const deserialize = (data) => { const storageData = storage === localStorage() || storage === sessionStorage() ? JSON.parse(data).data : data; const hasMigrations = Object.keys(migrations).length > 0; const result = hasMigrations ? migrate(storageData, migrations) : storageData; if ( outTransformers.length > 0 && result != null && typeof result === 'object' ) { Object.keys(result).forEach((key) => { result[key] = outTransformers.reduce( (acc, cur) => cur.out(acc, key, result), result[key], ); }); } return result; }; const isAsync = isPromise(storage.getItem('_')); return { getItem: (key) => { if (isAsync) { return storage .getItem(key) .then((wrapped) => wrapped != null ? deserialize(wrapped) : undefined, ); } const wrapped = storage.getItem(key); return wrapped != null ? deserialize(wrapped) : undefined; }, setItem: (key, data) => storage.setItem(key, serialize(data)), removeItem: (key) => storage.removeItem(key), }; } export function extractPersistConfig(path, persistdef = {}) { return { path, config: { allow: persistdef.allow || [], deny: persistdef.deny || [], mergeStrategy: persistdef.mergeStrategy || 'mergeDeep', storage: createStorageWrapper( persistdef.storage, persistdef.transformers, persistdef.migrations, ), }, }; } function resolvePersistTargets(target, allow, deny) { let targets = Object.keys(target); if (allow.length > 0) { targets = targets.reduce((acc, cur) => { if (allow.findIndex((x) => x === cur) !== -1) { return [...acc, cur]; } return acc; }, []); } if (deny.length > 0) { targets = targets.reduce((acc, cur) => { if (deny.findIndex((x) => x === cur) !== -1) { return acc; } return [...acc, cur]; }, []); } return targets; } function createPersistenceClearer(persistKey, _r) { return () => { if (_r._i._persistenceConfig.length === 0) { return Promise.resolve(); } return pSeries( _r._i._persistenceConfig.map( ({ path, config }) => () => Promise.resolve(config.storage.removeItem(persistKey(path))), ), ); }; } export function createPersistor(persistKey, _r) { let persistPromise = Promise.resolve(); let isPersisting = false; let nextPersistOperation; const timingMethod = typeof window === 'undefined' ? (fn) => fn() : window.requestIdleCallback != null ? // We need to wrap requestIdleCallback, because it doesn't work without // a second parameter on iOS with ReactNative (fn) => window.requestIdleCallback(fn, { timeout: 0 }) : window.requestAnimationFrame; const persist = (nextState) => { if (_r._i._persistenceConfig.length === 0) { return; } const operation = () => { isPersisting = true; persistPromise = new Promise((resolve) => { timingMethod(() => { pSeries( _r._i._persistenceConfig.map(({ path, config }) => () => { const { storage, allow, deny } = config; const persistRootState = clone(get(path, nextState)); const persistTargets = resolvePersistTargets( persistRootState, allow, deny, ); const stateToPersist = {}; persistTargets.map((key) => { const targetPath = [...path, key]; const rawValue = get(targetPath, nextState); const value = isPlainObject(rawValue) ? clone(rawValue) : rawValue; stateToPersist[key] = value; }); return Promise.resolve( storage.setItem(persistKey(path), stateToPersist), ); }), ).finally(() => { isPersisting = false; if (nextPersistOperation) { const next = nextPersistOperation; nextPersistOperation = null; next(); } else { resolve(); } }); }); }); }; if (isPersisting) { nextPersistOperation = operation; } else { operation(); } }; return { persist, clear: createPersistenceClearer(persistKey, _r), flush: () => { if (nextPersistOperation) { nextPersistOperation(); } return persistPromise; }, }; } export function createPersistMiddleware(persistor, _r) { return ({ getState }) => (next) => (action) => { const state = next(action); if ( action && action.type !== '@action.ePRS' && _r._i._persistenceConfig.length > 0 ) { persistor.persist(getState()); } return state; }; } export function rehydrateStateFromPersistIfNeeded( persistKey, replaceState, _r, root, ) { if (_r._i._persistenceConfig.length === 0) { return Promise.resolve(); } const state = clone(_r._i._dS); let rehydrating = false; return pSeries( _r._i._persistenceConfig.map((persistInstance) => () => { const { path, config } = persistInstance; const { mergeStrategy, storage } = config; if (root && (path.length < 1 || path[0] !== root)) { return Promise.resolve(); } const hasDataModelChanged = (dataModel, rehydratingModelData) => dataModel != null && rehydratingModelData != null && (typeof dataModel !== typeof rehydratingModelData || (Array.isArray(dataModel) && !Array.isArray(rehydratingModelData))); const applyRehydrationStrategy = (persistedState) => { if (mergeStrategy === 'overwrite') { set(path, state, persistedState); } else if (mergeStrategy === 'mergeShallow') { const targetState = get(path, state); Object.keys(persistedState).forEach((key) => { if (hasDataModelChanged(targetState[key], persistedState[key])) { // skip as the data model type has changed since the data was persisted } else { targetState[key] = persistedState[key]; } }); } else if (mergeStrategy === 'mergeDeep') { const targetState = get(path, state); const setAt = (currentTargetState, currentPersistedState) => { Object.keys(currentPersistedState).forEach((key) => { if ( hasDataModelChanged( currentTargetState[key], currentPersistedState[key], ) ) { // skip as the data model type has changed since the data was persisted } else if (isPlainObject(currentPersistedState[key])) { currentTargetState[key] = currentTargetState[key] || {}; setAt(currentTargetState[key], currentPersistedState[key]); } else { currentTargetState[key] = currentPersistedState[key]; } }); }; setAt(targetState, persistedState); } }; const rehydate = (persistedState) => { if (persistedState != null) { applyRehydrationStrategy(persistedState); rehydrating = true; } }; const getItemResult = storage.getItem(persistKey(path)); if (isPromise(getItemResult)) { return getItemResult.then(rehydate); } return Promise.resolve(rehydate(getItemResult)); }), ).then(() => { if (rehydrating) { replaceState(state); } }); }