diff --git a/src/third/redux-persist/createPersistor.js b/src/third/redux-persist/createPersistor.js index 3aea4f14198..c5459119211 100644 --- a/src/third/redux-persist/createPersistor.js +++ b/src/third/redux-persist/createPersistor.js @@ -30,43 +30,83 @@ export default function createPersistor (store, config) { const storage = config.storage; // initialize stateful values - let lastState = {} + let lastWrittenState = {} let paused = false - let storesToProcess = [] - let timeIterator = null + let writeInProgress = false store.subscribe(() => { - if (paused) return - - const state = store.getState() - - Object.keys(state).forEach((key) => { - if (!passWhitelistBlacklist(key)) return - if (lastState[key] === state[key]) return - if (storesToProcess.indexOf(key) !== -1) return - storesToProcess.push(key) - }) - - const len = storesToProcess.length - - // time iterator (read: debounce) - if (timeIterator === null) { - timeIterator = setInterval(() => { - if ((paused && len === storesToProcess.length) || storesToProcess.length === 0) { - clearInterval(timeIterator) - timeIterator = null - return - } + if (paused || writeInProgress) return; + + write(); + }) + + async function write() { + // Take the lock. + writeInProgress = true; + // Then yield so the `subscribe` callback can promptly return. + await new Promise(r => setTimeout(r, 0)); + + try { + let state = undefined; + // eslint-disable-next-line no-cond-assign + while ((state = store.getState()) !== lastWrittenState) { + await writeOnce(state); + } + } finally { + // Release the lock, so the next `subscribe` callback will start the + // loop again. + writeInProgress = false; + } + } - const key = storesToProcess.shift() - const storageKey = createStorageKey(key) - const endState = store.getState()[key] - storage.setItem(storageKey, serializer(endState)).catch(warnIfSetError(key)) - }, 0) + /** + * Update the storage to the given state. + * + * The storage is assumed to already reflect `lastWrittenState`. + * On completion, sets `lastWrittenState` to `state`. + */ + async function writeOnce(state) { + // Atomically collect the subtrees that need to be written out. + const updatedSubstates = []; + for (const key of Object.keys(state)) { + if (!passWhitelistBlacklist(key)) { + continue; + } + if (state[key] === lastWrittenState[key]) { + continue; + } + updatedSubstates.push([key, state[key]]); } - lastState = state - }) + // Serialize those subtrees, with yields after each one. + const writes = [] + for (const [key, substate] of updatedSubstates) { + writes.push([key, serializer(substate)]) + await new Promise(r => setTimeout(r, 0)); + } + + if (writes.length > 0) { // `multiSet` doesn't like an empty array + // Write them all out, in one `storage.multiSet` operation. + try { + // Warning: not guaranteed to be done in a transaction. + await storage.multiSet( + writes.map(([key, serializedSubstate]) => [createStorageKey(key), serializedSubstate]) + ) + } catch (e) { + logging.warn( + e, + { + message: 'An error was encountered while trying to persist this set of keys', + keys: writes.map(([key, _]) => key) + } + ); + throw e + } + } + + // Record success. + lastWrittenState = state + } function passWhitelistBlacklist (key) { if (whitelist && whitelist.indexOf(key) === -1) return false @@ -104,16 +144,18 @@ export default function createPersistor (store, config) { resume: () => { paused = false }, purge: (keys) => purgeStoredState({storage, keyPrefix}, keys), - // Only used in `persistStore`, to force `lastState` to update - // with the results of `REHYDRATE` even when the persistor is - // paused. - _resetLastState: () => { lastState = store.getState() } - } -} - -function warnIfSetError (key) { - return function setError (err) { - if (err) { logging.warn(err, { message: 'Error storing data for key:', key }) } + /** + * Set `lastWrittenState` to the current `store.getState()`. + * + * Only to be used in `persistStore`, to force `lastWrittenState` to + * update with the results of `REHYDRATE` even when the persistor is + * paused. + * + * If this is going to be called, it should be before any writes have + * begun. Otherwise it may not be effective; see + * https://github.com/zulip/zulip-mobile/pull/4694#discussion_r691739007. + */ + _resetLastWrittenState: () => { lastWrittenState = store.getState() } } } diff --git a/src/third/redux-persist/persistStore.js b/src/third/redux-persist/persistStore.js index f4f96caa75e..e9a744f373a 100644 --- a/src/third/redux-persist/persistStore.js +++ b/src/third/redux-persist/persistStore.js @@ -92,9 +92,9 @@ export default function persistStore (store, config = {}, onComplete) { // // So, fix that by still resetting `lastState` with the // result of `REHYDRATE` when the persistor is paused; we - // can do that because we've exposed `_resetLastState` on + // can do that because we've exposed `_resetLastWrittenState` on // the persistor. - persistor._resetLastState() + persistor._resetLastWrittenState() } } finally { complete(err, restoredState)