From 1318f6328fd410f61451ac5239972a977b51027f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 3 Aug 2023 15:45:20 +0200 Subject: [PATCH 1/5] add idb-keyval package --- package-lock.json | 13 +++++++++++++ package.json | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/package-lock.json b/package-lock.json index e5af46ff..8fc66873 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "eslint-config-expensify": "^2.0.38", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.31.10", + "idb-keyval": "^6.2.1", "jest": "^26.5.2", "jest-cli": "^26.5.2", "jsdoc-to-markdown": "^7.1.0", @@ -8529,6 +8530,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", + "dev": true + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -23601,6 +23608,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", + "dev": true + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", diff --git a/package.json b/package.json index e60107ca..b2a2c0ae 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "eslint-config-expensify": "^2.0.38", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.31.10", + "idb-keyval": "^6.2.1", "jest": "^26.5.2", "jest-cli": "^26.5.2", "jsdoc-to-markdown": "^7.1.0", @@ -73,6 +74,7 @@ "webpack-merge": "^5.8.0" }, "peerDependencies": { + "idb-keyval": "^6.2.1", "localforage": "^1.10.0", "localforage-removeitems": "^1.4.0", "react": ">=18.1.0", @@ -80,6 +82,9 @@ "react-native-quick-sqlite": "^8.0.0-beta.2" }, "peerDependenciesMeta": { + "idb-keyval": { + "optional": true + }, "react-native-performance": { "optional": true }, From 1e08d685eaaa1230f639bd8cc6b04158fc39c2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 3 Aug 2023 15:54:06 +0200 Subject: [PATCH 2/5] add idb-key provider --- lib/storage/providers/IDBKeyVal.js | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 lib/storage/providers/IDBKeyVal.js diff --git a/lib/storage/providers/IDBKeyVal.js b/lib/storage/providers/IDBKeyVal.js new file mode 100644 index 00000000..87584d0c --- /dev/null +++ b/lib/storage/providers/IDBKeyVal.js @@ -0,0 +1,55 @@ +import { + set, + keys, + getMany, + setMany, + get, + clear, + del, + delMany, + createStore, + promisifyRequest, +} from 'idb-keyval'; +import _ from 'underscore'; +import fastMerge from '../../fastMerge'; + +// Same config as localforage, so we can swap the providers easily +const customStore = createStore('OnyxDB', 'keyvaluepairs'); + +const provider = { + getAllKeys: () => keys(customStore), + + multiGet: keysParam => getMany(keysParam, customStore) + .then(values => _.map(values, (value, index) => [keysParam[index], value])), + + getItem: key => get(key, customStore), + + multiSet: pairs => setMany(pairs, customStore), + + setItem: (key, value) => set(key, value, customStore), + + multiMerge: pairs => customStore('readwrite', (store) => { + const getValues = Promise.all(_.map(pairs, ([key]) => promisifyRequest(store.get(key)))); + + return getValues.then((values) => { + const upsertMany = _.map(pairs, ([key, value], index) => { + const prev = values[index]; + const newValue = _.isObject(prev) ? fastMerge(prev, value) : value; + return promisifyRequest(store.put(newValue, key)); + }); + return Promise.all(upsertMany); + }); + }), + + mergeItem(key, _changes, modifiedData) { + return provider.multiMerge([[key, modifiedData]]); + }, + + clear: () => clear(customStore), + + removeItem: key => del(key, customStore), + + removeItems: keysParam => delMany(keysParam, customStore), +}; + +export default provider; From e4faf0b576cd3b2873c74e43d05c4b3a972a2b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 3 Aug 2023 15:55:05 +0200 Subject: [PATCH 3/5] swap provider --- lib/storage/WebStorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/storage/WebStorage.js b/lib/storage/WebStorage.js index fb786679..040e9e05 100644 --- a/lib/storage/WebStorage.js +++ b/lib/storage/WebStorage.js @@ -4,7 +4,7 @@ * data changes and then stay up-to-date with everything happening in Onyx. */ import _ from 'underscore'; -import Storage from './providers/LocalForage'; +import Storage from './providers/IDBKeyVal'; const SYNC_ONYX = 'SYNC_ONYX'; From 3cae36f8d84105ec97c6b6572e6740bc574d977d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 3 Aug 2023 16:02:45 +0200 Subject: [PATCH 4/5] document code --- lib/storage/providers/IDBKeyVal.js | 70 +++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/lib/storage/providers/IDBKeyVal.js b/lib/storage/providers/IDBKeyVal.js index 87584d0c..8265420f 100644 --- a/lib/storage/providers/IDBKeyVal.js +++ b/lib/storage/providers/IDBKeyVal.js @@ -17,18 +17,32 @@ import fastMerge from '../../fastMerge'; const customStore = createStore('OnyxDB', 'keyvaluepairs'); const provider = { - getAllKeys: () => keys(customStore), + /** + * Sets the value for a given key. The only requirement is that the value should be serializable to JSON string + * @param {String} key + * @param {*} value + * @return {Promise} + */ + setItem: (key, value) => set(key, value, customStore), + /** + * Get multiple key-value pairs for the give array of keys in a batch. + * This is optimized to use only one database transaction. + * @param {String[]} keysParam + * @return {Promise>} + */ multiGet: keysParam => getMany(keysParam, customStore) .then(values => _.map(values, (value, index) => [keysParam[index], value])), - getItem: key => get(key, customStore), - - multiSet: pairs => setMany(pairs, customStore), - - setItem: (key, value) => set(key, value, customStore), - + /** + * Multiple merging of existing and new values in a batch + * @param {Array<[key, value]>} pairs + * @return {Promise} + */ multiMerge: pairs => customStore('readwrite', (store) => { + // Note: we are using the manual store transaction here, to fit the read and update + // of the items in one transaction to achieve best performance. + const getValues = Promise.all(_.map(pairs, ([key]) => promisifyRequest(store.get(key)))); return getValues.then((values) => { @@ -41,14 +55,56 @@ const provider = { }); }), + /** + * Merging an existing value with a new one + * @param {String} key + * @param {any} _changes - not used, as we rely on the pre-merged data from the `modifiedData` + * @param {any} modifiedData - the pre-merged data from `Onyx.applyMerge` + * @return {Promise} + */ mergeItem(key, _changes, modifiedData) { return provider.multiMerge([[key, modifiedData]]); }, + /** + * Stores multiple key-value pairs in a batch + * @param {Array<[key, value]>} pairs + * @return {Promise} + */ + multiSet: pairs => setMany(pairs, customStore), + + /** + * Clear everything from storage and also stops the SyncQueue from adding anything more to storage + * @returns {Promise} + */ clear: () => clear(customStore), + /** + * Returns all keys available in storage + * @returns {Promise} + */ + getAllKeys: () => keys(customStore), + + /** + * Get the value of a given key or return `null` if it's not available in storage + * @param {String} key + * @return {Promise<*>} + */ + getItem: key => get(key, customStore), + + /** + * Remove given key and it's value from storage + * @param {String} key + * @returns {Promise} + */ removeItem: key => del(key, customStore), + /** + * Remove given keys and their values from storage + * + * @param {Array} keysParam + * @returns {Promise} + */ removeItems: keysParam => delMany(keysParam, customStore), }; From 2047f732881866f082f8464567fcfee5aaf72fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 11 Aug 2023 17:22:06 +0200 Subject: [PATCH 5/5] remove all traces of localforage --- jestSetup.js | 2 +- lib/storage/WebStorage.js | 2 +- lib/storage/__mocks__/index.js | 30 ++-- lib/storage/providers/IDBKeyVal.js | 1 - lib/storage/providers/LocalForage.js | 158 ------------------ package-lock.json | 77 +-------- package.json | 9 - ...oviderTest.js => IDBKeyvalProviderTest.js} | 52 +++--- 8 files changed, 45 insertions(+), 286 deletions(-) delete mode 100644 lib/storage/providers/LocalForage.js rename tests/unit/storage/providers/{LocalForageProviderTest.js => IDBKeyvalProviderTest.js} (64%) diff --git a/jestSetup.js b/jestSetup.js index b056db3a..ea31de16 100644 --- a/jestSetup.js +++ b/jestSetup.js @@ -1,4 +1,4 @@ jest.mock('./lib/storage'); jest.mock('./lib/storage/NativeStorage', () => require('./lib/storage/__mocks__')); jest.mock('./lib/storage/WebStorage', () => require('./lib/storage/__mocks__')); -jest.mock('./lib/storage/providers/LocalForage', () => require('./lib/storage/__mocks__')); +jest.mock('./lib/storage/providers/IDBKeyVal', () => require('./lib/storage/__mocks__')); diff --git a/lib/storage/WebStorage.js b/lib/storage/WebStorage.js index 040e9e05..6b4889f9 100644 --- a/lib/storage/WebStorage.js +++ b/lib/storage/WebStorage.js @@ -1,5 +1,5 @@ /** - * This file is here to wrap LocalForage with a layer that provides data-changed events like the ones that exist + * This file is here to wrap IDBKeyVal with a layer that provides data-changed events like the ones that exist * when using LocalStorage APIs in the browser. These events are great because multiple tabs can listen for when * data changes and then stay up-to-date with everything happening in Onyx. */ diff --git a/lib/storage/__mocks__/index.js b/lib/storage/__mocks__/index.js index fed1cb23..64e5b95d 100644 --- a/lib/storage/__mocks__/index.js +++ b/lib/storage/__mocks__/index.js @@ -8,7 +8,7 @@ const set = jest.fn((key, value) => { return Promise.resolve(value); }); -const localForageMock = { +const idbKeyvalMock = { setItem(key, value) { return set(key, value); }, @@ -57,23 +57,23 @@ const localForageMock = { config() {}, }; -const localForageMockSpy = { - localForageSet: set, - setItem: jest.fn(localForageMock.setItem), - getItem: jest.fn(localForageMock.getItem), - removeItem: jest.fn(localForageMock.removeItem), - removeItems: jest.fn(localForageMock.removeItems), - clear: jest.fn(localForageMock.clear), - getAllKeys: jest.fn(localForageMock.getAllKeys), - config: jest.fn(localForageMock.config), - multiGet: jest.fn(localForageMock.multiGet), - multiSet: jest.fn(localForageMock.multiSet), - multiMerge: jest.fn(localForageMock.multiMerge), - mergeItem: jest.fn(localForageMock.mergeItem), +const idbKeyvalMockSpy = { + idbKeyvalSet: set, + setItem: jest.fn(idbKeyvalMock.setItem), + getItem: jest.fn(idbKeyvalMock.getItem), + removeItem: jest.fn(idbKeyvalMock.removeItem), + removeItems: jest.fn(idbKeyvalMock.removeItems), + clear: jest.fn(idbKeyvalMock.clear), + getAllKeys: jest.fn(idbKeyvalMock.getAllKeys), + config: jest.fn(idbKeyvalMock.config), + multiGet: jest.fn(idbKeyvalMock.multiGet), + multiSet: jest.fn(idbKeyvalMock.multiSet), + multiMerge: jest.fn(idbKeyvalMock.multiMerge), + mergeItem: jest.fn(idbKeyvalMock.mergeItem), getStorageMap: jest.fn(() => storageMapInternal), setInitialMockData: jest.fn((data) => { storageMapInternal = data; }), }; -export default localForageMockSpy; +export default idbKeyvalMockSpy; diff --git a/lib/storage/providers/IDBKeyVal.js b/lib/storage/providers/IDBKeyVal.js index 8265420f..cdae8d23 100644 --- a/lib/storage/providers/IDBKeyVal.js +++ b/lib/storage/providers/IDBKeyVal.js @@ -13,7 +13,6 @@ import { import _ from 'underscore'; import fastMerge from '../../fastMerge'; -// Same config as localforage, so we can swap the providers easily const customStore = createStore('OnyxDB', 'keyvaluepairs'); const provider = { diff --git a/lib/storage/providers/LocalForage.js b/lib/storage/providers/LocalForage.js deleted file mode 100644 index ec39f138..00000000 --- a/lib/storage/providers/LocalForage.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @file - * The storage provider based on localforage allows us to store most anything in its - * natural form in the underlying DB without having to stringify or de-stringify it - */ - -import localforage from 'localforage'; -import _ from 'underscore'; -import {extendPrototype} from 'localforage-removeitems'; -import SyncQueue from '../../SyncQueue'; -import * as Str from '../../Str'; -import fastMerge from '../../fastMerge'; - -extendPrototype(localforage); - -localforage.config({ - name: 'OnyxDB', -}); - -/** - * Keys that will not ever be persisted to disk. - */ -let memoryOnlyKeys = []; - -const provider = { - /** - * 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 by waiting until one write is complete before moving on - * to the next. - */ - setItemQueue: new SyncQueue(({key, value, shouldMerge}) => { - if (_.find(memoryOnlyKeys, noCacheKey => Str.startsWith(key, noCacheKey))) { - return Promise.resolve(); - } - - if (shouldMerge) { - return localforage.getItem(key) - .then((existingValue) => { - const newValue = _.isObject(existingValue) - - // lodash adds a small overhead so we don't use it here - // eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method - ? Object.assign({}, fastMerge(existingValue, value)) - : value; - return localforage.setItem(key, newValue); - }); - } - - return localforage.setItem(key, value); - }), - - /** - * Sets the value for a given key. The only requirement is that the value should be serializable to JSON string - * @param {String} key - * @param {*} value - * @return {Promise} - */ - setItem(key, value) { - return this.setItemQueue.push({key, value}); - }, - - /** - * Get multiple key-value pairs for the give array of keys in a batch - * @param {String[]} keys - * @return {Promise>} - */ - multiGet(keys) { - const pairs = _.map( - keys, - key => localforage.getItem(key) - .then(value => [key, value]), - ); - - return Promise.all(pairs); - }, - - /** - * Multiple merging of existing and new values in a batch - * @param {Array<[key, value]>} pairs - * @return {Promise} - */ - multiMerge(pairs) { - const tasks = _.map(pairs, ([key, value]) => this.setItemQueue.push({key, value, shouldMerge: true})); - - // We're returning Promise.resolve, otherwise the array of task results will be returned to the caller - return Promise.all(tasks).then(() => Promise.resolve()); - }, - - /** - * Merging an existing value with a new one - * @param {String} key - * @param {any} _changes - not used, as we rely on the pre-merged data from the `modifiedData` - * @param {any} modifiedData - the pre-merged data from `Onyx.applyMerge` - * @return {Promise} - */ - mergeItem(key, _changes, modifiedData) { - return this.setItem(key, modifiedData); - }, - - /** - * Stores multiple key-value pairs in a batch - * @param {Array<[key, value]>} pairs - * @return {Promise} - */ - multiSet(pairs) { - // We're returning Promise.resolve, otherwise the array of task results will be returned to the caller - const tasks = _.map(pairs, ([key, value]) => this.setItem(key, value)); - return Promise.all(tasks).then(() => Promise.resolve()); - }, - - /** - * Clear everything from storage and also stops the SyncQueue from adding anything more to storage - * @returns {Promise} - */ - clear() { - this.setItemQueue.abort(); - return localforage.clear(); - }, - - /** - * Returns all keys available in storage - * @returns {Promise} - */ - getAllKeys: localforage.keys, - - /** - * Get the value of a given key or return `null` if it's not available in storage - * @param {String} key - * @return {Promise<*>} - */ - getItem: localforage.getItem, - - /** - * Remove given key and it's value from storage - * @param {String} key - * @returns {Promise} - */ - removeItem: localforage.removeItem, - - /** - * Remove given keys and their values from storage - * - * @param {Array} keys - * @returns {Promise} - */ - removeItems(keys) { - return localforage.removeItems(keys); - }, - - /** - * @param {string[]} keyList - */ - setMemoryOnlyKeys(keyList) { - memoryOnlyKeys = keyList; - }, -}; - -export default provider; diff --git a/package-lock.json b/package-lock.json index 8fc66873..ee507c47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,6 @@ "jest": "^26.5.2", "jest-cli": "^26.5.2", "jsdoc-to-markdown": "^7.1.0", - "localforage": "^1.10.0", "metro-react-native-babel-preset": "^0.72.3", "prop-types": "^15.7.2", "react": "18.2.0", @@ -48,17 +47,13 @@ "npm": "8.11.0" }, "peerDependencies": { - "localforage": "^1.10.0", - "localforage-removeitems": "^1.4.0", + "idb-keyval": "^6.2.1", "react": ">=18.1.0", "react-native-performance": "^4.0.0", "react-native-quick-sqlite": "^8.0.0-beta.2" }, "peerDependenciesMeta": { - "localforage": { - "optional": true - }, - "localforage-removeitems": { + "idb-keyval": { "optional": true }, "react-native-performance": { @@ -8577,12 +8572,6 @@ "node": ">=4.0" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", - "devOptional": true - }, "node_modules/import-fresh": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", @@ -11035,15 +11024,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", - "devOptional": true, - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -11082,25 +11062,6 @@ "node": ">=8.9.0" } }, - "node_modules/localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "devOptional": true, - "dependencies": { - "lie": "3.1.1" - } - }, - "node_modules/localforage-removeitems": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/localforage-removeitems/-/localforage-removeitems-1.4.0.tgz", - "integrity": "sha512-/rFJ/w/FAZbVeXa6ZVANociT2sWaUglp2rgYARXcd/2v4CJMRbzvdcfYrNEBzEVkBRQqxipsUfyDDrAy9YiqzA==", - "optional": true, - "peer": true, - "dependencies": { - "localforage": ">=1.4.0" - } - }, "node_modules/locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", @@ -23632,12 +23593,6 @@ "integrity": "sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==", "dev": true }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", - "devOptional": true - }, "import-fresh": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", @@ -25557,15 +25512,6 @@ "type-check": "~0.4.0" } }, - "lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", - "devOptional": true, - "requires": { - "immediate": "~3.0.5" - } - }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -25598,25 +25544,6 @@ "json5": "^2.1.2" } }, - "localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "devOptional": true, - "requires": { - "lie": "3.1.1" - } - }, - "localforage-removeitems": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/localforage-removeitems/-/localforage-removeitems-1.4.0.tgz", - "integrity": "sha512-/rFJ/w/FAZbVeXa6ZVANociT2sWaUglp2rgYARXcd/2v4CJMRbzvdcfYrNEBzEVkBRQqxipsUfyDDrAy9YiqzA==", - "optional": true, - "peer": true, - "requires": { - "localforage": ">=1.4.0" - } - }, "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", diff --git a/package.json b/package.json index b2a2c0ae..52ed22d8 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "jest": "^26.5.2", "jest-cli": "^26.5.2", "jsdoc-to-markdown": "^7.1.0", - "localforage": "^1.10.0", "metro-react-native-babel-preset": "^0.72.3", "prop-types": "^15.7.2", "react": "18.2.0", @@ -75,8 +74,6 @@ }, "peerDependencies": { "idb-keyval": "^6.2.1", - "localforage": "^1.10.0", - "localforage-removeitems": "^1.4.0", "react": ">=18.1.0", "react-native-performance": "^4.0.0", "react-native-quick-sqlite": "^8.0.0-beta.2" @@ -90,12 +87,6 @@ }, "react-native-quick-sqlite": { "optional": true - }, - "localforage": { - "optional": true - }, - "localforage-removeitems": { - "optional": true } }, "engines": { diff --git a/tests/unit/storage/providers/LocalForageProviderTest.js b/tests/unit/storage/providers/IDBKeyvalProviderTest.js similarity index 64% rename from tests/unit/storage/providers/LocalForageProviderTest.js rename to tests/unit/storage/providers/IDBKeyvalProviderTest.js index b2580e4a..e30d2161 100644 --- a/tests/unit/storage/providers/LocalForageProviderTest.js +++ b/tests/unit/storage/providers/IDBKeyvalProviderTest.js @@ -1,10 +1,10 @@ import _ from 'underscore'; -import LocalForageProviderMock from '../../../../lib/storage/providers/LocalForage'; +import IDBKeyValProviderMock from '../../../../lib/storage/providers/IDBKeyVal'; import createDeferredTask from '../../../../lib/createDeferredTask'; import waitForPromisesToResolve from '../../../utils/waitForPromisesToResolve'; -describe('storage/providers/LocalForage', () => { +describe('storage/providers/IDBKeyVal', () => { const SAMPLE_ITEMS = [ ['string', 'Plain String'], ['array', ['Mixed', {array: [{id: 1}, {id: 2}]}]], @@ -18,8 +18,8 @@ describe('storage/providers/LocalForage', () => { beforeAll(() => jest.useRealTimers()); beforeEach(() => { jest.clearAllMocks(); - LocalForageProviderMock.clear(); - LocalForageProviderMock.clear.mockClear(); + IDBKeyValProviderMock.clear(); + IDBKeyValProviderMock.clear.mockClear(); }); it('multiSet', () => { @@ -27,20 +27,20 @@ describe('storage/providers/LocalForage', () => { const pairs = SAMPLE_ITEMS.slice(); // When they are saved - return LocalForageProviderMock.multiSet(pairs).then(() => { - // We expect a call to localForage.setItem for each pair - _.each(pairs, ([key, value]) => expect(LocalForageProviderMock.setItem).toHaveBeenCalledWith(key, value)); + return IDBKeyValProviderMock.multiSet(pairs).then(() => { + // We expect a call to idbKeyval.setItem for each pair + _.each(pairs, ([key, value]) => expect(IDBKeyValProviderMock.setItem).toHaveBeenCalledWith(key, value)); }); }); it('multiGet', () => { // Given we have some data in storage - LocalForageProviderMock.multiSet(SAMPLE_ITEMS); + IDBKeyValProviderMock.multiSet(SAMPLE_ITEMS); return waitForPromisesToResolve().then(() => { // Then multi get should retrieve them const keys = _.map(SAMPLE_ITEMS, _.head); - return LocalForageProviderMock.multiGet(keys) + return IDBKeyValProviderMock.multiGet(keys) .then(pairs => expect(pairs).toEqual(expect.arrayContaining(SAMPLE_ITEMS))); }); }); @@ -59,10 +59,10 @@ describe('storage/providers/LocalForage', () => { traits: {hair: 'black'}, }; - LocalForageProviderMock.multiSet([['@USER_1', USER_1], ['@USER_2', USER_2]]); + IDBKeyValProviderMock.multiSet([['@USER_1', USER_1], ['@USER_2', USER_2]]); return waitForPromisesToResolve().then(() => { - LocalForageProviderMock.localForageSet.mockClear(); + IDBKeyValProviderMock.idbKeyvalSet.mockClear(); // Given deltas matching existing structure const USER_1_DELTA = { @@ -76,12 +76,12 @@ describe('storage/providers/LocalForage', () => { }; // When data is merged to storage - return LocalForageProviderMock.multiMerge([ + return IDBKeyValProviderMock.multiMerge([ ['@USER_1', USER_1_DELTA], ['@USER_2', USER_2_DELTA], ]).then(() => { // Then each existing item should be set with the merged content - expect(LocalForageProviderMock.localForageSet).toHaveBeenNthCalledWith(1, + expect(IDBKeyValProviderMock.idbKeyvalSet).toHaveBeenNthCalledWith(1, '@USER_1', { name: 'Tom', age: 31, @@ -91,7 +91,7 @@ describe('storage/providers/LocalForage', () => { }, }); - expect(LocalForageProviderMock.localForageSet).toHaveBeenNthCalledWith(2, + expect(IDBKeyValProviderMock.idbKeyvalSet).toHaveBeenNthCalledWith(2, '@USER_2', { name: 'Sarah', age: 26, @@ -107,31 +107,31 @@ describe('storage/providers/LocalForage', () => { // We're creating a Promise which we programatically control when to resolve. const task = createDeferredTask(); - // We configure localforage.setItem to return this promise the first time it's called and to otherwise return resolved promises - LocalForageProviderMock.setItem = jest.fn() + // We configure idbKeyval.setItem to return this promise the first time it's called and to otherwise return resolved promises + IDBKeyValProviderMock.setItem = jest.fn() .mockReturnValue(Promise.resolve()) // Default behavior .mockReturnValueOnce(task.promise); // First call behavior - // Make 5 StorageProvider.setItem calls - this adds 5 items to the queue and starts executing the first localForage.setItem + // Make 5 StorageProvider.setItem calls - this adds 5 items to the queue and starts executing the first idbKeyval.setItem for (let i = 0; i < 5; i++) { - LocalForageProviderMock.setItem(`key${i}`, `value${i}`); + IDBKeyValProviderMock.setItem(`key${i}`, `value${i}`); } - // At this point,`localForage.setItem` should have been called once, but we control when it resolves, and we'll keep it unresolved. - // This simulates the 1st localForage.setItem taking a random time. - // We then call StorageProvider.clear() while the first localForage.setItem isn't completed yet. - LocalForageProviderMock.clear(); + // At this point,`idbKeyval.setItem` should have been called once, but we control when it resolves, and we'll keep it unresolved. + // This simulates the 1st idbKeyval.setItem taking a random time. + // We then call StorageProvider.clear() while the first idbKeyval.setItem isn't completed yet. + IDBKeyValProviderMock.clear(); - // Any calls that follow this would have been queued - so we don't expect more than 1 `localForage.setItem` call after the + // Any calls that follow this would have been queued - so we don't expect more than 1 `idbKeyval.setItem` call after the // first one resolves. task.resolve(); // waitForPromisesToResolve() makes jest wait for any promises (even promises returned as the result of a promise) to resolve. - // If StorageProvider.clear() does not abort the queue, more localForage.setItem calls would be executed because they would + // If StorageProvider.clear() does not abort the queue, more idbKeyval.setItem calls would be executed because they would // be sitting in the setItemQueue return waitForPromisesToResolve().then(() => { - expect(LocalForageProviderMock.localForageSet).toHaveBeenCalledTimes(0); - expect(LocalForageProviderMock.clear).toHaveBeenCalledTimes(1); + expect(IDBKeyValProviderMock.idbKeyvalSet).toHaveBeenCalledTimes(0); + expect(IDBKeyValProviderMock.clear).toHaveBeenCalledTimes(1); }); }); });