diff --git a/src/persistReducer.js b/src/persistReducer.js index a35d5cae0..3dd8bcd00 100644 --- a/src/persistReducer.js +++ b/src/persistReducer.js @@ -21,6 +21,7 @@ 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 @@ -47,6 +48,8 @@ export default function persistReducer( ? 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 @@ -64,6 +67,33 @@ export default function persistReducer( let restState: State = rest if (action.type === PERSIST) { + let _sealed = false + let _rehydrate = (payload, err) => { + // only rehydrate if we are not already sealed + !_sealed && action.rehydrate(config.key, payload, err) + if (process.env.NODE_ENV !== 'production' && _sealed) + console.error( + `redux-persist: rehydrate for "${ + config.key + }" called after timeout.`, + payload, + err + ) + } + timeout && + setTimeout(() => { + !_sealed && + _rehydrate( + undefined, + new Error( + `redux-persist: persist timed out for persist key "${ + config.key + }"` + ) + ) + _sealed = true + }, timeout) + // @NOTE PERSIST resumes if paused. _paused = false @@ -87,17 +117,17 @@ export default function persistReducer( const migrate = config.migrate || ((s, v) => Promise.resolve(s)) migrate(restoredState, version).then( migratedState => { - action.rehydrate(config.key, migratedState) + _rehydrate(migratedState) }, migrateErr => { if (process.env.NODE_ENV !== 'production' && migrateErr) console.error('redux-persist: migration error', migrateErr) - action.rehydrate(config.key, undefined, migrateErr) + _rehydrate(undefined, migrateErr) } ) }, err => { - action.rehydrate(config.key, undefined, err) + _rehydrate(undefined, err) } ) diff --git a/src/persistStore.js b/src/persistStore.js index c79e5a8ac..51ee480e2 100644 --- a/src/persistStore.js +++ b/src/persistStore.js @@ -40,13 +40,12 @@ const persistorReducer = (state = initialState, action) => { export default function persistStore( store: Object, - persistorOptions?: PersistorOptions, + options?: ?PersistorOptions, cb?: BoostrappedCb ): Persistor { - let options: Object = persistorOptions || {} - // help catch incorrect usage of passing PersistConfig in as PersistorOptions if (process.env.NODE_ENV !== 'production') { + let optionsToTest: Object = options || {} let bannedKeys = [ 'blacklist', 'whitelist', @@ -56,7 +55,7 @@ export default function persistStore( 'migrate', ] bannedKeys.forEach(k => { - if (!!options[k]) + if (!!optionsToTest[k]) console.error( `redux-persist: invalid option passed to persistStore: "${k}". You may be incorrectly passing persistConfig into persistStore, whereas it should be passed into persistReducer.` ) @@ -64,7 +63,11 @@ export default function persistStore( } let boostrappedCb = cb || false - let _pStore = createStore(persistorReducer, initialState, options.enhancer) + let _pStore = createStore( + persistorReducer, + initialState, + options ? options.enhancer : undefined + ) let register = (key: string) => { _pStore.dispatch({ type: REGISTER, diff --git a/src/types.js b/src/types.js index e7a099a35..3f7f9fd78 100644 --- a/src/types.js +++ b/src/types.js @@ -24,6 +24,7 @@ export type PersistConfig = { getStoredState?: PersistConfig => Promise, // used for migrations debug?: boolean, serialize?: boolean, + timeout?: number, } export type PersistorOptions = { diff --git a/tests/complete.spec.js b/tests/complete.spec.js index 5c62ea351..62f811f7a 100644 --- a/tests/complete.spec.js +++ b/tests/complete.spec.js @@ -1,3 +1,4 @@ + // @flow import test from 'ava' @@ -10,6 +11,7 @@ import { combineReducers, createStore } from 'redux' import persistReducer from '../src/persistReducer' import persistStore from '../src/persistStore' import { createMemoryStorage } from 'storage-memory' +import brokenStorage from './utils/brokenStorage' import { PERSIST, REHYDRATE } from '../src/constants' import sleep from './utils/sleep' @@ -19,6 +21,7 @@ const config = { version: 1, storage: createMemoryStorage(), debug: true, + timeout: 5, } test('multiple persistReducers work together', t => { @@ -28,9 +31,40 @@ test('multiple persistReducers work together', t => { const rootReducer = combineReducers({ r1, r2 }) const store = createStore(rootReducer) const persistor = persistStore(store, {}, () => { - console.log(store.getState(), persistor.getState()) t.is(persistor.getState().bootstrapped, true) resolve() }) }) }) + +test('persistStore timeout 0 never bootstraps', t => { + return new Promise((resolve, reject) => { + let r1 = persistReducer({...config, storage: brokenStorage, timeout: 0}, reducer) + const rootReducer = combineReducers({ r1 }) + const store = createStore(rootReducer) + const persistor = persistStore(store, null, () => { + console.log('resolve') + reject() + }) + setTimeout(() => { + t.is(persistor.getState().bootstrapped, false) + resolve() + }, 10) + }) +}) + + +test('persistStore timeout forces bootstrap', t => { + return new Promise((resolve, reject) => { + let r1 = persistReducer({...config, storage: brokenStorage}, reducer) + const rootReducer = combineReducers({ r1 }) + const store = createStore(rootReducer) + const persistor = persistStore(store, null, () => { + t.is(persistor.getState().bootstrapped, true) + resolve() + }) + setTimeout(() => { + reject() + }, 10) + }) +}) diff --git a/tests/utils/brokenStorage.js b/tests/utils/brokenStorage.js new file mode 100644 index 000000000..75225cc4b --- /dev/null +++ b/tests/utils/brokenStorage.js @@ -0,0 +1,14 @@ +// @flow + +export default { + getItem(): Promise { + return new Promise((resolve: Function, reject: Function) => {}) + }, + setItem(): Promise { + return new Promise((resolve: Function, reject: Function) => {}) + }, + removeItem(): Promise { + return new Promise((resolve: Function, reject: Function) => {}) + } + } + \ No newline at end of file