-
Notifications
You must be signed in to change notification settings - Fork 864
/
Copy pathpersistReducer.ts
205 lines (186 loc) · 6.45 KB
/
persistReducer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Action, AnyAction, Reducer
} from 'redux'
import {
FLUSH,
PAUSE,
PERSIST,
PURGE,
REHYDRATE,
DEFAULT_VERSION,
} from './constants'
import type {
PersistConfig,
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 } | any;
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<S, A extends Action>(
config: PersistConfig<S>,
baseReducer: Reducer<S, A>
): Reducer<S & PersistPartial, AnyAction> {
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 stateReconciler =
config.stateReconciler === undefined
? autoMergeLevel1
: config.stateReconciler
const getStoredState = config.getStoredState || defaultGetStoredState
const timeout =
config.timeout !== undefined ? config.timeout : DEFAULT_TIMEOUT
let _persistoid: Persistoid | null = null
let _purge = false
let _paused = true
const conditionalUpdate = (state: any) => {
// update the persistoid only if we are rehydrated and not paused
state._persist.rehydrated &&
_persistoid &&
!_paused &&
_persistoid.update(state)
return state
}
return (state: any, action: any) => {
const { _persist, ...rest } = state || {}
const restState: S = rest
if (action.type === PERSIST) {
let _sealed = false
const _rehydrate = (payload: any, err?: Error) => {
// 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 => {
if (restoredState) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const migrate = config.migrate || ((s, _) => Promise.resolve(s))
migrate(restoredState as any, 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) {
const reducedState = baseReducer(restState, action)
const inboundState = action.payload
// only reconcile state if stateReconciler and inboundState are both defined
const reconciledRest: S =
stateReconciler !== false && inboundState !== undefined
? stateReconciler(inboundState, state, reducedState, config)
: reducedState
const 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
const newState = baseReducer(restState, action)
if (newState === restState) return state
return conditionalUpdate({ ...newState, _persist })
}
}