From 2dd23129f5cee6388f622a38a75eca39e1c918a1 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 21 Feb 2024 17:56:27 +0100 Subject: [PATCH 1/9] Migrate reamaining files to TS --- lib/Logger.d.ts | 13 ---- lib/Logger.js | 31 ---------- lib/Logger.ts | 31 ++++++++++ lib/Str.js | 27 --------- lib/Str.ts | 24 ++++++++ lib/SyncQueue.js | 59 ------------------- lib/compose.js | 34 ----------- ...eDeferredTask.js => createDeferredTask.ts} | 13 ++-- 8 files changed, 63 insertions(+), 169 deletions(-) delete mode 100644 lib/Logger.d.ts delete mode 100644 lib/Logger.js create mode 100644 lib/Logger.ts delete mode 100644 lib/Str.js create mode 100644 lib/Str.ts delete mode 100644 lib/SyncQueue.js delete mode 100644 lib/compose.js rename lib/{createDeferredTask.js => createDeferredTask.ts} (61%) diff --git a/lib/Logger.d.ts b/lib/Logger.d.ts deleted file mode 100644 index 5c7e4d545..000000000 --- a/lib/Logger.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare type LogData = { - message: string; - level: 'alert' | 'info'; -}; - -/** - * Register the logging callback - * - * @param callback - */ -declare function registerLogger(callback: (data: LogData) => void): void; - -export {registerLogger}; diff --git a/lib/Logger.js b/lib/Logger.js deleted file mode 100644 index 1cc6240b9..000000000 --- a/lib/Logger.js +++ /dev/null @@ -1,31 +0,0 @@ -// Logging callback -let logger = () => {}; - -/** - * Register the logging callback - * - * @param {Function} callback - */ -function registerLogger(callback) { - logger = callback; -} - -/** - * Send an alert message to the logger - * - * @param {String} message - */ -function logAlert(message) { - logger({message: `[Onyx] ${message}`, level: 'alert'}); -} - -/** - * Send an info message to the logger - * - * @param {String} message - */ -function logInfo(message) { - logger({message: `[Onyx] ${message}`, level: 'info'}); -} - -export {registerLogger, logInfo, logAlert}; diff --git a/lib/Logger.ts b/lib/Logger.ts new file mode 100644 index 000000000..b83df67fb --- /dev/null +++ b/lib/Logger.ts @@ -0,0 +1,31 @@ +type LogData = { + message: string; + level: 'alert' | 'info'; +}; +type LoggerCallback = (data: LogData) => void; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +let logger: LoggerCallback = () => {}; + +/** + * Register the logging callback + */ +function registerLogger(callback: LoggerCallback) { + logger = callback; +} + +/** + * Send an alert message to the logger + */ +function logAlert(message: string) { + logger({message: `[Onyx] ${message}`, level: 'alert'}); +} + +/** + * Send an info message to the logger + */ +function logInfo(message: string) { + logger({message: `[Onyx] ${message}`, level: 'info'}); +} + +export {registerLogger, logInfo, logAlert}; diff --git a/lib/Str.js b/lib/Str.js deleted file mode 100644 index 71f00f628..000000000 --- a/lib/Str.js +++ /dev/null @@ -1,27 +0,0 @@ -import _ from 'underscore'; - -/** - * Returns true if the haystack begins with the needle - * - * @param {String} haystack The full string to be searched - * @param {String} needle The case-sensitive string to search for - * @return {Boolean} Returns true if the haystack starts with the needle. - */ -function startsWith(haystack, needle) { - return _.isString(haystack) && _.isString(needle) && haystack.startsWith(needle); -} - -/** - * Checks if parameter is a string or function. - * If it is a string, then we will just return it. - * If it is a function, then we will call it with - * any additional arguments and return the result. - * - * @param {String|Function} parameter - * @returns {*} - */ -function result(parameter, ...args) { - return _.isFunction(parameter) ? parameter(...args) : parameter; -} - -export {startsWith, result}; diff --git a/lib/Str.ts b/lib/Str.ts new file mode 100644 index 000000000..bb1429b42 --- /dev/null +++ b/lib/Str.ts @@ -0,0 +1,24 @@ +/** + * Returns true if the haystack begins with the needle + * + * @param haystack The full string to be searched + * @param needle The case-sensitive string to search for + * @return Returns true if the haystack starts with the needle. + */ +function startsWith(haystack: string, needle: string) { + return typeof haystack === 'string' && typeof needle === 'string' && haystack.startsWith(needle); +} + +/** + * Checks if parameter is a string or function. + * If it is a string, then we will just return it. + * If it is a function, then we will call it with + * any additional arguments and return the result. + */ +function result(parameter: string): string; +function result unknown, TArgs extends unknown[]>(parameter: TFunction, ...args: TArgs): ReturnType; +function result unknown, TArgs extends unknown[]>(parameter: TFunction, ...args: TArgs): ReturnType | string { + return typeof parameter === 'function' ? (parameter(...args) as ReturnType) : parameter; +} + +export {startsWith, result}; diff --git a/lib/SyncQueue.js b/lib/SyncQueue.js deleted file mode 100644 index baf5e5691..000000000 --- a/lib/SyncQueue.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Synchronous queue that can be used to ensure promise based tasks are run in sequence. - * Pass to the constructor a function that returns a promise to run the task then add data. - * - * @example - * - * const queue = new SyncQueue(({key, val}) => { - * return someAsyncProcess(key, val); - * }); - * - * queue.push({key: 1, val: '1'}); - * queue.push({key: 2, val: '2'}); - */ -export default class SyncQueue { - /** - * @param {Function} run - must return a promise - */ - constructor(run) { - this.queue = []; - this.isProcessing = false; - this.run = run; - } - - /** - * Stop the queue from being processed and clear out any existing tasks - */ - abort() { - this.queue = []; - this.isProcessing = false; - } - - process() { - if (this.isProcessing || this.queue.length === 0) { - return; - } - - this.isProcessing = true; - - const {data, resolve, reject} = this.queue.shift(); - this.run(data) - .then(resolve) - .catch(reject) - .finally(() => { - this.isProcessing = false; - this.process(); - }); - } - - /** - * @param {*} data - * @returns {Promise} - */ - push(data) { - return new Promise((resolve, reject) => { - this.queue.push({resolve, reject, data}); - this.process(); - }); - } -} diff --git a/lib/compose.js b/lib/compose.js deleted file mode 100644 index 702f3683c..000000000 --- a/lib/compose.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * This is a utility function taken directly from Redux. (We don't want to add Redux as a dependency) - * It enables functional composition, useful for the chaining/composition of HOCs. - * - * For example, instead of: - * - * export default hoc1(config1, hoc2(config2, hoc3(config3)))(Component); - * - * Use this instead: - * - * export default compose( - * hoc1(config1), - * hoc2(config2), - * hoc3(config3), - * )(Component) - * - * @returns {Function} - */ -export default function compose(...funcs) { - if (funcs.length === 0) { - return (arg) => arg; - } - - if (funcs.length === 1) { - return funcs[0]; - } - - // eslint-disable-next-line rulesdir/prefer-underscore-method - return funcs.reduce( - (a, b) => - (...args) => - a(b(...args)), - ); -} diff --git a/lib/createDeferredTask.js b/lib/createDeferredTask.ts similarity index 61% rename from lib/createDeferredTask.js rename to lib/createDeferredTask.ts index b5b2e308a..81610e2bc 100644 --- a/lib/createDeferredTask.js +++ b/lib/createDeferredTask.ts @@ -1,13 +1,16 @@ +type DeferredTask = { + promise: Promise; + resolve: (value: unknown) => void; +}; + /** * Create a deferred task that can be resolved when we call `resolve()` * The returned promise will complete when we call `resolve` * Useful when we want to wait for a tasks that is resolved from an external action - * - * @template T - * @returns {{ resolve: function(*), promise: Promise }} */ -export default function createDeferredTask() { - const deferred = {}; +export default function createDeferredTask(): DeferredTask { + const deferred: DeferredTask = {} as DeferredTask; + deferred.promise = new Promise((res) => { deferred.resolve = res; }); From 39768e58ed95c1b72ae8f4a2f31ea0796d608d17 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 29 Feb 2024 11:48:06 +0100 Subject: [PATCH 2/9] Fix after review --- lib/createDeferredTask.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/createDeferredTask.ts b/lib/createDeferredTask.ts index 81610e2bc..3d37c91f3 100644 --- a/lib/createDeferredTask.ts +++ b/lib/createDeferredTask.ts @@ -1,6 +1,6 @@ type DeferredTask = { - promise: Promise; - resolve: (value: unknown) => void; + promise: Promise; + resolve?: () => void; }; /** @@ -9,7 +9,7 @@ type DeferredTask = { * Useful when we want to wait for a tasks that is resolved from an external action */ export default function createDeferredTask(): DeferredTask { - const deferred: DeferredTask = {} as DeferredTask; + const deferred = {} as DeferredTask; deferred.promise = new Promise((res) => { deferred.resolve = res; From d9569cf9680baed79d04bfeddf72b9682145caa4 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Mon, 4 Mar 2024 14:37:16 +0000 Subject: [PATCH 3/9] 2.0.13 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1970441c5..590ee19f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-onyx", - "version": "2.0.12", + "version": "2.0.13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-native-onyx", - "version": "2.0.12", + "version": "2.0.13", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 1ac332a2d..5d61479d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-onyx", - "version": "2.0.12", + "version": "2.0.13", "author": "Expensify, Inc.", "homepage": "https://expensify.com", "description": "State management for React Native", From 29ec13bd6da9aa6d7aa9fa76ef4a45622eac1b6e Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Wed, 28 Feb 2024 16:33:51 +0100 Subject: [PATCH 4/9] feat: fallback to InMemoryProvider --- lib/storage/__mocks__/index.ts | 1 + lib/storage/index.ts | 138 +++++++++++++------ lib/storage/providers/IDBKeyValProvider.ts | 4 + lib/storage/providers/MemoryOnlyProvider.ts | 144 ++++++++++++++++++++ lib/storage/providers/NoopProvider.ts | 103 ++++++++++++++ lib/storage/providers/SQLiteProvider.ts | 4 + lib/storage/providers/types.ts | 6 +- package-lock.json | 59 +++++++- package.json | 1 + 9 files changed, 408 insertions(+), 52 deletions(-) create mode 100644 lib/storage/providers/MemoryOnlyProvider.ts create mode 100644 lib/storage/providers/NoopProvider.ts diff --git a/lib/storage/__mocks__/index.ts b/lib/storage/__mocks__/index.ts index 2c8578ea2..87b6712bd 100644 --- a/lib/storage/__mocks__/index.ts +++ b/lib/storage/__mocks__/index.ts @@ -10,6 +10,7 @@ const set = jest.fn((key, value) => { }); const idbKeyvalMock: StorageProvider = { + name: 'KeyValMockProvider', init: () => undefined, setItem(key, value) { return set(key, value); diff --git a/lib/storage/index.ts b/lib/storage/index.ts index a38348fc2..acb5a87bb 100644 --- a/lib/storage/index.ts +++ b/lib/storage/index.ts @@ -1,13 +1,52 @@ +import * as Logger from '../Logger'; + import PlatformStorage from './platforms'; import InstanceSync from './InstanceSync'; +import NoopProvider from './providers/NoopProvider'; import type StorageProvider from './providers/types'; -const provider = PlatformStorage; +let provider = PlatformStorage; let shouldKeepInstancesSync = false; +let finishInitalization: (value?: unknown) => void; +const initPromise = new Promise((resolve) => { + finishInitalization = resolve; +}); type Storage = { getStorageProvider: () => StorageProvider; -} & StorageProvider; +} & Omit; + +/** + * Degrade performance by removing the storage provider and only using cache + */ +function degradePerformance(error: Error) { + Logger.logAlert(`Error while using ${provider.name}. Falling back to only using cache and dropping storage.`); + console.error(error); + provider = NoopProvider; +} + +/** + * Runs a piece of code and degrades performance if certain errors are thrown + */ +function tryOrDegradePerformance(fn: () => Promise | T): Promise { + return new Promise((resolve, reject) => { + initPromise.then(() => { + try { + resolve(fn()); + } catch (error) { + // Test for known critical errors that the storage provider throws, e.g. when storage is full + if (error instanceof Error) { + // IndexedDB error when storage is full (https://github.com/Expensify/App/issues/29403) + if (error.message.includes('Internal error opening backing store for indexedDB.open')) { + degradePerformance(error); + } + } + + reject(error); + } + }); + }); +} const Storage: Storage = { /** @@ -22,112 +61,121 @@ const Storage: Storage = { * and enables fallback providers if necessary */ init() { - provider.init(); + tryOrDegradePerformance(() => { + provider.init(); + }).finally(() => { + finishInitalization(); + }); }, /** * Get the value of a given key or return `null` if it's not available */ - getItem: (key) => provider.getItem(key), + getItem: (key) => tryOrDegradePerformance(() => provider.getItem(key)), /** * Get multiple key-value pairs for the give array of keys in a batch */ - multiGet: (keys) => provider.multiGet(keys), + multiGet: (keys) => tryOrDegradePerformance(() => provider.multiGet(keys)), /** * Sets the value for a given key. The only requirement is that the value should be serializable to JSON string */ - setItem: (key, value) => { - const promise = provider.setItem(key, value); + setItem: (key, value) => + tryOrDegradePerformance(() => { + const promise = provider.setItem(key, value); - if (shouldKeepInstancesSync) { - return promise.then(() => InstanceSync.setItem(key)); - } + if (shouldKeepInstancesSync) { + return promise.then(() => InstanceSync.setItem(key)); + } - return promise; - }, + return promise; + }), /** * Stores multiple key-value pairs in a batch */ - multiSet: (pairs) => provider.multiSet(pairs), + multiSet: (pairs) => tryOrDegradePerformance(() => provider.multiSet(pairs)), /** * Merging an existing value with a new one */ - mergeItem: (key, changes, modifiedData) => { - const promise = provider.mergeItem(key, changes, modifiedData); + mergeItem: (key, changes, modifiedData) => + tryOrDegradePerformance(() => { + const promise = provider.mergeItem(key, changes, modifiedData); - if (shouldKeepInstancesSync) { - return promise.then(() => InstanceSync.mergeItem(key)); - } + if (shouldKeepInstancesSync) { + return promise.then(() => InstanceSync.mergeItem(key)); + } - return promise; - }, + return promise; + }), /** * Multiple merging of existing and new values in a batch * This function also removes all nested null values from an object. */ - multiMerge: (pairs) => provider.multiMerge(pairs), + multiMerge: (pairs) => tryOrDegradePerformance(() => provider.multiMerge(pairs)), /** * Removes given key and its value */ - removeItem: (key) => { - const promise = provider.removeItem(key); + removeItem: (key) => + tryOrDegradePerformance(() => { + const promise = provider.removeItem(key); - if (shouldKeepInstancesSync) { - return promise.then(() => InstanceSync.removeItem(key)); - } + if (shouldKeepInstancesSync) { + return promise.then(() => InstanceSync.removeItem(key)); + } - return promise; - }, + return promise; + }), /** * Remove given keys and their values */ - removeItems: (keys) => { - const promise = provider.removeItems(keys); + removeItems: (keys) => + tryOrDegradePerformance(() => { + const promise = provider.removeItems(keys); - if (shouldKeepInstancesSync) { - return promise.then(() => InstanceSync.removeItems(keys)); - } + if (shouldKeepInstancesSync) { + return promise.then(() => InstanceSync.removeItems(keys)); + } - return promise; - }, + return promise; + }), /** * Clears everything */ - clear: () => { - if (shouldKeepInstancesSync) { - return InstanceSync.clear(() => provider.clear()); - } + clear: () => + tryOrDegradePerformance(() => { + if (shouldKeepInstancesSync) { + return InstanceSync.clear(() => provider.clear()); + } - return provider.clear(); - }, + return provider.clear(); + }), // This is a noop for now in order to keep clients from crashing see https://github.com/Expensify/Expensify/issues/312438 - setMemoryOnlyKeys: () => provider.setMemoryOnlyKeys(), + setMemoryOnlyKeys: () => tryOrDegradePerformance(() => provider.setMemoryOnlyKeys()), /** * Returns all available keys */ - getAllKeys: () => provider.getAllKeys(), + getAllKeys: () => tryOrDegradePerformance(() => provider.getAllKeys()), /** * Gets the total bytes of the store */ - getDatabaseSize: () => provider.getDatabaseSize(), + getDatabaseSize: () => tryOrDegradePerformance(() => provider.getDatabaseSize()), /** * @param onStorageKeyChanged - Storage synchronization mechanism keeping all opened tabs in sync (web only) */ keepInstancesSync(onStorageKeyChanged) { // If InstanceSync is null, it means we're on a native platform and we don't need to keep instances in sync - if (InstanceSync == null) return; + if (InstanceSync === null) return; shouldKeepInstancesSync = true; InstanceSync.init(onStorageKeyChanged); diff --git a/lib/storage/providers/IDBKeyValProvider.ts b/lib/storage/providers/IDBKeyValProvider.ts index ea3593999..fac5704d8 100644 --- a/lib/storage/providers/IDBKeyValProvider.ts +++ b/lib/storage/providers/IDBKeyValProvider.ts @@ -9,6 +9,10 @@ import type {Value} from './types'; let idbKeyValStore: UseStore; const provider: StorageProvider = { + /** + * The name of the provider that can be printed to the logs + */ + name: 'IDBKeyValProvider', /** * Initializes the storage provider */ diff --git a/lib/storage/providers/MemoryOnlyProvider.ts b/lib/storage/providers/MemoryOnlyProvider.ts new file mode 100644 index 000000000..c319ee3b0 --- /dev/null +++ b/lib/storage/providers/MemoryOnlyProvider.ts @@ -0,0 +1,144 @@ +import _ from 'underscore'; +import sizeof from 'object-sizeof'; +import utils from '../../utils'; +import type StorageProvider from './types'; +import type {Key, KeyValuePair, Value} from './types'; + +type Store = Record; + +// eslint-disable-next-line import/no-mutable-exports +let store: Store = {}; + +const setInternal = (key: Key, value: Value) => { + store[key] = value; + return Promise.resolve(value); +}; + +const isJestRunning = typeof jest !== 'undefined'; +const set = isJestRunning ? jest.fn(setInternal) : setInternal; + +const provider: StorageProvider = { + /** + * The name of the provider that can be printed to the logs + */ + name: 'MemoryOnlyProvider', + + /** + * Initializes the storage provider + */ + init() { + // do nothing + }, + + /** + * Get the value of a given key or return `null` if it's not available in memory + */ + getItem(key) { + const value = store[key]; + + return Promise.resolve(value === undefined ? null : value); + }, + + /** + * Get multiple key-value pairs for the give array of keys in a batch. + */ + multiGet(keys) { + const getPromises = _.map(keys, (key) => new Promise((resolve) => this.getItem(key).then((value) => resolve([key, value])))) as Array>; + return Promise.all(getPromises); + }, + + /** + * Sets the value for a given key. The only requirement is that the value should be serializable to JSON string + */ + setItem(key, value) { + set(key, value); + + return Promise.resolve(); + }, + + /** + * Stores multiple key-value pairs in a batch + */ + multiSet(pairs) { + const setPromises = _.map(pairs, ([key, value]) => this.setItem(key, value)); + return new Promise((resolve) => Promise.all(setPromises).then(() => resolve())); + }, + + /** + * Merging an existing value with a new one + */ + mergeItem(key, _changes, modifiedData) { + // Since Onyx already merged the existing value with the changes, we can just set the value directly + return this.setItem(key, modifiedData); + }, + + /** + * Multiple merging of existing and new values in a batch + * This function also removes all nested null values from an object. + */ + multiMerge(pairs) { + _.forEach(pairs, ([key, value]) => { + const existingValue = store[key] as unknown as Record; + const newValue = utils.fastMerge(existingValue, value as unknown as Record) as unknown as Value; + + set(key, newValue); + }); + + return Promise.resolve([]); + }, + + /** + * Remove given key and it's value from memory + */ + removeItem(key) { + delete store[key]; + return Promise.resolve(); + }, + + /** + * Remove given keys and their values from memory + */ + removeItems(keys) { + _.each(keys, (key) => { + delete store[key]; + }); + return Promise.resolve(); + }, + + /** + * Clear everything from memory + */ + clear() { + store = {}; + return Promise.resolve(); + }, + + // This is a noop for now in order to keep clients from crashing see https://github.com/Expensify/Expensify/issues/312438 + setMemoryOnlyKeys() { + // do nothing + }, + + /** + * Returns all keys available in memory + */ + getAllKeys() { + return Promise.resolve(_.keys(store)); + }, + + /** + * Gets the total bytes of the store. + * `bytesRemaining` will always be `Number.POSITIVE_INFINITY` since we don't have a hard limit on memory. + */ + getDatabaseSize() { + const storeSize = sizeof(store); + + return Promise.resolve({bytesRemaining: Number.POSITIVE_INFINITY, bytesUsed: storeSize}); + }, +}; + +const setMockStore = (data: Store) => { + store = data; +}; + +export default provider; +export {store as mockStore, set as mockSet, setMockStore}; diff --git a/lib/storage/providers/NoopProvider.ts b/lib/storage/providers/NoopProvider.ts new file mode 100644 index 000000000..06d05e65d --- /dev/null +++ b/lib/storage/providers/NoopProvider.ts @@ -0,0 +1,103 @@ +import type StorageProvider from './types'; + +const provider: StorageProvider = { + /** + * The name of the provider that can be printed to the logs + */ + name: 'NoopProvider', + + /** + * Initializes the storage provider + */ + init() { + // do nothing + }, + + /** + * Get the value of a given key or return `null` if it's not available in memory + * @param {String} key + * @return {Promise<*>} + */ + getItem() { + return Promise.resolve(null); + }, + + /** + * Get multiple key-value pairs for the give array of keys in a batch. + */ + multiGet() { + return Promise.resolve([]); + }, + + /** + * Sets the value for a given key. The only requirement is that the value should be serializable to JSON string + */ + setItem() { + return Promise.resolve(); + }, + + /** + * Stores multiple key-value pairs in a batch + */ + multiSet() { + return Promise.resolve(); + }, + + /** + * Merging an existing value with a new one + */ + mergeItem() { + return Promise.resolve(); + }, + + /** + * Multiple merging of existing and new values in a batch + * This function also removes all nested null values from an object. + */ + multiMerge() { + return Promise.resolve([]); + }, + + /** + * Remove given key and it's value from memory + */ + removeItem() { + return Promise.resolve(); + }, + + /** + * Remove given keys and their values from memory + */ + removeItems() { + return Promise.resolve(); + }, + + /** + * Clear everything from memory + */ + clear() { + return Promise.resolve(); + }, + + // This is a noop for now in order to keep clients from crashing see https://github.com/Expensify/Expensify/issues/312438 + setMemoryOnlyKeys() { + // do nothing + }, + + /** + * Returns all keys available in memory + */ + getAllKeys() { + return Promise.resolve([]); + }, + + /** + * Gets the total bytes of the store. + * `bytesRemaining` will always be `Number.POSITIVE_INFINITY` since we don't have a hard limit on memory. + */ + getDatabaseSize() { + return Promise.resolve({bytesRemaining: Number.POSITIVE_INFINITY, bytesUsed: 0}); + }, +}; + +export default provider; diff --git a/lib/storage/providers/SQLiteProvider.ts b/lib/storage/providers/SQLiteProvider.ts index 4b93e821c..1a86088bf 100644 --- a/lib/storage/providers/SQLiteProvider.ts +++ b/lib/storage/providers/SQLiteProvider.ts @@ -13,6 +13,10 @@ const DB_NAME = 'OnyxDB'; let db: QuickSQLiteConnection; const provider: StorageProvider = { + /** + * The name of the provider that can be printed to the logs + */ + name: 'SQLiteProvider', /** * Initializes the storage provider */ diff --git a/lib/storage/providers/types.ts b/lib/storage/providers/types.ts index 8749034d4..48ec62979 100644 --- a/lib/storage/providers/types.ts +++ b/lib/storage/providers/types.ts @@ -9,6 +9,10 @@ type KeyValuePairList = KeyValuePair[]; type OnStorageKeyChanged = (key: Key, value: Value | null) => void; type StorageProvider = { + /** + * The name of the provider that can be printed to the logs + */ + name: string; /** * Initializes the storage provider */ @@ -82,4 +86,4 @@ type StorageProvider = { }; export default StorageProvider; -export type {Value, Key, KeyList, KeyValuePairList, OnStorageKeyChanged}; +export type {Value, Key, KeyList, KeyValuePair, KeyValuePairList, OnStorageKeyChanged}; diff --git a/package-lock.json b/package-lock.json index 590ee19f4..c53fc65cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", + "object-sizeof": "^2.6.4", "underscore": "^1.13.6" }, "devDependencies": { @@ -5357,7 +5358,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -9586,7 +9586,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -13898,6 +13897,37 @@ "node": ">= 0.4" } }, + "node_modules/object-sizeof": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/object-sizeof/-/object-sizeof-2.6.4.tgz", + "integrity": "sha512-YuJAf7Bi61KROcYmXm8RCeBrBw8UOaJDzTm1gp0eU7RjYi1xEte3/Nmg/VyPaHcJZ3sNojs1Y0xvSrgwkLmcFw==", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/object-sizeof/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/object-to-spawn-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", @@ -22273,8 +22303,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "binary-extensions": { "version": "2.2.0", @@ -25529,8 +25558,7 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { "version": "4.0.6", @@ -28947,6 +28975,25 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, + "object-sizeof": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/object-sizeof/-/object-sizeof-2.6.4.tgz", + "integrity": "sha512-YuJAf7Bi61KROcYmXm8RCeBrBw8UOaJDzTm1gp0eU7RjYi1xEte3/Nmg/VyPaHcJZ3sNojs1Y0xvSrgwkLmcFw==", + "requires": { + "buffer": "^6.0.3" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + } + } + }, "object-to-spawn-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", diff --git a/package.json b/package.json index 5d61479d0..07b77d3e6 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", + "object-sizeof": "^2.6.4", "underscore": "^1.13.6" }, "devDependencies": { From b6fb255c936dc2a00f3a070e293df41ccc1fc1a0 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Wed, 28 Feb 2024 16:56:18 +0100 Subject: [PATCH 5/9] test: verify all methods are implemented --- tests/unit/storage/providers/StorageProviderTest.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/storage/providers/StorageProviderTest.js b/tests/unit/storage/providers/StorageProviderTest.js index 5ce43cfc9..589eff96d 100644 --- a/tests/unit/storage/providers/StorageProviderTest.js +++ b/tests/unit/storage/providers/StorageProviderTest.js @@ -6,10 +6,12 @@ jest.unmock('../../../../lib/storage/providers/IDBKeyValProvider'); import _ from 'underscore'; import NativeStorage from '../../../../lib/storage/platforms/index.native'; import WebStorage from '../../../../lib/storage/platforms/index'; +import MemoryOnlyProvider from '../../../../lib/storage/providers/MemoryOnlyProvider'; it('storage providers have same methods implemented', () => { + const memoryOnlyMethods = _.keys(MemoryOnlyProvider); const nativeMethods = _.keys(NativeStorage); const webMethods = _.keys(WebStorage); - const unimplementedMethods = _.difference(nativeMethods, webMethods); + const unimplementedMethods = _.difference(nativeMethods, webMethods, memoryOnlyMethods); expect(unimplementedMethods.length).toBe(0); }); From 8b6d93de98b343304e714945d2b3f5a565519afd Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Wed, 28 Feb 2024 17:00:16 +0100 Subject: [PATCH 6/9] refactor: move MemoryOnlyProvider to 3rd PR --- lib/storage/providers/MemoryOnlyProvider.ts | 144 ------------------ .../storage/providers/StorageProviderTest.js | 4 +- 2 files changed, 1 insertion(+), 147 deletions(-) delete mode 100644 lib/storage/providers/MemoryOnlyProvider.ts diff --git a/lib/storage/providers/MemoryOnlyProvider.ts b/lib/storage/providers/MemoryOnlyProvider.ts deleted file mode 100644 index c319ee3b0..000000000 --- a/lib/storage/providers/MemoryOnlyProvider.ts +++ /dev/null @@ -1,144 +0,0 @@ -import _ from 'underscore'; -import sizeof from 'object-sizeof'; -import utils from '../../utils'; -import type StorageProvider from './types'; -import type {Key, KeyValuePair, Value} from './types'; - -type Store = Record; - -// eslint-disable-next-line import/no-mutable-exports -let store: Store = {}; - -const setInternal = (key: Key, value: Value) => { - store[key] = value; - return Promise.resolve(value); -}; - -const isJestRunning = typeof jest !== 'undefined'; -const set = isJestRunning ? jest.fn(setInternal) : setInternal; - -const provider: StorageProvider = { - /** - * The name of the provider that can be printed to the logs - */ - name: 'MemoryOnlyProvider', - - /** - * Initializes the storage provider - */ - init() { - // do nothing - }, - - /** - * Get the value of a given key or return `null` if it's not available in memory - */ - getItem(key) { - const value = store[key]; - - return Promise.resolve(value === undefined ? null : value); - }, - - /** - * Get multiple key-value pairs for the give array of keys in a batch. - */ - multiGet(keys) { - const getPromises = _.map(keys, (key) => new Promise((resolve) => this.getItem(key).then((value) => resolve([key, value])))) as Array>; - return Promise.all(getPromises); - }, - - /** - * Sets the value for a given key. The only requirement is that the value should be serializable to JSON string - */ - setItem(key, value) { - set(key, value); - - return Promise.resolve(); - }, - - /** - * Stores multiple key-value pairs in a batch - */ - multiSet(pairs) { - const setPromises = _.map(pairs, ([key, value]) => this.setItem(key, value)); - return new Promise((resolve) => Promise.all(setPromises).then(() => resolve())); - }, - - /** - * Merging an existing value with a new one - */ - mergeItem(key, _changes, modifiedData) { - // Since Onyx already merged the existing value with the changes, we can just set the value directly - return this.setItem(key, modifiedData); - }, - - /** - * Multiple merging of existing and new values in a batch - * This function also removes all nested null values from an object. - */ - multiMerge(pairs) { - _.forEach(pairs, ([key, value]) => { - const existingValue = store[key] as unknown as Record; - const newValue = utils.fastMerge(existingValue, value as unknown as Record) as unknown as Value; - - set(key, newValue); - }); - - return Promise.resolve([]); - }, - - /** - * Remove given key and it's value from memory - */ - removeItem(key) { - delete store[key]; - return Promise.resolve(); - }, - - /** - * Remove given keys and their values from memory - */ - removeItems(keys) { - _.each(keys, (key) => { - delete store[key]; - }); - return Promise.resolve(); - }, - - /** - * Clear everything from memory - */ - clear() { - store = {}; - return Promise.resolve(); - }, - - // This is a noop for now in order to keep clients from crashing see https://github.com/Expensify/Expensify/issues/312438 - setMemoryOnlyKeys() { - // do nothing - }, - - /** - * Returns all keys available in memory - */ - getAllKeys() { - return Promise.resolve(_.keys(store)); - }, - - /** - * Gets the total bytes of the store. - * `bytesRemaining` will always be `Number.POSITIVE_INFINITY` since we don't have a hard limit on memory. - */ - getDatabaseSize() { - const storeSize = sizeof(store); - - return Promise.resolve({bytesRemaining: Number.POSITIVE_INFINITY, bytesUsed: storeSize}); - }, -}; - -const setMockStore = (data: Store) => { - store = data; -}; - -export default provider; -export {store as mockStore, set as mockSet, setMockStore}; diff --git a/tests/unit/storage/providers/StorageProviderTest.js b/tests/unit/storage/providers/StorageProviderTest.js index 589eff96d..5ce43cfc9 100644 --- a/tests/unit/storage/providers/StorageProviderTest.js +++ b/tests/unit/storage/providers/StorageProviderTest.js @@ -6,12 +6,10 @@ jest.unmock('../../../../lib/storage/providers/IDBKeyValProvider'); import _ from 'underscore'; import NativeStorage from '../../../../lib/storage/platforms/index.native'; import WebStorage from '../../../../lib/storage/platforms/index'; -import MemoryOnlyProvider from '../../../../lib/storage/providers/MemoryOnlyProvider'; it('storage providers have same methods implemented', () => { - const memoryOnlyMethods = _.keys(MemoryOnlyProvider); const nativeMethods = _.keys(NativeStorage); const webMethods = _.keys(WebStorage); - const unimplementedMethods = _.difference(nativeMethods, webMethods, memoryOnlyMethods); + const unimplementedMethods = _.difference(nativeMethods, webMethods); expect(unimplementedMethods.length).toBe(0); }); From 5491ea214b802d706180975684b257eed07e545f Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Wed, 28 Feb 2024 18:49:24 +0100 Subject: [PATCH 7/9] chore: remove unused package --- package-lock.json | 59 +++++------------------------------------------ package.json | 1 - 2 files changed, 6 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index c53fc65cb..590ee19f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", - "object-sizeof": "^2.6.4", "underscore": "^1.13.6" }, "devDependencies": { @@ -5358,6 +5357,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -9586,6 +9586,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", @@ -13897,37 +13898,6 @@ "node": ">= 0.4" } }, - "node_modules/object-sizeof": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/object-sizeof/-/object-sizeof-2.6.4.tgz", - "integrity": "sha512-YuJAf7Bi61KROcYmXm8RCeBrBw8UOaJDzTm1gp0eU7RjYi1xEte3/Nmg/VyPaHcJZ3sNojs1Y0xvSrgwkLmcFw==", - "dependencies": { - "buffer": "^6.0.3" - } - }, - "node_modules/object-sizeof/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/object-to-spawn-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", @@ -22303,7 +22273,8 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true }, "binary-extensions": { "version": "2.2.0", @@ -25558,7 +25529,8 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true }, "ignore": { "version": "4.0.6", @@ -28975,25 +28947,6 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, - "object-sizeof": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/object-sizeof/-/object-sizeof-2.6.4.tgz", - "integrity": "sha512-YuJAf7Bi61KROcYmXm8RCeBrBw8UOaJDzTm1gp0eU7RjYi1xEte3/Nmg/VyPaHcJZ3sNojs1Y0xvSrgwkLmcFw==", - "requires": { - "buffer": "^6.0.3" - }, - "dependencies": { - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - } - } - }, "object-to-spawn-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", diff --git a/package.json b/package.json index 07b77d3e6..5d61479d0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", - "object-sizeof": "^2.6.4", "underscore": "^1.13.6" }, "devDependencies": { From 956193b5d8b06ac36d1ad52debaad77fcb38104e Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Fri, 1 Mar 2024 12:19:23 +0100 Subject: [PATCH 8/9] fix: simplify initizlization --- lib/storage/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/storage/index.ts b/lib/storage/index.ts index acb5a87bb..7090cd6ce 100644 --- a/lib/storage/index.ts +++ b/lib/storage/index.ts @@ -61,9 +61,7 @@ const Storage: Storage = { * and enables fallback providers if necessary */ init() { - tryOrDegradePerformance(() => { - provider.init(); - }).finally(() => { + tryOrDegradePerformance(provider.init).finally(() => { finishInitalization(); }); }, From 0dabe97da883c11a02e5c1d327e0390a1c8d51f1 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Mon, 4 Mar 2024 17:59:50 +0000 Subject: [PATCH 9/9] 2.0.14 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 590ee19f4..1fb05cb73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-onyx", - "version": "2.0.13", + "version": "2.0.14", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-native-onyx", - "version": "2.0.13", + "version": "2.0.14", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 5d61479d0..cb7050d5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-onyx", - "version": "2.0.13", + "version": "2.0.14", "author": "Expensify, Inc.", "homepage": "https://expensify.com", "description": "State management for React Native",