123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- // @flow
- import {
- FLUSH,
- PAUSE,
- PERSIST,
- PURGE,
- REHYDRATE,
- DEFAULT_VERSION,
- } from './constants'
- import type {
- PersistConfig,
- MigrationManifest,
- PersistState,
- Persistoid,
- } from './types'
- import autoMergeLevel1 from './stateReconciler/autoMergeLevel1'
- import createPersistoid from './createPersistoid'
- import defaultGetStoredState from './getStoredState'
- import purgeStoredState from './purgeStoredState'
- type PersistPartial = { _persist: PersistState }
- const DEFAULT_TIMEOUT = 5000
- /*
- @TODO add validation / handling for:
- - persisting a reducer which has nested _persist
- - handling actions that fire before reydrate is called
- */
- export default function persistReducer<State: Object, Action: Object>(
- config: PersistConfig,
- baseReducer: (State, Action) => State
- ): (State, Action) => State & PersistPartial {
- if (process.env.NODE_ENV !== 'production') {
- if (!config) throw new Error('config is required for persistReducer')
- if (!config.key) throw new Error('key is required in persistor config')
- if (!config.storage)
- throw new Error(
- "redux-persist: config.storage is required. Try using one of the provided storage engines `import storage from 'redux-persist/lib/storage'`"
- )
- }
- const version =
- config.version !== undefined ? config.version : DEFAULT_VERSION
- const debug = config.debug || false
- const stateReconciler =
- config.stateReconciler === undefined
- ? autoMergeLevel1
- : config.stateReconciler
- const getStoredState = config.getStoredState || defaultGetStoredState
- const timeout =
- config.timeout !== undefined ? config.timeout : DEFAULT_TIMEOUT
- let _persistoid = null
- let _purge = false
- let _paused = true
- const conditionalUpdate = state => {
- // update the persistoid only if we are rehydrated and not paused
- state._persist.rehydrated &&
- _persistoid &&
- !_paused &&
- _persistoid.update(state)
- return state
- }
- return (state: State, action: Action) => {
- let { _persist, ...rest } = state || {}
- // $FlowIgnore need to update State type
- let restState: State = rest
- if (action.type === PERSIST) {
- let _sealed = false
- let _rehydrate = (payload, err) => {
- // dev warning if we are already sealed
- if (process.env.NODE_ENV !== 'production' && _sealed)
- console.error(
- `redux-persist: rehydrate for "${
- config.key
- }" called after timeout.`,
- payload,
- err
- )
- // only rehydrate if we are not already sealed
- if (!_sealed) {
- action.rehydrate(config.key, payload, err)
- _sealed = true
- }
- }
- timeout &&
- setTimeout(() => {
- !_sealed &&
- _rehydrate(
- undefined,
- new Error(
- `redux-persist: persist timed out for persist key "${
- config.key
- }"`
- )
- )
- }, timeout)
- // @NOTE PERSIST resumes if paused.
- _paused = false
- // @NOTE only ever create persistoid once, ensure we call it at least once, even if _persist has already been set
- if (!_persistoid) _persistoid = createPersistoid(config)
- // @NOTE PERSIST can be called multiple times, noop after the first
- if (_persist) {
- // We still need to call the base reducer because there might be nested
- // uses of persistReducer which need to be aware of the PERSIST action
- return {
- ...baseReducer(restState, action),
- _persist,
- };
- }
- if (
- typeof action.rehydrate !== 'function' ||
- typeof action.register !== 'function'
- )
- throw new Error(
- 'redux-persist: either rehydrate or register is not a function on the PERSIST action. This can happen if the action is being replayed. This is an unexplored use case, please open an issue and we will figure out a resolution.'
- )
- action.register(config.key)
- getStoredState(config).then(
- restoredState => {
- const migrate = config.migrate || ((s, v) => Promise.resolve(s))
- migrate(restoredState, version).then(
- migratedState => {
- _rehydrate(migratedState)
- },
- migrateErr => {
- if (process.env.NODE_ENV !== 'production' && migrateErr)
- console.error('redux-persist: migration error', migrateErr)
- _rehydrate(undefined, migrateErr)
- }
- )
- },
- err => {
- _rehydrate(undefined, err)
- }
- )
- return {
- ...baseReducer(restState, action),
- _persist: { version, rehydrated: false },
- }
- } else if (action.type === PURGE) {
- _purge = true
- action.result(purgeStoredState(config))
- return {
- ...baseReducer(restState, action),
- _persist,
- }
- } else if (action.type === FLUSH) {
- action.result(_persistoid && _persistoid.flush())
- return {
- ...baseReducer(restState, action),
- _persist,
- }
- } else if (action.type === PAUSE) {
- _paused = true
- } else if (action.type === REHYDRATE) {
- // noop on restState if purging
- if (_purge)
- return {
- ...restState,
- _persist: { ..._persist, rehydrated: true },
- }
- // @NOTE if key does not match, will continue to default else below
- if (action.key === config.key) {
- let reducedState = baseReducer(restState, action)
- let inboundState = action.payload
- // only reconcile state if stateReconciler and inboundState are both defined
- let reconciledRest: State =
- stateReconciler !== false && inboundState !== undefined
- ? stateReconciler(inboundState, state, reducedState, config)
- : reducedState
- let newState = {
- ...reconciledRest,
- _persist: { ..._persist, rehydrated: true },
- }
- return conditionalUpdate(newState)
- }
- }
- // if we have not already handled PERSIST, straight passthrough
- if (!_persist) return baseReducer(state, action)
- // run base reducer:
- // is state modified ? return original : return updated
- let newState = baseReducer(restState, action)
- if (newState === restState) return state
- return conditionalUpdate({ ...newState, _persist })
- }
- }
|