From 8dd5bde44dd3754961be0b09864d4ed51a565a17 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Mon, 7 Mar 2022 16:13:31 -1000 Subject: [PATCH 1/2] Add write queue to localForage storage provider --- lib/promiseAllSettled.js | 10 +++++ lib/storage/providers/LocalForage.js | 39 ++++++++++++++++++- .../providers/LocalForageProviderTest.js | 14 ++++--- 3 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 lib/promiseAllSettled.js diff --git a/lib/promiseAllSettled.js b/lib/promiseAllSettled.js new file mode 100644 index 000000000..101ae9f4b --- /dev/null +++ b/lib/promiseAllSettled.js @@ -0,0 +1,10 @@ +import _ from 'underscore'; + +export default (arrayOfPromises) => { + const wrappedPromises = _.map(arrayOfPromises, p => Promise.resolve(p) + .then( + val => ({status: 'fulfilled', value: val}), + err => ({status: 'rejected', reason: err}), + )); + return Promise.all(wrappedPromises); +}; diff --git a/lib/storage/providers/LocalForage.js b/lib/storage/providers/LocalForage.js index 3a8c4634a..0d738a42d 100644 --- a/lib/storage/providers/LocalForage.js +++ b/lib/storage/providers/LocalForage.js @@ -7,11 +7,41 @@ import localforage from 'localforage'; import _ from 'underscore'; import lodashMerge from 'lodash/merge'; +import promiseAllSettled from '../../promiseAllSettled'; localforage.config({ name: 'OnyxDB' }); +const MAX_BATCH_SIZE = 20; +const writeQueue = []; +let isQueueProcessing = false; + +/** + * Writing very quickly to IndexedDB causes performance issues and can lock up the page and lead to jank. + * So, we are slowing this process down a bit here by waiting until one batch of writes is complete before moving on + * to the next. + */ +function processNextWriteQueueBatch() { + if (isQueueProcessing || writeQueue.length === 0) { + return; + } + + isQueueProcessing = true; + + const nextBatch = _.map(writeQueue.splice(0, MAX_BATCH_SIZE), ({ + key, value, resolve, reject, + }) => localforage.setItem(key, value) + .then(resolve) + .catch(reject)); + + promiseAllSettled(nextBatch) + .then(() => { + isQueueProcessing = false; + processNextWriteQueueBatch(); + }); +} + const provider = { /** * Get multiple key-value pairs for the give array of keys in a batch @@ -90,7 +120,14 @@ const provider = { * @param {*} value * @return {Promise} */ - setItem: localforage.setItem, + setItem: (key, value) => ( + new Promise((resolve, reject) => { + writeQueue.push({ + key, value, resolve, reject, + }); + processNextWriteQueueBatch(); + }) + ), }; export default provider; diff --git a/tests/unit/storage/providers/LocalForageProviderTest.js b/tests/unit/storage/providers/LocalForageProviderTest.js index 9a6ef1d15..1940a09a1 100644 --- a/tests/unit/storage/providers/LocalForageProviderTest.js +++ b/tests/unit/storage/providers/LocalForageProviderTest.js @@ -15,17 +15,21 @@ describe('storage/providers/LocalForage', () => { // For some reason fake timers cause promises to hang beforeAll(() => jest.useRealTimers()); - beforeEach(() => jest.resetAllMocks()); + beforeEach(() => { + jest.resetAllMocks(); + localforage.setItem = jest.fn(() => Promise.resolve()); + }); it('multiSet', () => { // Given multiple pairs to be saved in storage const pairs = SAMPLE_ITEMS.slice(); // When they are saved - StorageProvider.multiSet(pairs); - - // We expect a call to localForage.setItem for each pair - _.each(pairs, ([key, value]) => expect(localforage.setItem).toHaveBeenCalledWith(key, value)); + return StorageProvider.multiSet(pairs) + .then(() => { + // We expect a call to localForage.setItem for each pair + _.each(pairs, ([key, value]) => expect(localforage.setItem).toHaveBeenCalledWith(key, value)); + }); }); it('multiGet', () => { From a3cdebb4a3724f1185b1326d6dc602cb6d04ec4c Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 8 Mar 2022 09:25:56 -1000 Subject: [PATCH 2/2] simplify solution --- lib/promiseAllSettled.js | 10 ---------- lib/storage/providers/LocalForage.js | 21 +++++++++------------ 2 files changed, 9 insertions(+), 22 deletions(-) delete mode 100644 lib/promiseAllSettled.js diff --git a/lib/promiseAllSettled.js b/lib/promiseAllSettled.js deleted file mode 100644 index 101ae9f4b..000000000 --- a/lib/promiseAllSettled.js +++ /dev/null @@ -1,10 +0,0 @@ -import _ from 'underscore'; - -export default (arrayOfPromises) => { - const wrappedPromises = _.map(arrayOfPromises, p => Promise.resolve(p) - .then( - val => ({status: 'fulfilled', value: val}), - err => ({status: 'rejected', reason: err}), - )); - return Promise.all(wrappedPromises); -}; diff --git a/lib/storage/providers/LocalForage.js b/lib/storage/providers/LocalForage.js index 0d738a42d..7c254be8d 100644 --- a/lib/storage/providers/LocalForage.js +++ b/lib/storage/providers/LocalForage.js @@ -7,38 +7,35 @@ import localforage from 'localforage'; import _ from 'underscore'; import lodashMerge from 'lodash/merge'; -import promiseAllSettled from '../../promiseAllSettled'; localforage.config({ name: 'OnyxDB' }); -const MAX_BATCH_SIZE = 20; const writeQueue = []; let isQueueProcessing = false; /** * Writing very quickly to IndexedDB causes performance issues and can lock up the page and lead to jank. - * So, we are slowing this process down a bit here by waiting until one batch of writes is complete before moving on + * So, we are slowing this process down by waiting until one write is complete before moving on * to the next. */ -function processNextWriteQueueBatch() { +function processNextWrite() { if (isQueueProcessing || writeQueue.length === 0) { return; } isQueueProcessing = true; - const nextBatch = _.map(writeQueue.splice(0, MAX_BATCH_SIZE), ({ + const { key, value, resolve, reject, - }) => localforage.setItem(key, value) + } = writeQueue.shift(); + localforage.setItem(key, value) .then(resolve) - .catch(reject)); - - promiseAllSettled(nextBatch) - .then(() => { + .catch(reject) + .finally(() => { isQueueProcessing = false; - processNextWriteQueueBatch(); + processNextWrite(); }); } @@ -125,7 +122,7 @@ const provider = { writeQueue.push({ key, value, resolve, reject, }); - processNextWriteQueueBatch(); + processNextWrite(); }) ), };