You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
6.3 KiB
202 lines
6.3 KiB
2 years ago
|
// @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 })
|
||
|
}
|
||
|
}
|