From df729c3a1e11d39a673035c47a06fad7a987e5b5 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 28 Oct 2025 13:49:22 +0100 Subject: [PATCH 01/23] Update evictStorageAndRetry to not evict the data if error isn't storage related --- API-INTERNAL.md | 22 ++++++----- lib/Onyx.ts | 6 +-- lib/OnyxUtils.ts | 52 +++++++++++++++----------- tests/perf-test/OnyxUtils.perf-test.ts | 4 +- 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/API-INTERNAL.md b/API-INTERNAL.md index b853a457..f2610b37 100644 --- a/API-INTERNAL.md +++ b/API-INTERNAL.md @@ -109,10 +109,11 @@ subscriber callbacks receive the data in a different format than they normally e
remove()

Remove a key from Onyx and update the subscribers

-
evictStorageAndRetry()
-

If we fail to set or merge we must handle this by -evicting some data from Onyx and then retrying to do -whatever it is we attempted to do.

+
retryOperation()
+

Handles storage operation failures based on the error type: +- Storage capacity errors: evicts data and retries the operation +- Invalid data errors: logs an alert and throws an error +- Other errors: retries the operation

broadcastUpdate()

Notifies subscribers and writes current value to cache

@@ -405,13 +406,14 @@ subscriber callbacks receive the data in a different format than they normally e ## remove() Remove a key from Onyx and update the subscribers -**Kind**: global function - +**Kind**: global function + -## evictStorageAndRetry() -If we fail to set or merge we must handle this by -evicting some data from Onyx and then retrying to do -whatever it is we attempted to do. +## retryOperation() +Handles storage operation failures based on the error type: +- Storage capacity errors: evicts data and retries the operation +- Invalid data errors: logs an alert and throws an error +- Other errors: retries the operation **Kind**: global function diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 49dd300c..41186f64 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -208,7 +208,7 @@ function set(key: TKey, value: OnyxSetInput, options } return Storage.setItem(key, valueWithoutNestedNullValues) - .catch((error) => OnyxUtils.evictStorageAndRetry(error, set, key, valueWithoutNestedNullValues)) + .catch((error) => OnyxUtils.retryOperation(error, set, key, valueWithoutNestedNullValues)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues); return updatePromise; @@ -258,7 +258,7 @@ function multiSet(data: OnyxMultiSetInput): Promise { }); return Storage.multiSet(keyValuePairsToSet) - .catch((error) => OnyxUtils.evictStorageAndRetry(error, multiSet, newData)) + .catch((error) => OnyxUtils.retryOperation(error, multiSet, newData)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData); return Promise.all(updatePromises); @@ -702,7 +702,7 @@ function setCollection(collectionKey: TKey const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); return Storage.multiSet(keyValuePairs) - .catch((error) => OnyxUtils.evictStorageAndRetry(error, setCollection, collectionKey, collection)) + .catch((error) => OnyxUtils.retryOperation(error, setCollection, collectionKey, collection)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); return updatePromise; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 39e8afd0..aa90a59e 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -858,11 +858,12 @@ function reportStorageQuota(): Promise { } /** - * If we fail to set or merge we must handle this by - * evicting some data from Onyx and then retrying to do - * whatever it is we attempted to do. + * Handles storage operation failures based on the error type: + * - Storage capacity errors: evicts data and retries the operation + * - Invalid data errors: logs an alert and throws an error + * - Other errors: retries the operation */ -function evictStorageAndRetry( +function retryOperation( error: Error, onyxMethod: TMethod, ...args: Parameters @@ -874,22 +875,31 @@ function evictStorageAndRetry errorMessage?.includes(message)); + + if (isStorageCapacityError) { + // Find the first key that we can remove that has no subscribers in our blocklist + const keyForRemoval = cache.getKeyForEviction(); + if (!keyForRemoval) { + // If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case, + // then we should stop retrying as there is not much the user can do to fix this. Instead of getting them stuck in an infinite loop we + // will allow this write to be skipped. + Logger.logAlert('Out of storage. But found no acceptable keys to remove.'); + return reportStorageQuota(); + } - // Remove the least recently viewed key that is not currently being accessed and retry. - Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); - reportStorageQuota(); + // Remove the least recently viewed key that is not currently being accessed and retry. + Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); + reportStorageQuota(); + + // @ts-expect-error No overload matches this call. + return remove(keyForRemoval).then(() => onyxMethod(...args)); + } // @ts-expect-error No overload matches this call. - return remove(keyForRemoval).then(() => onyxMethod(...args)); + return onyxMethod(...args); } /** @@ -1341,7 +1351,7 @@ function mergeCollectionWithPatches( }); return Promise.all(promises) - .catch((error) => evictStorageAndRetry(error, mergeCollectionWithPatches, collectionKey, resultCollection)) + .catch((error) => retryOperation(error, mergeCollectionWithPatches, collectionKey, resultCollection)) .then(() => { sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection); return promiseUpdate; @@ -1396,7 +1406,7 @@ function partialSetCollection(collectionKe const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); return Storage.multiSet(keyValuePairs) - .catch((error) => evictStorageAndRetry(error, partialSetCollection, collectionKey, collection)) + .catch((error) => retryOperation(error, partialSetCollection, collectionKey, collection)) .then(() => { sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection); return updatePromise; @@ -1452,7 +1462,7 @@ const OnyxUtils = { scheduleNotifyCollectionSubscribers, remove, reportStorageQuota, - evictStorageAndRetry, + retryOperation, broadcastUpdate, hasPendingMergeForKey, prepareKeyValuePairsForStorage, @@ -1512,7 +1522,7 @@ GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { // @ts-expect-error Reassign reportStorageQuota = decorateWithMetrics(reportStorageQuota, 'OnyxUtils.reportStorageQuota'); // @ts-expect-error Complex type signature - evictStorageAndRetry = decorateWithMetrics(evictStorageAndRetry, 'OnyxUtils.evictStorageAndRetry'); + retryOperation = decorateWithMetrics(retryOperation, 'OnyxUtils.retryOperation'); // @ts-expect-error Reassign broadcastUpdate = decorateWithMetrics(broadcastUpdate, 'OnyxUtils.broadcastUpdate'); // @ts-expect-error Reassign diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts index 5a817b85..c4cb08bb 100644 --- a/tests/perf-test/OnyxUtils.perf-test.ts +++ b/tests/perf-test/OnyxUtils.perf-test.ts @@ -502,12 +502,12 @@ describe('OnyxUtils', () => { }); }); - describe('evictStorageAndRetry', () => { + describe('retryOperation', () => { test('one call', async () => { const error = new Error(); const onyxMethod = jest.fn() as typeof Onyx.set; - await measureAsyncFunction(() => OnyxUtils.evictStorageAndRetry(error, onyxMethod, '', null), { + await measureAsyncFunction(() => OnyxUtils.retryOperation(error, onyxMethod, '', null), { beforeEach: async () => { mockedReportActionsKeys.forEach((key) => OnyxCache.addLastAccessedKey(key, false)); OnyxCache.addLastAccessedKey(`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}1`, false); From 6fc70eda9ff76c65531d9975fb790cb9dd539707 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 28 Oct 2025 13:58:32 +0100 Subject: [PATCH 02/23] Minor improvement and test fix --- lib/OnyxUtils.ts | 34 ++++++++++++++++----------------- tests/unit/cacheEvictionTest.ts | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index aa90a59e..bc9db845 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -879,27 +879,27 @@ function retryOperation errorMessage?.includes(message)); - if (isStorageCapacityError) { - // Find the first key that we can remove that has no subscribers in our blocklist - const keyForRemoval = cache.getKeyForEviction(); - if (!keyForRemoval) { - // If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case, - // then we should stop retrying as there is not much the user can do to fix this. Instead of getting them stuck in an infinite loop we - // will allow this write to be skipped. - Logger.logAlert('Out of storage. But found no acceptable keys to remove.'); - return reportStorageQuota(); - } - - // Remove the least recently viewed key that is not currently being accessed and retry. - Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); - reportStorageQuota(); - + if (!isStorageCapacityError) { // @ts-expect-error No overload matches this call. - return remove(keyForRemoval).then(() => onyxMethod(...args)); + return onyxMethod(...args); } + // Find the first key that we can remove that has no subscribers in our blocklist + const keyForRemoval = cache.getKeyForEviction(); + if (!keyForRemoval) { + // If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case, + // then we should stop retrying as there is not much the user can do to fix this. Instead of getting them stuck in an infinite loop we + // will allow this write to be skipped. + Logger.logAlert('Out of storage. But found no acceptable keys to remove.'); + return reportStorageQuota(); + } + + // Remove the least recently viewed key that is not currently being accessed and retry. + Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); + reportStorageQuota(); + // @ts-expect-error No overload matches this call. - return onyxMethod(...args); + return remove(keyForRemoval).then(() => onyxMethod(...args)); } /** diff --git a/tests/unit/cacheEvictionTest.ts b/tests/unit/cacheEvictionTest.ts index 8e9e83b0..56585b50 100644 --- a/tests/unit/cacheEvictionTest.ts +++ b/tests/unit/cacheEvictionTest.ts @@ -45,7 +45,7 @@ test('Cache eviction', () => { const setItemMock = jest.fn(originalSetItem).mockImplementationOnce( () => new Promise((_resolve, reject) => { - reject(); + reject(new Error('out of memory')); }), ); StorageMock.setItem = setItemMock; From c4d29dc27de9788eb271153af6a4b3928190eeae Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 28 Oct 2025 17:19:11 +0100 Subject: [PATCH 03/23] Applied feedback --- lib/OnyxUtils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index bc9db845..9a09503b 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -49,6 +49,10 @@ const METHOD = { CLEAR: 'clear', } as const; +const IDB_STORAGE_ERRORS = ['quotaexceedederror'] as const; +const SQL_STORAGE_ERRORS = ['database or disk is full', 'disk I/O error', 'out of memory'] as const; +const STORAGE_ERRORS = [...IDB_STORAGE_ERRORS, ...SQL_STORAGE_ERRORS]; + type OnyxMethod = ValueOf; // Key/value store of Onyx key and arrays of values to merge @@ -876,8 +880,7 @@ function retryOperation errorMessage?.includes(message)); + const isStorageCapacityError = STORAGE_ERRORS.some((storageError) => storageError === error?.name?.toLowerCase() || errorMessage?.includes(storageError)); if (!isStorageCapacityError) { // @ts-expect-error No overload matches this call. From 67bd9d462eba6bf95a8b25dd25bae27e9e8977a2 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 29 Oct 2025 11:57:31 +0100 Subject: [PATCH 04/23] Add comments --- lib/OnyxUtils.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 9a09503b..6daa4b0a 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -49,8 +49,18 @@ const METHOD = { CLEAR: 'clear', } as const; -const IDB_STORAGE_ERRORS = ['quotaexceedederror'] as const; -const SQL_STORAGE_ERRORS = ['database or disk is full', 'disk I/O error', 'out of memory'] as const; +// IndexedDB errors that indicate storage capacity issues where eviction can help +const IDB_STORAGE_ERRORS = [ + 'quotaexceedederror', // Browser storage quota exceeded +] as const; + +// SQLite errors that indicate storage capacity issues where eviction can help +const SQL_STORAGE_ERRORS = [ + 'database or disk is full', // Device storage is full + 'disk I/O error', // File system I/O failure, often due to insufficient space or corrupted storage + 'out of memory', // Insufficient RAM or storage space to complete the operation +] as const; + const STORAGE_ERRORS = [...IDB_STORAGE_ERRORS, ...SQL_STORAGE_ERRORS]; type OnyxMethod = ValueOf; From a8fbd8515f1699908d94a5cf782599bab898deb4 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 29 Oct 2025 15:27:35 +0100 Subject: [PATCH 05/23] Add a simple limitation of operations retries --- lib/Onyx.ts | 15 +++++++++------ lib/OnyxUtils.ts | 23 ++++++++++++++++------- tests/perf-test/OnyxUtils.perf-test.ts | 2 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 41186f64..a76bca17 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -146,8 +146,9 @@ function disconnect(connection: Connection): void { * @param key ONYXKEY to set * @param value value to store * @param options optional configuration object + * @param retryAttempt retry attempt */ -function set(key: TKey, value: OnyxSetInput, options?: SetOptions): Promise { +function set(key: TKey, value: OnyxSetInput, options?: SetOptions, retryAttempt?: number): Promise { // When we use Onyx.set to set a key we want to clear the current delta changes from Onyx.merge that were queued // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes. if (OnyxUtils.hasPendingMergeForKey(key)) { @@ -208,7 +209,7 @@ function set(key: TKey, value: OnyxSetInput, options } return Storage.setItem(key, valueWithoutNestedNullValues) - .catch((error) => OnyxUtils.retryOperation(error, set, key, valueWithoutNestedNullValues)) + .catch((error) => OnyxUtils.retryOperation(error, set, [key, valueWithoutNestedNullValues, undefined], retryAttempt)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues); return updatePromise; @@ -221,8 +222,9 @@ function set(key: TKey, value: OnyxSetInput, options * @example Onyx.multiSet({'key1': 'a', 'key2': 'b'}); * * @param data object keyed by ONYXKEYS and the values to set + * @param retryAttempt retry attempt */ -function multiSet(data: OnyxMultiSetInput): Promise { +function multiSet(data: OnyxMultiSetInput, retryAttempt?: number): Promise { let newData = data; const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); @@ -258,7 +260,7 @@ function multiSet(data: OnyxMultiSetInput): Promise { }); return Storage.multiSet(keyValuePairsToSet) - .catch((error) => OnyxUtils.retryOperation(error, multiSet, newData)) + .catch((error) => OnyxUtils.retryOperation(error, multiSet, [newData], retryAttempt)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData); return Promise.all(updatePromises); @@ -650,8 +652,9 @@ function update(data: OnyxUpdate[]): Promise { * * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` * @param collection Object collection keyed by individual collection member keys and values + * @param retryAttempt retry attempt */ -function setCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { +function setCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput, retryAttempt?: number): Promise { let resultCollection: OnyxInputKeyValueMapping = collection; let resultCollectionKeys = Object.keys(resultCollection); @@ -702,7 +705,7 @@ function setCollection(collectionKey: TKey const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); return Storage.multiSet(keyValuePairs) - .catch((error) => OnyxUtils.retryOperation(error, setCollection, collectionKey, collection)) + .catch((error) => OnyxUtils.retryOperation(error, setCollection, [collectionKey, collection], retryAttempt)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); return updatePromise; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 6daa4b0a..602de151 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -63,6 +63,8 @@ const SQL_STORAGE_ERRORS = [ const STORAGE_ERRORS = [...IDB_STORAGE_ERRORS, ...SQL_STORAGE_ERRORS]; +const MAX_RETRY_ATTEMPTS = 5; + type OnyxMethod = ValueOf; // Key/value store of Onyx key and arrays of values to merge @@ -880,9 +882,13 @@ function reportStorageQuota(): Promise { function retryOperation( error: Error, onyxMethod: TMethod, - ...args: Parameters + args: Parameters, + retryAttempt: number | undefined, ): Promise { - Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}`); + const currentRetryAttempt = retryAttempt ?? 0; + const nextRetryAttempt = currentRetryAttempt + 1; + + Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}. retryAttempt: ${currentRetryAttempt}/${MAX_RETRY_ATTEMPTS}`); if (error && Str.startsWith(error.message, "Failed to execute 'put' on 'IDBObjectStore'")) { Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.'); @@ -894,7 +900,7 @@ function retryOperation MAX_RETRY_ATTEMPTS ? undefined : onyxMethod(...args, nextRetryAttempt); } // Find the first key that we can remove that has no subscribers in our blocklist @@ -912,7 +918,7 @@ function retryOperation onyxMethod(...args)); + return remove(keyForRemoval).then(() => (nextRetryAttempt > MAX_RETRY_ATTEMPTS ? undefined : onyxMethod(...args, nextRetryAttempt))); } /** @@ -1255,11 +1261,13 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array< * @param collection Object collection keyed by individual collection member keys and values * @param mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of * tuples that we'll use to replace the nested objects of that collection member record with something else. + * @param retryAttempt retry attempt */ function mergeCollectionWithPatches( collectionKey: TKey, collection: OnyxMergeCollectionInput, mergeReplaceNullPatches?: MultiMergeReplaceNullPatches, + retryAttempt?: number, ): Promise { if (!isValidNonEmptyCollectionForMerge(collection)) { Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.'); @@ -1364,7 +1372,7 @@ function mergeCollectionWithPatches( }); return Promise.all(promises) - .catch((error) => retryOperation(error, mergeCollectionWithPatches, collectionKey, resultCollection)) + .catch((error) => retryOperation(error, mergeCollectionWithPatches, [collectionKey, resultCollection, undefined], retryAttempt)) .then(() => { sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection); return promiseUpdate; @@ -1379,8 +1387,9 @@ function mergeCollectionWithPatches( * * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` * @param collection Object collection keyed by individual collection member keys and values + * @param retryAttempt retry attempt */ -function partialSetCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { +function partialSetCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput, retryAttempt?: number): Promise { let resultCollection: OnyxInputKeyValueMapping = collection; let resultCollectionKeys = Object.keys(resultCollection); @@ -1419,7 +1428,7 @@ function partialSetCollection(collectionKe const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); return Storage.multiSet(keyValuePairs) - .catch((error) => retryOperation(error, partialSetCollection, collectionKey, collection)) + .catch((error) => retryOperation(error, partialSetCollection, [collectionKey, collection], retryAttempt)) .then(() => { sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection); return updatePromise; diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts index c4cb08bb..172098e8 100644 --- a/tests/perf-test/OnyxUtils.perf-test.ts +++ b/tests/perf-test/OnyxUtils.perf-test.ts @@ -507,7 +507,7 @@ describe('OnyxUtils', () => { const error = new Error(); const onyxMethod = jest.fn() as typeof Onyx.set; - await measureAsyncFunction(() => OnyxUtils.retryOperation(error, onyxMethod, '', null), { + await measureAsyncFunction(() => OnyxUtils.retryOperation(error, onyxMethod, ['', null], 1), { beforeEach: async () => { mockedReportActionsKeys.forEach((key) => OnyxCache.addLastAccessedKey(key, false)); OnyxCache.addLastAccessedKey(`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}1`, false); From 3bfb0e53802b9ae4d57ee0022cc14a47b24dabe5 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 29 Oct 2025 18:15:46 +0100 Subject: [PATCH 06/23] re-run checks From 9db02f2a5f899afac74e74077c1721ccecb33379 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 30 Oct 2025 12:19:26 +0100 Subject: [PATCH 07/23] Add internal functions for retries --- lib/Onyx.ts | 68 ++++++++++++++++++++++++++++++++++++------------ lib/OnyxUtils.ts | 12 +++------ lib/types.ts | 13 +++++++++ 3 files changed, 69 insertions(+), 24 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index cb9fbb76..58174cf7 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -23,6 +23,8 @@ import type { OnyxInput, OnyxMethodMap, SetOptions, + SetParams, + SetCollectionParams, } from './types'; import OnyxUtils from './OnyxUtils'; import logMessages from './logMessages'; @@ -144,14 +146,14 @@ function disconnect(connection: Connection): void { } /** - * Write a value to our store with the given key + * Write a value to our store with the given key, retry if the operation fails. * * @param key ONYXKEY to set * @param value value to store * @param options optional configuration object * @param retryAttempt retry attempt */ -function set(key: TKey, value: OnyxSetInput, options?: SetOptions, retryAttempt?: number): Promise { +function setWithRetry({key, value, options}: SetParams, retryAttempt?: number): Promise { // When we use Onyx.set to set a key we want to clear the current delta changes from Onyx.merge that were queued // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes. if (OnyxUtils.hasPendingMergeForKey(key)) { @@ -212,7 +214,7 @@ function set(key: TKey, value: OnyxSetInput, options } return Storage.setItem(key, valueWithoutNestedNullValues) - .catch((error) => OnyxUtils.retryOperation(error, set, [key, valueWithoutNestedNullValues, undefined], retryAttempt)) + .catch((error) => OnyxUtils.retryOperation(error, setWithRetry, {key, value: valueWithoutNestedNullValues}, retryAttempt)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues); return updatePromise; @@ -220,14 +222,23 @@ function set(key: TKey, value: OnyxSetInput, options } /** - * Sets multiple keys and values + * Write a value to our store with the given key * - * @example Onyx.multiSet({'key1': 'a', 'key2': 'b'}); + * @param key ONYXKEY to set + * @param value value to store + * @param options optional configuration object + */ +function set(key: TKey, value: OnyxSetInput, options?: SetOptions): Promise { + return setWithRetry({key, value, options}); +} + +/** + * Sets multiple keys and values, retries if the operation fails * * @param data object keyed by ONYXKEYS and the values to set * @param retryAttempt retry attempt */ -function multiSet(data: OnyxMultiSetInput, retryAttempt?: number): Promise { +function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Promise { let newData = data; const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); @@ -263,7 +274,7 @@ function multiSet(data: OnyxMultiSetInput, retryAttempt?: number): Promise }); return Storage.multiSet(keyValuePairsToSet) - .catch((error) => OnyxUtils.retryOperation(error, multiSet, [newData], retryAttempt)) + .catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData); return Promise.all(updatePromises); @@ -271,6 +282,17 @@ function multiSet(data: OnyxMultiSetInput, retryAttempt?: number): Promise .then(() => undefined); } +/** + * Sets multiple keys and values + * + * @example Onyx.multiSet({'key1': 'a', 'key2': 'b'}); + * + * @param data object keyed by ONYXKEYS and the values to set + */ +function multiSet(data: OnyxMultiSetInput): Promise { + return multiSetWithRetry(data); +} + /** * Merge a new value into an existing value at a key. * @@ -647,18 +669,13 @@ function update(data: OnyxUpdate[]): Promise { /** * Sets a collection by replacing all existing collection members with new values. * Any existing collection members not included in the new data will be removed. - * - * @example - * Onyx.setCollection(ONYXKEYS.COLLECTION.REPORT, { - * [`${ONYXKEYS.COLLECTION.REPORT}1`]: report1, - * [`${ONYXKEYS.COLLECTION.REPORT}2`]: report2, - * }); + * Retries in case of failures. * * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` * @param collection Object collection keyed by individual collection member keys and values * @param retryAttempt retry attempt */ -function setCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput, retryAttempt?: number): Promise { +function setCollectionWithRetry({collectionKey, collection}: SetCollectionParams, retryAttempt?: number): Promise { let resultCollection: OnyxInputKeyValueMapping = collection; let resultCollectionKeys = Object.keys(resultCollection); @@ -709,7 +726,7 @@ function setCollection(collectionKey: TKey const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); return Storage.multiSet(keyValuePairs) - .catch((error) => OnyxUtils.retryOperation(error, setCollection, [collectionKey, collection], retryAttempt)) + .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, {collectionKey, collection}, retryAttempt)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); return updatePromise; @@ -717,6 +734,23 @@ function setCollection(collectionKey: TKey }); } +/** + * Sets a collection by replacing all existing collection members with new values. + * Any existing collection members not included in the new data will be removed. + * + * @example + * Onyx.setCollection(ONYXKEYS.COLLECTION.REPORT, { + * [`${ONYXKEYS.COLLECTION.REPORT}1`]: report1, + * [`${ONYXKEYS.COLLECTION.REPORT}2`]: report2, + * }); + * + * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` + * @param collection Object collection keyed by individual collection member keys and values + */ +function setCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { + return setCollectionWithRetry({collectionKey, collection}); +} + const Onyx = { METHOD: OnyxUtils.METHOD, connect, @@ -755,5 +789,7 @@ function applyDecorators() { /* eslint-enable rulesdir/prefer-actions-set-data */ } +type OnyxRetryOperation = typeof setWithRetry | typeof multiSetWithRetry | typeof setCollectionWithRetry; + export default Onyx; -export type {OnyxUpdate, ConnectOptions, SetOptions}; +export type {OnyxUpdate, ConnectOptions, SetOptions, OnyxRetryOperation}; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 90901acc..010988fc 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -6,6 +6,7 @@ import _ from 'underscore'; import DevTools from './DevTools'; import * as Logger from './Logger'; import type Onyx from './Onyx'; +import type {OnyxRetryOperation} from './Onyx'; import cache, {TASK} from './OnyxCache'; import * as Str from './Str'; import unstable_batchedUpdates from './batch'; @@ -886,12 +887,7 @@ function reportStorageQuota(): Promise { * - Invalid data errors: logs an alert and throws an error * - Other errors: retries the operation */ -function retryOperation( - error: Error, - onyxMethod: TMethod, - args: Parameters, - retryAttempt: number | undefined, -): Promise { +function retryOperation(error: Error, onyxMethod: TMethod, defaultParams: Parameters[0], retryAttempt: number | undefined): Promise { const currentRetryAttempt = retryAttempt ?? 0; const nextRetryAttempt = currentRetryAttempt + 1; @@ -907,7 +903,7 @@ function retryOperation MAX_RETRY_ATTEMPTS ? undefined : onyxMethod(...args, nextRetryAttempt); + return nextRetryAttempt > MAX_RETRY_ATTEMPTS ? Promise.resolve() : onyxMethod(defaultParams, nextRetryAttempt); } // Find the first key that we can remove that has no subscribers in our blocklist @@ -925,7 +921,7 @@ function retryOperation (nextRetryAttempt > MAX_RETRY_ATTEMPTS ? undefined : onyxMethod(...args, nextRetryAttempt))); + return remove(keyForRemoval).then(() => (nextRetryAttempt > MAX_RETRY_ATTEMPTS ? Promise.resolve() : onyxMethod(defaultParams, nextRetryAttempt))); } /** diff --git a/lib/types.ts b/lib/types.ts index f037ef7c..01dd0b14 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -373,6 +373,17 @@ type SetOptions = { skipCacheCheck?: boolean; }; +type SetParams = { + key: TKey; + value: OnyxSetInput; + options?: SetOptions; +}; + +type SetCollectionParams = { + collectionKey: TKey; + collection: OnyxMergeCollectionInput; +}; + /** * Represents the options used in `Onyx.init()` method. */ @@ -480,6 +491,8 @@ export type { OnyxValue, Selector, SetOptions, + SetParams, + SetCollectionParams, MultiMergeReplaceNullPatches, MixedOperationsQueue, }; From c146abf99fe5727b94ec0d7ba8035e7c2aef4b96 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 30 Oct 2025 12:53:59 +0100 Subject: [PATCH 08/23] Update mergeCollectionWithPatches --- lib/Onyx.ts | 26 ++++++++++++++------------ lib/OnyxUtils.ts | 18 +++++++++--------- lib/types.ts | 8 ++++++++ 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 58174cf7..dd298fbf 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -148,9 +148,10 @@ function disconnect(connection: Connection): void { /** * Write a value to our store with the given key, retry if the operation fails. * - * @param key ONYXKEY to set - * @param value value to store - * @param options optional configuration object + * @param params - set parameters + * @param params.key ONYXKEY to set + * @param params.value value to store + * @param params.options optional configuration object * @param retryAttempt retry attempt */ function setWithRetry({key, value, options}: SetParams, retryAttempt?: number): Promise { @@ -399,7 +400,7 @@ function merge(key: TKey, changes: OnyxMergeInput): * @param collection Object collection keyed by individual collection member keys and values */ function mergeCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { - return OnyxUtils.mergeCollectionWithPatches(collectionKey, collection, undefined, true); + return OnyxUtils.mergeCollectionWithPatches({collectionKey, collection, isProcessingCollectionUpdate: true}); } /** @@ -633,12 +634,12 @@ function update(data: OnyxUpdate[]): Promise { if (!utils.isEmptyObject(batchedCollectionUpdates.merge)) { promises.push(() => - OnyxUtils.mergeCollectionWithPatches( + OnyxUtils.mergeCollectionWithPatches({ collectionKey, - batchedCollectionUpdates.merge as Collection, - batchedCollectionUpdates.mergeReplaceNullPatches, - true, - ), + collection: batchedCollectionUpdates.merge as Collection, + mergeReplaceNullPatches: batchedCollectionUpdates.mergeReplaceNullPatches, + isProcessingCollectionUpdate: true, + }), ); } if (!utils.isEmptyObject(batchedCollectionUpdates.set)) { @@ -671,8 +672,9 @@ function update(data: OnyxUpdate[]): Promise { * Any existing collection members not included in the new data will be removed. * Retries in case of failures. * - * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` - * @param collection Object collection keyed by individual collection member keys and values + * @param params - collection parameters + * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` + * @param params.collection Object collection keyed by individual collection member keys and values * @param retryAttempt retry attempt */ function setCollectionWithRetry({collectionKey, collection}: SetCollectionParams, retryAttempt?: number): Promise { @@ -789,7 +791,7 @@ function applyDecorators() { /* eslint-enable rulesdir/prefer-actions-set-data */ } -type OnyxRetryOperation = typeof setWithRetry | typeof multiSetWithRetry | typeof setCollectionWithRetry; +type OnyxRetryOperation = typeof setWithRetry | typeof multiSetWithRetry | typeof setCollectionWithRetry | typeof OnyxUtils.mergeCollectionWithPatches; export default Onyx; export type {OnyxUpdate, ConnectOptions, SetOptions, OnyxRetryOperation}; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 010988fc..6577becf 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -30,6 +30,7 @@ import type { OnyxUpdate, OnyxValue, Selector, + MergeCollectionWithPatchesParams, } from './types'; import type {FastMergeOptions, FastMergeResult} from './utils'; import utils from './utils'; @@ -1259,19 +1260,18 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array< /** * Merges a collection based on their keys. * Serves as core implementation for `Onyx.mergeCollection()` public function, the difference being - * that this internal function allows passing an additional `mergeReplaceNullPatches` parameter. + * that this internal function allows passing an additional `mergeReplaceNullPatches` parameter and retries on failure. * - * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` - * @param collection Object collection keyed by individual collection member keys and values - * @param mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of + * @param params - mergeCollection parameters + * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` + * @param params.collection Object collection keyed by individual collection member keys and values + * @param params.mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of * tuples that we'll use to replace the nested objects of that collection member record with something else. + * @param params.isProcessingCollectionUpdate whether this is part of a collection update operation. * @param retryAttempt retry attempt */ function mergeCollectionWithPatches( - collectionKey: TKey, - collection: OnyxMergeCollectionInput, - mergeReplaceNullPatches?: MultiMergeReplaceNullPatches, - isProcessingCollectionUpdate?: boolean, + {collectionKey, collection, mergeReplaceNullPatches, isProcessingCollectionUpdate = false}: MergeCollectionWithPatchesParams, retryAttempt?: number, ): Promise { if (!isValidNonEmptyCollectionForMerge(collection)) { @@ -1377,7 +1377,7 @@ function mergeCollectionWithPatches( }); return Promise.all(promises) - .catch((error) => retryOperation(error, mergeCollectionWithPatches, [collectionKey, resultCollection, undefined, false], retryAttempt)) + .catch((error) => retryOperation(error, mergeCollectionWithPatches, {collectionKey, collection: resultCollection}, retryAttempt)) .then(() => { sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection); return promiseUpdate; diff --git a/lib/types.ts b/lib/types.ts index 01dd0b14..780d0f14 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -384,6 +384,13 @@ type SetCollectionParams = { collection: OnyxMergeCollectionInput; }; +type MergeCollectionWithPatchesParams = { + collectionKey: TKey; + collection: OnyxMergeCollectionInput; + mergeReplaceNullPatches?: MultiMergeReplaceNullPatches; + isProcessingCollectionUpdate?: boolean; +}; + /** * Represents the options used in `Onyx.init()` method. */ @@ -493,6 +500,7 @@ export type { SetOptions, SetParams, SetCollectionParams, + MergeCollectionWithPatchesParams, MultiMergeReplaceNullPatches, MixedOperationsQueue, }; From 5a6d6967b969a3828d3fea58d0915e0f612dc501 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 30 Oct 2025 13:05:49 +0100 Subject: [PATCH 09/23] Update partialSetCollection --- lib/Onyx.ts | 11 ++++++++--- lib/OnyxUtils.ts | 11 +++++++---- tests/perf-test/OnyxUtils.perf-test.ts | 8 ++++---- tests/unit/onyxUtilsTest.ts | 24 +++++++++++++++--------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index dd298fbf..acea4034 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -34,6 +34,13 @@ import * as GlobalSettings from './GlobalSettings'; import decorateWithMetrics from './metrics'; import OnyxMerge from './OnyxMerge'; +type OnyxRetryOperation = + | typeof setWithRetry + | typeof multiSetWithRetry + | typeof setCollectionWithRetry + | typeof OnyxUtils.mergeCollectionWithPatches + | typeof OnyxUtils.partialSetCollection; + /** Initialize the store with actions and listening for storage events */ function init({ keys = {}, @@ -643,7 +650,7 @@ function update(data: OnyxUpdate[]): Promise { ); } if (!utils.isEmptyObject(batchedCollectionUpdates.set)) { - promises.push(() => OnyxUtils.partialSetCollection(collectionKey, batchedCollectionUpdates.set as Collection)); + promises.push(() => OnyxUtils.partialSetCollection({collectionKey, collection: batchedCollectionUpdates.set as Collection})); } }); @@ -791,7 +798,5 @@ function applyDecorators() { /* eslint-enable rulesdir/prefer-actions-set-data */ } -type OnyxRetryOperation = typeof setWithRetry | typeof multiSetWithRetry | typeof setCollectionWithRetry | typeof OnyxUtils.mergeCollectionWithPatches; - export default Onyx; export type {OnyxUpdate, ConnectOptions, SetOptions, OnyxRetryOperation}; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 6577becf..314b86c5 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -31,6 +31,7 @@ import type { OnyxValue, Selector, MergeCollectionWithPatchesParams, + SetCollectionParams, } from './types'; import type {FastMergeOptions, FastMergeResult} from './utils'; import utils from './utils'; @@ -1389,12 +1390,14 @@ function mergeCollectionWithPatches( /** * Sets keys in a collection by replacing all targeted collection members with new values. * Any existing collection members not included in the new data will not be removed. + * Retries on failure. * - * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` - * @param collection Object collection keyed by individual collection member keys and values + * @param params - collection parameters + * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` + * @param params.collection Object collection keyed by individual collection member keys and values * @param retryAttempt retry attempt */ -function partialSetCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput, retryAttempt?: number): Promise { +function partialSetCollection({collectionKey, collection}: SetCollectionParams, retryAttempt?: number): Promise { let resultCollection: OnyxInputKeyValueMapping = collection; let resultCollectionKeys = Object.keys(resultCollection); @@ -1433,7 +1436,7 @@ function partialSetCollection(collectionKe const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); return Storage.multiSet(keyValuePairs) - .catch((error) => retryOperation(error, partialSetCollection, [collectionKey, collection], retryAttempt)) + .catch((error) => retryOperation(error, partialSetCollection, {collectionKey, collection}, retryAttempt)) .then(() => { sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection); return updatePromise; diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts index 172098e8..f46e9fe4 100644 --- a/tests/perf-test/OnyxUtils.perf-test.ts +++ b/tests/perf-test/OnyxUtils.perf-test.ts @@ -7,7 +7,7 @@ import StorageMock from '../../lib/storage'; import OnyxCache from '../../lib/OnyxCache'; import OnyxUtils, {clearOnyxUtilsInternals} from '../../lib/OnyxUtils'; import type GenericCollection from '../utils/GenericCollection'; -import type {OnyxUpdate} from '../../lib/Onyx'; +import type {OnyxRetryOperation, OnyxUpdate} from '../../lib/Onyx'; import createDeferredTask from '../../lib/createDeferredTask'; import type {OnyxInputKeyValueMapping} from '../../lib/types'; @@ -281,7 +281,7 @@ describe('OnyxUtils', () => { const changedReportActions = Object.fromEntries( Object.entries(mockedReportActionsMap).map(([k, v]) => [k, randBoolean() ? v : createRandomReportAction(Number(v.reportActionID))] as const), ) as GenericCollection; - await measureAsyncFunction(() => OnyxUtils.partialSetCollection(collectionKey, changedReportActions), { + await measureAsyncFunction(() => OnyxUtils.partialSetCollection({collectionKey, collection: changedReportActions}), { beforeEach: async () => { await Onyx.setCollection(collectionKey, mockedReportActionsMap as GenericCollection); }, @@ -505,9 +505,9 @@ describe('OnyxUtils', () => { describe('retryOperation', () => { test('one call', async () => { const error = new Error(); - const onyxMethod = jest.fn() as typeof Onyx.set; + const onyxMethod = jest.fn() as OnyxRetryOperation; - await measureAsyncFunction(() => OnyxUtils.retryOperation(error, onyxMethod, ['', null], 1), { + await measureAsyncFunction(() => OnyxUtils.retryOperation(error, onyxMethod, {key: '', value: null}, 1), { beforeEach: async () => { mockedReportActionsKeys.forEach((key) => OnyxCache.addLastAccessedKey(key, false)); OnyxCache.addLastAccessedKey(`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}1`, false); diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 9b120a58..0c3cebcd 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -155,11 +155,14 @@ describe('OnyxUtils', () => { } as GenericCollection); // Replace with new collection data - await OnyxUtils.partialSetCollection(ONYXKEYS.COLLECTION.ROUTES, { - [routeA]: {name: 'New Route A'}, - [routeB]: {name: 'New Route B'}, - [routeC]: {name: 'New Route C'}, - } as GenericCollection); + await OnyxUtils.partialSetCollection({ + collectionKey: ONYXKEYS.COLLECTION.ROUTES, + collection: { + [routeA]: {name: 'New Route A'}, + [routeB]: {name: 'New Route B'}, + [routeC]: {name: 'New Route C'}, + } as GenericCollection, + }); expect(result).toEqual({ [routeA]: {name: 'New Route A'}, @@ -185,7 +188,7 @@ describe('OnyxUtils', () => { [routeA]: {name: 'Route A'}, } as GenericCollection); - await OnyxUtils.partialSetCollection(ONYXKEYS.COLLECTION.ROUTES, {} as GenericCollection); + await OnyxUtils.partialSetCollection({collectionKey: ONYXKEYS.COLLECTION.ROUTES, collection: {} as GenericCollection}); expect(result).toEqual({ [routeA]: {name: 'Route A'}, @@ -209,9 +212,12 @@ describe('OnyxUtils', () => { [routeA]: {name: 'Route A'}, } as GenericCollection); - await OnyxUtils.partialSetCollection(ONYXKEYS.COLLECTION.ROUTES, { - [invalidRoute]: {name: 'Invalid Route'}, - } as GenericCollection); + await OnyxUtils.partialSetCollection({ + collectionKey: ONYXKEYS.COLLECTION.ROUTES, + collection: { + [invalidRoute]: {name: 'Invalid Route'}, + } as GenericCollection, + }); expect(result).toEqual({ [routeA]: {name: 'Route A'}, From 23e996e982dfc05db8bb36968c75742dbee0b0be Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 30 Oct 2025 13:09:47 +0100 Subject: [PATCH 10/23] Rename const --- lib/OnyxUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 314b86c5..1ca7f575 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -58,13 +58,13 @@ const IDB_STORAGE_ERRORS = [ ] as const; // SQLite errors that indicate storage capacity issues where eviction can help -const SQL_STORAGE_ERRORS = [ +const SQLITE_STORAGE_ERRORS = [ 'database or disk is full', // Device storage is full 'disk I/O error', // File system I/O failure, often due to insufficient space or corrupted storage 'out of memory', // Insufficient RAM or storage space to complete the operation ] as const; -const STORAGE_ERRORS = [...IDB_STORAGE_ERRORS, ...SQL_STORAGE_ERRORS]; +const STORAGE_ERRORS = [...IDB_STORAGE_ERRORS, ...SQLITE_STORAGE_ERRORS]; const MAX_RETRY_ATTEMPTS = 5; From 1bd718a6c2690479248ea220adf8691fa5413a7c Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 30 Oct 2025 13:18:59 +0100 Subject: [PATCH 11/23] Minor clean-up --- lib/OnyxUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 1ca7f575..35b649af 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1311,7 +1311,7 @@ function mergeCollectionWithPatches( // Split to keys that exist in storage and keys that don't const keys = resultCollectionKeys.filter((key) => { if (resultCollection[key] === null) { - remove(key, !!isProcessingCollectionUpdate); + remove(key, isProcessingCollectionUpdate); return false; } return true; From b57b22a07a76021e727a96cac07860a4d474bdb9 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 30 Oct 2025 15:26:25 +0100 Subject: [PATCH 12/23] Rename const --- lib/OnyxUtils.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 35b649af..0b1eb76f 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -66,7 +66,8 @@ const SQLITE_STORAGE_ERRORS = [ const STORAGE_ERRORS = [...IDB_STORAGE_ERRORS, ...SQLITE_STORAGE_ERRORS]; -const MAX_RETRY_ATTEMPTS = 5; +// Max number of retries for failed storage operations +const MAX_STORAGE_OPERATION_RETRY_ATTEMPTS = 5; type OnyxMethod = ValueOf; @@ -893,7 +894,7 @@ function retryOperation(error: Error, onyxMe const currentRetryAttempt = retryAttempt ?? 0; const nextRetryAttempt = currentRetryAttempt + 1; - Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}. retryAttempt: ${currentRetryAttempt}/${MAX_RETRY_ATTEMPTS}`); + Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}. retryAttempt: ${currentRetryAttempt}/${MAX_STORAGE_OPERATION_RETRY_ATTEMPTS}`); if (error && Str.startsWith(error.message, "Failed to execute 'put' on 'IDBObjectStore'")) { Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.'); @@ -901,11 +902,11 @@ function retryOperation(error: Error, onyxMe } const errorMessage = error?.message?.toLowerCase?.(); - const isStorageCapacityError = STORAGE_ERRORS.some((storageError) => storageError === error?.name?.toLowerCase() || errorMessage?.includes(storageError)); + const isStorageCapacityError = STORAGE_ERRORS.some((storageError) => storageError === error?.name?.toLowerCase?.() || errorMessage?.includes(storageError)); if (!isStorageCapacityError) { // @ts-expect-error No overload matches this call. - return nextRetryAttempt > MAX_RETRY_ATTEMPTS ? Promise.resolve() : onyxMethod(defaultParams, nextRetryAttempt); + return nextRetryAttempt > MAX_STORAGE_OPERATION_RETRY_ATTEMPTS ? Promise.resolve() : onyxMethod(defaultParams, nextRetryAttempt); } // Find the first key that we can remove that has no subscribers in our blocklist @@ -923,7 +924,7 @@ function retryOperation(error: Error, onyxMe reportStorageQuota(); // @ts-expect-error No overload matches this call. - return remove(keyForRemoval).then(() => (nextRetryAttempt > MAX_RETRY_ATTEMPTS ? Promise.resolve() : onyxMethod(defaultParams, nextRetryAttempt))); + return remove(keyForRemoval).then(() => (nextRetryAttempt > MAX_STORAGE_OPERATION_RETRY_ATTEMPTS ? Promise.resolve() : onyxMethod(defaultParams, nextRetryAttempt))); } /** From 5e0c4ad28a5d0506bd849d193f46df7ccaa2172d Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 30 Oct 2025 15:48:07 +0100 Subject: [PATCH 13/23] Move setWithRetry, multiSetWithRetry, setCollectionWithRetry to OnyxUtils --- lib/Onyx.ts | 213 +------------------------ lib/OnyxUtils.ts | 211 +++++++++++++++++++++++- lib/types.ts | 8 + tests/perf-test/OnyxUtils.perf-test.ts | 4 +- 4 files changed, 224 insertions(+), 212 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index acea4034..a16b323a 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -23,8 +23,6 @@ import type { OnyxInput, OnyxMethodMap, SetOptions, - SetParams, - SetCollectionParams, } from './types'; import OnyxUtils from './OnyxUtils'; import logMessages from './logMessages'; @@ -34,13 +32,6 @@ import * as GlobalSettings from './GlobalSettings'; import decorateWithMetrics from './metrics'; import OnyxMerge from './OnyxMerge'; -type OnyxRetryOperation = - | typeof setWithRetry - | typeof multiSetWithRetry - | typeof setCollectionWithRetry - | typeof OnyxUtils.mergeCollectionWithPatches - | typeof OnyxUtils.partialSetCollection; - /** Initialize the store with actions and listening for storage events */ function init({ keys = {}, @@ -152,83 +143,6 @@ function disconnect(connection: Connection): void { connectionManager.disconnect(connection); } -/** - * Write a value to our store with the given key, retry if the operation fails. - * - * @param params - set parameters - * @param params.key ONYXKEY to set - * @param params.value value to store - * @param params.options optional configuration object - * @param retryAttempt retry attempt - */ -function setWithRetry({key, value, options}: SetParams, retryAttempt?: number): Promise { - // When we use Onyx.set to set a key we want to clear the current delta changes from Onyx.merge that were queued - // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes. - if (OnyxUtils.hasPendingMergeForKey(key)) { - delete OnyxUtils.getMergeQueue()[key]; - } - - const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); - if (skippableCollectionMemberIDs.size) { - try { - const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key); - if (skippableCollectionMemberIDs.has(collectionMemberID)) { - // The key is a skippable one, so we set the new value to null. - // eslint-disable-next-line no-param-reassign - value = null; - } - } catch (e) { - // The key is not a collection one or something went wrong during split, so we proceed with the function's logic. - } - } - - // Onyx.set will ignore `undefined` values as inputs, therefore we can return early. - if (value === undefined) { - return Promise.resolve(); - } - - const existingValue = cache.get(key, false); - // If the existing value as well as the new value are null, we can return early. - if (existingValue === undefined && value === null) { - return Promise.resolve(); - } - - // Check if the value is compatible with the existing value in the storage - const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(value, existingValue); - if (!isCompatible) { - Logger.logAlert(logMessages.incompatibleUpdateAlert(key, 'set', existingValueType, newValueType)); - return Promise.resolve(); - } - - // If the change is null, we can just delete the key. - // Therefore, we don't need to further broadcast and update the value so we can return early. - if (value === null) { - OnyxUtils.remove(key); - OnyxUtils.logKeyRemoved(OnyxUtils.METHOD.SET, key); - return Promise.resolve(); - } - - const valueWithoutNestedNullValues = utils.removeNestedNullValues(value) as OnyxValue; - const hasChanged = options?.skipCacheCheck ? true : cache.hasValueChanged(key, valueWithoutNestedNullValues); - - OnyxUtils.logKeyChanged(OnyxUtils.METHOD.SET, key, value, hasChanged); - - // This approach prioritizes fast UI changes without waiting for data to be stored in device storage. - const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged); - - // If the value has not changed or the key got removed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead. - if (!hasChanged) { - return updatePromise; - } - - return Storage.setItem(key, valueWithoutNestedNullValues) - .catch((error) => OnyxUtils.retryOperation(error, setWithRetry, {key, value: valueWithoutNestedNullValues}, retryAttempt)) - .then(() => { - OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues); - return updatePromise; - }); -} - /** * Write a value to our store with the given key * @@ -237,57 +151,7 @@ function setWithRetry({key, value, options}: SetParams(key: TKey, value: OnyxSetInput, options?: SetOptions): Promise { - return setWithRetry({key, value, options}); -} - -/** - * Sets multiple keys and values, retries if the operation fails - * - * @param data object keyed by ONYXKEYS and the values to set - * @param retryAttempt retry attempt - */ -function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Promise { - let newData = data; - - const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); - if (skippableCollectionMemberIDs.size) { - newData = Object.keys(newData).reduce((result: OnyxMultiSetInput, key) => { - try { - const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key); - // If the collection member key is a skippable one we set its value to null. - // eslint-disable-next-line no-param-reassign - result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? newData[key] : null; - } catch { - // The key is not a collection one or something went wrong during split, so we assign the data to result anyway. - // eslint-disable-next-line no-param-reassign - result[key] = newData[key]; - } - - return result; - }, {}); - } - - const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(newData, true); - - const updatePromises = keyValuePairsToSet.map(([key, value]) => { - // When we use multiSet to set a key we want to clear the current delta changes from Onyx.merge that were queued - // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes. - if (OnyxUtils.hasPendingMergeForKey(key)) { - delete OnyxUtils.getMergeQueue()[key]; - } - - // Update cache and optimistically inform subscribers on the next tick - cache.set(key, value); - return OnyxUtils.scheduleSubscriberUpdate(key, value); - }); - - return Storage.multiSet(keyValuePairsToSet) - .catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt)) - .then(() => { - OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData); - return Promise.all(updatePromises); - }) - .then(() => undefined); + return OnyxUtils.setWithRetry({key, value, options}); } /** @@ -298,7 +162,7 @@ function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Prom * @param data object keyed by ONYXKEYS and the values to set */ function multiSet(data: OnyxMultiSetInput): Promise { - return multiSetWithRetry(data); + return OnyxUtils.multiSetWithRetry(data); } /** @@ -674,75 +538,6 @@ function update(data: OnyxUpdate[]): Promise { return clearPromise.then(() => Promise.all(finalPromises.map((p) => p()))).then(() => undefined); } -/** - * Sets a collection by replacing all existing collection members with new values. - * Any existing collection members not included in the new data will be removed. - * Retries in case of failures. - * - * @param params - collection parameters - * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` - * @param params.collection Object collection keyed by individual collection member keys and values - * @param retryAttempt retry attempt - */ -function setCollectionWithRetry({collectionKey, collection}: SetCollectionParams, retryAttempt?: number): Promise { - let resultCollection: OnyxInputKeyValueMapping = collection; - let resultCollectionKeys = Object.keys(resultCollection); - - // Confirm all the collection keys belong to the same parent - if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) { - Logger.logAlert(`setCollection called with keys that do not belong to the same parent ${collectionKey}. Skipping this update.`); - return Promise.resolve(); - } - - const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); - if (skippableCollectionMemberIDs.size) { - resultCollection = resultCollectionKeys.reduce((result: OnyxInputKeyValueMapping, key) => { - try { - const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key, collectionKey); - // If the collection member key is a skippable one we set its value to null. - // eslint-disable-next-line no-param-reassign - result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null; - } catch { - // Something went wrong during split, so we assign the data to result anyway. - // eslint-disable-next-line no-param-reassign - result[key] = resultCollection[key]; - } - - return result; - }, {}); - } - resultCollectionKeys = Object.keys(resultCollection); - - return OnyxUtils.getAllKeys().then((persistedKeys) => { - const mutableCollection: OnyxInputKeyValueMapping = {...resultCollection}; - - persistedKeys.forEach((key) => { - if (!key.startsWith(collectionKey)) { - return; - } - if (resultCollectionKeys.includes(key)) { - return; - } - - mutableCollection[key] = null; - }); - - const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true); - const previousCollection = OnyxUtils.getCachedCollection(collectionKey); - - keyValuePairs.forEach(([key, value]) => cache.set(key, value)); - - const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); - - return Storage.multiSet(keyValuePairs) - .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, {collectionKey, collection}, retryAttempt)) - .then(() => { - OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); - return updatePromise; - }); - }); -} - /** * Sets a collection by replacing all existing collection members with new values. * Any existing collection members not included in the new data will be removed. @@ -757,7 +552,7 @@ function setCollectionWithRetry({collectio * @param collection Object collection keyed by individual collection member keys and values */ function setCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { - return setCollectionWithRetry({collectionKey, collection}); + return OnyxUtils.setCollectionWithRetry({collectionKey, collection}); } const Onyx = { @@ -799,4 +594,4 @@ function applyDecorators() { } export default Onyx; -export type {OnyxUpdate, ConnectOptions, SetOptions, OnyxRetryOperation}; +export type {OnyxUpdate, ConnectOptions, SetOptions}; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 0b1eb76f..3209ab6d 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -6,7 +6,6 @@ import _ from 'underscore'; import DevTools from './DevTools'; import * as Logger from './Logger'; import type Onyx from './Onyx'; -import type {OnyxRetryOperation} from './Onyx'; import cache, {TASK} from './OnyxCache'; import * as Str from './Str'; import unstable_batchedUpdates from './batch'; @@ -32,6 +31,9 @@ import type { Selector, MergeCollectionWithPatchesParams, SetCollectionParams, + SetParams, + OnyxMultiSetInput, + OnyxRetryOperation, } from './types'; import type {FastMergeOptions, FastMergeResult} from './utils'; import utils from './utils'; @@ -1259,6 +1261,204 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array< return promises; } +/** + * Writes a value to our store with the given key. + * Serves as core implementation for `Onyx.set()` public function, the difference being + * that this internal function allows passing an additional `retryAttempt` parameter to retry on failure. + * + * @param params - set parameters + * @param params.key ONYXKEY to set + * @param params.value value to store + * @param params.options optional configuration object + * @param retryAttempt retry attempt + */ +function setWithRetry({key, value, options}: SetParams, retryAttempt?: number): Promise { + // When we use Onyx.set to set a key we want to clear the current delta changes from Onyx.merge that were queued + // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes. + if (OnyxUtils.hasPendingMergeForKey(key)) { + delete OnyxUtils.getMergeQueue()[key]; + } + + if (skippableCollectionMemberIDs.size) { + try { + const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key); + if (skippableCollectionMemberIDs.has(collectionMemberID)) { + // The key is a skippable one, so we set the new value to null. + // eslint-disable-next-line no-param-reassign + value = null; + } + } catch (e) { + // The key is not a collection one or something went wrong during split, so we proceed with the function's logic. + } + } + + // Onyx.set will ignore `undefined` values as inputs, therefore we can return early. + if (value === undefined) { + return Promise.resolve(); + } + + const existingValue = cache.get(key, false); + // If the existing value as well as the new value are null, we can return early. + if (existingValue === undefined && value === null) { + return Promise.resolve(); + } + + // Check if the value is compatible with the existing value in the storage + const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(value, existingValue); + if (!isCompatible) { + Logger.logAlert(logMessages.incompatibleUpdateAlert(key, 'set', existingValueType, newValueType)); + return Promise.resolve(); + } + + // If the change is null, we can just delete the key. + // Therefore, we don't need to further broadcast and update the value so we can return early. + if (value === null) { + OnyxUtils.remove(key); + OnyxUtils.logKeyRemoved(OnyxUtils.METHOD.SET, key); + return Promise.resolve(); + } + + const valueWithoutNestedNullValues = utils.removeNestedNullValues(value) as OnyxValue; + const hasChanged = options?.skipCacheCheck ? true : cache.hasValueChanged(key, valueWithoutNestedNullValues); + + OnyxUtils.logKeyChanged(OnyxUtils.METHOD.SET, key, value, hasChanged); + + // This approach prioritizes fast UI changes without waiting for data to be stored in device storage. + const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged); + + // If the value has not changed or the key got removed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead. + if (!hasChanged && !retryAttempt) { + return updatePromise; + } + + return Storage.setItem(key, valueWithoutNestedNullValues) + .catch((error) => OnyxUtils.retryOperation(error, setWithRetry, {key, value: valueWithoutNestedNullValues}, retryAttempt)) + .then(() => { + OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues); + return updatePromise; + }); +} + +/** + * Sets multiple keys and values. + * Serves as core implementation for `Onyx.multiSet()` public function, the difference being + * that this internal function allows passing an additional `retryAttempt` parameter to retry on failure. + * + * @param data object keyed by ONYXKEYS and the values to set + * @param retryAttempt retry attempt + */ +function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Promise { + let newData = data; + + if (skippableCollectionMemberIDs.size) { + newData = Object.keys(newData).reduce((result: OnyxMultiSetInput, key) => { + try { + const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key); + // If the collection member key is a skippable one we set its value to null. + // eslint-disable-next-line no-param-reassign + result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? newData[key] : null; + } catch { + // The key is not a collection one or something went wrong during split, so we assign the data to result anyway. + // eslint-disable-next-line no-param-reassign + result[key] = newData[key]; + } + + return result; + }, {}); + } + + const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(newData, true); + + const updatePromises = keyValuePairsToSet.map(([key, value]) => { + // When we use multiSet to set a key we want to clear the current delta changes from Onyx.merge that were queued + // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes. + if (OnyxUtils.hasPendingMergeForKey(key)) { + delete OnyxUtils.getMergeQueue()[key]; + } + + // Update cache and optimistically inform subscribers on the next tick + cache.set(key, value); + return OnyxUtils.scheduleSubscriberUpdate(key, value); + }); + + return Storage.multiSet(keyValuePairsToSet) + .catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt)) + .then(() => { + OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData); + return Promise.all(updatePromises); + }) + .then(() => undefined); +} + +/** + * Sets a collection by replacing all existing collection members with new values. + * Any existing collection members not included in the new data will be removed. + * Serves as core implementation for `Onyx.setCollection()` public function, the difference being + * that this internal function allows passing an additional `retryAttempt` parameter to retry on failure. + * + * @param params - collection parameters + * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` + * @param params.collection Object collection keyed by individual collection member keys and values + * @param retryAttempt retry attempt + */ +function setCollectionWithRetry({collectionKey, collection}: SetCollectionParams, retryAttempt?: number): Promise { + let resultCollection: OnyxInputKeyValueMapping = collection; + let resultCollectionKeys = Object.keys(resultCollection); + + // Confirm all the collection keys belong to the same parent + if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) { + Logger.logAlert(`setCollection called with keys that do not belong to the same parent ${collectionKey}. Skipping this update.`); + return Promise.resolve(); + } + + if (skippableCollectionMemberIDs.size) { + resultCollection = resultCollectionKeys.reduce((result: OnyxInputKeyValueMapping, key) => { + try { + const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key, collectionKey); + // If the collection member key is a skippable one we set its value to null. + // eslint-disable-next-line no-param-reassign + result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null; + } catch { + // Something went wrong during split, so we assign the data to result anyway. + // eslint-disable-next-line no-param-reassign + result[key] = resultCollection[key]; + } + + return result; + }, {}); + } + resultCollectionKeys = Object.keys(resultCollection); + + return OnyxUtils.getAllKeys().then((persistedKeys) => { + const mutableCollection: OnyxInputKeyValueMapping = {...resultCollection}; + + persistedKeys.forEach((key) => { + if (!key.startsWith(collectionKey)) { + return; + } + if (resultCollectionKeys.includes(key)) { + return; + } + + mutableCollection[key] = null; + }); + + const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true); + const previousCollection = OnyxUtils.getCachedCollection(collectionKey); + + keyValuePairs.forEach(([key, value]) => cache.set(key, value)); + + const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); + + return Storage.multiSet(keyValuePairs) + .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, {collectionKey, collection}, retryAttempt)) + .then(() => { + OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); + return updatePromise; + }); + }); +} + /** * Merges a collection based on their keys. * Serves as core implementation for `Onyx.mergeCollection()` public function, the difference being @@ -1518,6 +1718,9 @@ const OnyxUtils = { partialSetCollection, logKeyChanged, logKeyRemoved, + setWithRetry, + multiSetWithRetry, + setCollectionWithRetry, }; GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { @@ -1564,6 +1767,12 @@ GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { tupleGet = decorateWithMetrics(tupleGet, 'OnyxUtils.tupleGet'); // @ts-expect-error Reassign subscribeToKey = decorateWithMetrics(subscribeToKey, 'OnyxUtils.subscribeToKey'); + // @ts-expect-error Reassign + setWithRetry = decorateWithMetrics(setWithRetry, 'OnyxUtils.setWithRetry'); + // @ts-expect-error Reassign + multiSetWithRetry = decorateWithMetrics(multiSetWithRetry, 'OnyxUtils.multiSetWithRetry'); + // @ts-expect-error Reassign + setCollectionWithRetry = decorateWithMetrics(setCollectionWithRetry, 'OnyxUtils.setCollectionWithRetry'); }); export type {OnyxMethod}; diff --git a/lib/types.ts b/lib/types.ts index 780d0f14..328b6083 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -391,6 +391,13 @@ type MergeCollectionWithPatchesParams = { isProcessingCollectionUpdate?: boolean; }; +type OnyxRetryOperation = + | typeof OnyxUtils.setWithRetry + | typeof OnyxUtils.multiSetWithRetry + | typeof OnyxUtils.setCollectionWithRetry + | typeof OnyxUtils.mergeCollectionWithPatches + | typeof OnyxUtils.partialSetCollection; + /** * Represents the options used in `Onyx.init()` method. */ @@ -503,4 +510,5 @@ export type { MergeCollectionWithPatchesParams, MultiMergeReplaceNullPatches, MixedOperationsQueue, + OnyxRetryOperation, }; diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts index f46e9fe4..a300e917 100644 --- a/tests/perf-test/OnyxUtils.perf-test.ts +++ b/tests/perf-test/OnyxUtils.perf-test.ts @@ -7,9 +7,9 @@ import StorageMock from '../../lib/storage'; import OnyxCache from '../../lib/OnyxCache'; import OnyxUtils, {clearOnyxUtilsInternals} from '../../lib/OnyxUtils'; import type GenericCollection from '../utils/GenericCollection'; -import type {OnyxRetryOperation, OnyxUpdate} from '../../lib/Onyx'; +import type {OnyxUpdate} from '../../lib/Onyx'; import createDeferredTask from '../../lib/createDeferredTask'; -import type {OnyxInputKeyValueMapping} from '../../lib/types'; +import type {OnyxInputKeyValueMapping, OnyxRetryOperation} from '../../lib/types'; const ONYXKEYS = { TEST_KEY: 'test', From 7f0bf74f7f6d4fddb4b2eb76fe739b685ffd9035 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 31 Oct 2025 09:15:48 +0100 Subject: [PATCH 14/23] Minor code improvements --- lib/OnyxUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 3209ab6d..7a76a28b 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -904,7 +904,8 @@ function retryOperation(error: Error, onyxMe } const errorMessage = error?.message?.toLowerCase?.(); - const isStorageCapacityError = STORAGE_ERRORS.some((storageError) => storageError === error?.name?.toLowerCase?.() || errorMessage?.includes(storageError)); + const errorName = error?.name?.toLowerCase?.(); + const isStorageCapacityError = STORAGE_ERRORS.some((storageError) => errorName?.includes(storageError) || errorMessage?.includes(storageError)); if (!isStorageCapacityError) { // @ts-expect-error No overload matches this call. @@ -1326,7 +1327,7 @@ function setWithRetry({key, value, options}: SetParams Date: Fri, 31 Oct 2025 11:47:31 +0100 Subject: [PATCH 15/23] Add unit tests --- tests/unit/onyxUtilsTest.ts | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 0c3cebcd..5fafc2c5 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -4,6 +4,7 @@ import type {GenericDeepRecord} from '../types'; import utils from '../../lib/utils'; import type {Collection, OnyxCollection} from '../../lib/types'; import type GenericCollection from '../utils/GenericCollection'; +import StorageMock from '../../lib/storage'; const testObject: GenericDeepRecord = { a: 'a', @@ -82,6 +83,8 @@ Onyx.init({ beforeEach(() => Onyx.clear()); +afterEach(() => jest.restoreAllMocks()); + describe('OnyxUtils', () => { describe('splitCollectionMemberKey', () => { describe('should return correct values', () => { @@ -415,4 +418,49 @@ describe('OnyxUtils', () => { ]); }); }); + + describe('retryOperation', () => { + it('should retry only one time if the operation is firstly failed and then passed', async () => { + const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); + const genericError = new Error('Generic storage error'); + + StorageMock.setItem = jest.fn(StorageMock.setItem).mockRejectedValueOnce(genericError).mockImplementation(StorageMock.setItem); + + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + + // Should be called once, since Storage.setItem if failed only once + expect(retryOperationSpy).toHaveBeenCalledTimes(1); + }); + + it('should stop retrying after MAX_STORAGE_OPERATION_RETRY_ATTEMPTS retries for failing operation', async () => { + const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); + const genericError = new Error('Generic storage error'); + + StorageMock.setItem = jest.fn().mockRejectedValue(genericError); + + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + + // Should be called 6 times: initial attempt + 5 retries (MAX_STORAGE_OPERATION_RETRY_ATTEMPTS) + expect(retryOperationSpy).toHaveBeenCalledTimes(6); + }); + + it("should throw error for if operation failed with \"Failed to execute 'put' on 'IDBObjectStore': invalid data\" error", async () => { + const idbError = new Error("Failed to execute 'put' on 'IDBObjectStore': invalid data"); + StorageMock.setItem = jest.fn().mockRejectedValueOnce(idbError); + + await expect(Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'})).rejects.toThrow(idbError); + }); + + it('should not retry in case of storage capacity error and no keys to evict', async () => { + const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); + const quotaError = new Error('out of memory'); + + StorageMock.setItem = jest.fn().mockRejectedValue(quotaError); + + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + + // Should only be called once since there are no evictable keys + expect(retryOperationSpy).toHaveBeenCalledTimes(1); + }); + }); }); From c55cc64fa029ed31175e4488614e445044f94db9 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 31 Oct 2025 16:16:33 +0100 Subject: [PATCH 16/23] Update the docs --- API-INTERNAL.md | 107 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 17 deletions(-) diff --git a/API-INTERNAL.md b/API-INTERNAL.md index f2610b37..38a5d76c 100644 --- a/API-INTERNAL.md +++ b/API-INTERNAL.md @@ -110,10 +110,12 @@ subscriber callbacks receive the data in a different format than they normally e

Remove a key from Onyx and update the subscribers

retryOperation()
-

Handles storage operation failures based on the error type: -- Storage capacity errors: evicts data and retries the operation -- Invalid data errors: logs an alert and throws an error -- Other errors: retries the operation

+

Handles storage operation failures based on the error type:

+
broadcastUpdate()

Notifies subscribers and writes current value to cache

@@ -148,14 +150,31 @@ It will also mark deep nested objects that need to be entirely replaced during t
unsubscribeFromKey(subscriptionID)

Disconnects and removes the listener from the Onyx key.

-
mergeCollectionWithPatches(collectionKey, collection, mergeReplaceNullPatches)
+
setWithRetry(params, retryAttempt)
+

Writes a value to our store with the given key. +Serves as core implementation for Onyx.set() public function, the difference being +that this internal function allows passing an additional retryAttempt parameter to retry on failure.

+
+
multiSetWithRetry(data, retryAttempt)
+

Sets multiple keys and values. +Serves as core implementation for Onyx.multiSet() public function, the difference being +that this internal function allows passing an additional retryAttempt parameter to retry on failure.

+
+
setCollectionWithRetry(params, retryAttempt)
+

Sets a collection by replacing all existing collection members with new values. +Any existing collection members not included in the new data will be removed. +Serves as core implementation for Onyx.setCollection() public function, the difference being +that this internal function allows passing an additional retryAttempt parameter to retry on failure.

+
+
mergeCollectionWithPatches(params, retryAttempt)

Merges a collection based on their keys. Serves as core implementation for Onyx.mergeCollection() public function, the difference being -that this internal function allows passing an additional mergeReplaceNullPatches parameter.

+that this internal function allows passing an additional mergeReplaceNullPatches parameter and retries on failure.

-
partialSetCollection(collectionKey, collection)
+
partialSetCollection(params, retryAttempt)

Sets keys in a collection by replacing all targeted collection members with new values. -Any existing collection members not included in the new data will not be removed.

+Any existing collection members not included in the new data will not be removed. +Retries on failure.

clearOnyxUtilsInternals()

Clear internal variables used in this file, useful in test environments.

@@ -406,7 +425,7 @@ subscriber callbacks receive the data in a different format than they normally e ## remove() Remove a key from Onyx and update the subscribers -**Kind**: global function +**Kind**: global function ## retryOperation() @@ -509,33 +528,87 @@ Disconnects and removes the listener from the Onyx key. | --- | --- | | subscriptionID | Subscription ID returned by calling `OnyxUtils.subscribeToKey()`. | + + +## setWithRetry(params, retryAttempt) +Writes a value to our store with the given key. +Serves as core implementation for `Onyx.set()` public function, the difference being +that this internal function allows passing an additional `retryAttempt` parameter to retry on failure. + +**Kind**: global function + +| Param | Description | +| --- | --- | +| params | set parameters | +| params.key | ONYXKEY to set | +| params.value | value to store | +| params.options | optional configuration object | +| retryAttempt | retry attempt | + + + +## multiSetWithRetry(data, retryAttempt) +Sets multiple keys and values. +Serves as core implementation for `Onyx.multiSet()` public function, the difference being +that this internal function allows passing an additional `retryAttempt` parameter to retry on failure. + +**Kind**: global function + +| Param | Description | +| --- | --- | +| data | object keyed by ONYXKEYS and the values to set | +| retryAttempt | retry attempt | + + + +## setCollectionWithRetry(params, retryAttempt) +Sets a collection by replacing all existing collection members with new values. +Any existing collection members not included in the new data will be removed. +Serves as core implementation for `Onyx.setCollection()` public function, the difference being +that this internal function allows passing an additional `retryAttempt` parameter to retry on failure. + +**Kind**: global function + +| Param | Description | +| --- | --- | +| params | collection parameters | +| params.collectionKey | e.g. `ONYXKEYS.COLLECTION.REPORT` | +| params.collection | Object collection keyed by individual collection member keys and values | +| retryAttempt | retry attempt | + -## mergeCollectionWithPatches(collectionKey, collection, mergeReplaceNullPatches) +## mergeCollectionWithPatches(params, retryAttempt) Merges a collection based on their keys. Serves as core implementation for `Onyx.mergeCollection()` public function, the difference being -that this internal function allows passing an additional `mergeReplaceNullPatches` parameter. +that this internal function allows passing an additional `mergeReplaceNullPatches` parameter and retries on failure. **Kind**: global function | Param | Description | | --- | --- | -| collectionKey | e.g. `ONYXKEYS.COLLECTION.REPORT` | -| collection | Object collection keyed by individual collection member keys and values | -| mergeReplaceNullPatches | Record where the key is a collection member key and the value is a list of tuples that we'll use to replace the nested objects of that collection member record with something else. | +| params | mergeCollection parameters | +| params.collectionKey | e.g. `ONYXKEYS.COLLECTION.REPORT` | +| params.collection | Object collection keyed by individual collection member keys and values | +| params.mergeReplaceNullPatches | Record where the key is a collection member key and the value is a list of tuples that we'll use to replace the nested objects of that collection member record with something else. | +| params.isProcessingCollectionUpdate | whether this is part of a collection update operation. | +| retryAttempt | retry attempt | -## partialSetCollection(collectionKey, collection) +## partialSetCollection(params, retryAttempt) Sets keys in a collection by replacing all targeted collection members with new values. Any existing collection members not included in the new data will not be removed. +Retries on failure. **Kind**: global function | Param | Description | | --- | --- | -| collectionKey | e.g. `ONYXKEYS.COLLECTION.REPORT` | -| collection | Object collection keyed by individual collection member keys and values | +| params | collection parameters | +| params.collectionKey | e.g. `ONYXKEYS.COLLECTION.REPORT` | +| params.collection | Object collection keyed by individual collection member keys and values | +| retryAttempt | retry attempt | From 7da26ae30475d00bc8d0d0068aaf8520cdbf4a83 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 3 Nov 2025 09:47:13 +0100 Subject: [PATCH 17/23] Adjust params passed to the retry function --- lib/OnyxUtils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 7a76a28b..7bd790e0 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1333,7 +1333,7 @@ function setWithRetry({key, value, options}: SetParams OnyxUtils.retryOperation(error, setWithRetry, {key, value: valueWithoutNestedNullValues}, retryAttempt)) + .catch((error) => OnyxUtils.retryOperation(error, setWithRetry, {key, value: valueWithoutNestedNullValues, options}, retryAttempt)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues); return updatePromise; @@ -1580,7 +1580,9 @@ function mergeCollectionWithPatches( }); return Promise.all(promises) - .catch((error) => retryOperation(error, mergeCollectionWithPatches, {collectionKey, collection: resultCollection}, retryAttempt)) + .catch((error) => + retryOperation(error, mergeCollectionWithPatches, {collectionKey, collection: resultCollection, mergeReplaceNullPatches, isProcessingCollectionUpdate}, retryAttempt), + ) .then(() => { sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection); return promiseUpdate; From f5e8b4bca9a25e96f435dd7f8a72d308ed6fc2f9 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 3 Nov 2025 12:10:03 +0100 Subject: [PATCH 18/23] Apply feedback --- lib/OnyxUtils.ts | 4 ++-- lib/types.ts | 4 ++-- tests/perf-test/OnyxUtils.perf-test.ts | 4 ++-- tests/unit/onyxUtilsTest.ts | 23 +++++++++-------------- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 7bd790e0..0a8db3e4 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -33,7 +33,7 @@ import type { SetCollectionParams, SetParams, OnyxMultiSetInput, - OnyxRetryOperation, + RetriableOnyxOperation, } from './types'; import type {FastMergeOptions, FastMergeResult} from './utils'; import utils from './utils'; @@ -892,7 +892,7 @@ function reportStorageQuota(): Promise { * - Invalid data errors: logs an alert and throws an error * - Other errors: retries the operation */ -function retryOperation(error: Error, onyxMethod: TMethod, defaultParams: Parameters[0], retryAttempt: number | undefined): Promise { +function retryOperation(error: Error, onyxMethod: TMethod, defaultParams: Parameters[0], retryAttempt: number | undefined): Promise { const currentRetryAttempt = retryAttempt ?? 0; const nextRetryAttempt = currentRetryAttempt + 1; diff --git a/lib/types.ts b/lib/types.ts index 328b6083..5659af91 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -391,7 +391,7 @@ type MergeCollectionWithPatchesParams = { isProcessingCollectionUpdate?: boolean; }; -type OnyxRetryOperation = +type RetriableOnyxOperation = | typeof OnyxUtils.setWithRetry | typeof OnyxUtils.multiSetWithRetry | typeof OnyxUtils.setCollectionWithRetry @@ -510,5 +510,5 @@ export type { MergeCollectionWithPatchesParams, MultiMergeReplaceNullPatches, MixedOperationsQueue, - OnyxRetryOperation, + RetriableOnyxOperation, }; diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts index a300e917..ea8844a2 100644 --- a/tests/perf-test/OnyxUtils.perf-test.ts +++ b/tests/perf-test/OnyxUtils.perf-test.ts @@ -9,7 +9,7 @@ import OnyxUtils, {clearOnyxUtilsInternals} from '../../lib/OnyxUtils'; import type GenericCollection from '../utils/GenericCollection'; import type {OnyxUpdate} from '../../lib/Onyx'; import createDeferredTask from '../../lib/createDeferredTask'; -import type {OnyxInputKeyValueMapping, OnyxRetryOperation} from '../../lib/types'; +import type {OnyxInputKeyValueMapping, RetriableOnyxOperation} from '../../lib/types'; const ONYXKEYS = { TEST_KEY: 'test', @@ -505,7 +505,7 @@ describe('OnyxUtils', () => { describe('retryOperation', () => { test('one call', async () => { const error = new Error(); - const onyxMethod = jest.fn() as OnyxRetryOperation; + const onyxMethod = jest.fn() as RetriableOnyxOperation; await measureAsyncFunction(() => OnyxUtils.retryOperation(error, onyxMethod, {key: '', value: null}, 1), { beforeEach: async () => { diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 5fafc2c5..b4f49e54 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -83,7 +83,7 @@ Onyx.init({ beforeEach(() => Onyx.clear()); -afterEach(() => jest.restoreAllMocks()); +afterEach(() => jest.clearAllMocks()); describe('OnyxUtils', () => { describe('splitCollectionMemberKey', () => { @@ -420,10 +420,12 @@ describe('OnyxUtils', () => { }); describe('retryOperation', () => { - it('should retry only one time if the operation is firstly failed and then passed', async () => { - const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); - const genericError = new Error('Generic storage error'); + const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); + const genericError = new Error('Generic storage error'); + const invalidDataError = new Error("Failed to execute 'put' on 'IDBObjectStore': invalid data"); + const memoryError = new Error('out of memory'); + it('should retry only one time if the operation is firstly failed and then passed', async () => { StorageMock.setItem = jest.fn(StorageMock.setItem).mockRejectedValueOnce(genericError).mockImplementation(StorageMock.setItem); await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); @@ -433,9 +435,6 @@ describe('OnyxUtils', () => { }); it('should stop retrying after MAX_STORAGE_OPERATION_RETRY_ATTEMPTS retries for failing operation', async () => { - const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); - const genericError = new Error('Generic storage error'); - StorageMock.setItem = jest.fn().mockRejectedValue(genericError); await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); @@ -445,17 +444,13 @@ describe('OnyxUtils', () => { }); it("should throw error for if operation failed with \"Failed to execute 'put' on 'IDBObjectStore': invalid data\" error", async () => { - const idbError = new Error("Failed to execute 'put' on 'IDBObjectStore': invalid data"); - StorageMock.setItem = jest.fn().mockRejectedValueOnce(idbError); + StorageMock.setItem = jest.fn().mockRejectedValueOnce(invalidDataError); - await expect(Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'})).rejects.toThrow(idbError); + await expect(Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'})).rejects.toThrow(invalidDataError); }); it('should not retry in case of storage capacity error and no keys to evict', async () => { - const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); - const quotaError = new Error('out of memory'); - - StorageMock.setItem = jest.fn().mockRejectedValue(quotaError); + StorageMock.setItem = jest.fn().mockRejectedValue(memoryError); await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); From 3a6e61df1352a43733684c7d651db34ee2a8ae1b Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 3 Nov 2025 18:32:58 +0100 Subject: [PATCH 19/23] Add changes from main --- lib/OnyxUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 9f0764b0..d3603f09 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1501,9 +1501,10 @@ function setCollectionWithRetry({collectio const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true); const previousCollection = OnyxUtils.getCachedCollection(collectionKey); - keyValuePairs.forEach(([key, value]) => cache.set(key, value)); + // Preserve references for unchanged items in setCollection + const preservedCollection = OnyxUtils.preserveCollectionReferences(keyValuePairs); - const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); + const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, preservedCollection, previousCollection); return Storage.multiSet(keyValuePairs) .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, {collectionKey, collection}, retryAttempt)) From 8ebfc39f995c46f561bd7c923c46e9075327c679 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 3 Nov 2025 20:10:09 +0100 Subject: [PATCH 20/23] Add logging when max retries are reached --- lib/OnyxUtils.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index d3603f09..d372e37c 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -961,9 +961,14 @@ function retryOperation(error: Error, on const errorName = error?.name?.toLowerCase?.(); const isStorageCapacityError = STORAGE_ERRORS.some((storageError) => errorName?.includes(storageError) || errorMessage?.includes(storageError)); + if (nextRetryAttempt > MAX_STORAGE_OPERATION_RETRY_ATTEMPTS) { + Logger.logAlert(`Storage operation failed after 5 retries. Error: ${error}. onyxMethod: ${onyxMethod.name}.`); + return Promise.resolve(); + } + if (!isStorageCapacityError) { // @ts-expect-error No overload matches this call. - return nextRetryAttempt > MAX_STORAGE_OPERATION_RETRY_ATTEMPTS ? Promise.resolve() : onyxMethod(defaultParams, nextRetryAttempt); + return onyxMethod(defaultParams, nextRetryAttempt); } // Find the first key that we can remove that has no subscribers in our blocklist @@ -981,7 +986,7 @@ function retryOperation(error: Error, on reportStorageQuota(); // @ts-expect-error No overload matches this call. - return remove(keyForRemoval).then(() => (nextRetryAttempt > MAX_STORAGE_OPERATION_RETRY_ATTEMPTS ? Promise.resolve() : onyxMethod(defaultParams, nextRetryAttempt))); + return remove(keyForRemoval).then(() => onyxMethod(defaultParams, nextRetryAttempt)); } /** From fddfe2d5b9a7b2429d82fa4e8efc9d20c7891317 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 3 Nov 2025 20:21:13 +0100 Subject: [PATCH 21/23] Re-run checks From fa363e81560f442d442990830251a147fab97588 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 5 Nov 2025 09:40:50 +0100 Subject: [PATCH 22/23] TS adjustments after merging main --- lib/OnyxUtils.ts | 12 ++++++++---- lib/types.ts | 8 ++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index ca31e9ff..2f8127f9 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -29,7 +29,6 @@ import type { OnyxUpdate, OnyxValue, Selector, - OnyxSetCollectionInput, MergeCollectionWithPatchesParams, SetCollectionParams, SetParams, @@ -1462,7 +1461,7 @@ function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Prom * @param params.collection Object collection keyed by individual collection member keys and values * @param retryAttempt retry attempt */ -function setCollectionWithRetry({collectionKey, collection}: SetCollectionParams, retryAttempt?: number): Promise { +function setCollectionWithRetry({collectionKey, collection}: SetCollectionParams, retryAttempt?: number): Promise { let resultCollection: OnyxInputKeyValueMapping = collection; let resultCollectionKeys = Object.keys(resultCollection); @@ -1653,7 +1652,12 @@ function mergeCollectionWithPatches( return Promise.all(promises) .catch((error) => - retryOperation(error, mergeCollectionWithPatches, {collectionKey, collection: resultCollection, mergeReplaceNullPatches, isProcessingCollectionUpdate}, retryAttempt), + retryOperation( + error, + mergeCollectionWithPatches, + {collectionKey, collection: resultCollection as OnyxMergeCollectionInput, mergeReplaceNullPatches, isProcessingCollectionUpdate}, + retryAttempt, + ), ) .then(() => { sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection); @@ -1673,7 +1677,7 @@ function mergeCollectionWithPatches( * @param params.collection Object collection keyed by individual collection member keys and values * @param retryAttempt retry attempt */ -function partialSetCollection({collectionKey, collection}: SetCollectionParams, retryAttempt?: number): Promise { +function partialSetCollection({collectionKey, collection}: SetCollectionParams, retryAttempt?: number): Promise { let resultCollection: OnyxInputKeyValueMapping = collection; let resultCollectionKeys = Object.keys(resultCollection); diff --git a/lib/types.ts b/lib/types.ts index 74a986a1..c802814c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -386,14 +386,14 @@ type SetParams = { options?: SetOptions; }; -type SetCollectionParams = { +type SetCollectionParams = { collectionKey: TKey; - collection: OnyxMergeCollectionInput; + collection: OnyxSetCollectionInput; }; -type MergeCollectionWithPatchesParams = { +type MergeCollectionWithPatchesParams = { collectionKey: TKey; - collection: OnyxMergeCollectionInput; + collection: OnyxMergeCollectionInput; mergeReplaceNullPatches?: MultiMergeReplaceNullPatches; isProcessingCollectionUpdate?: boolean; }; From 447f9e398cae746e3f6a277875e652b6c3f79bb5 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 5 Nov 2025 10:37:13 +0100 Subject: [PATCH 23/23] Minor improvement --- lib/OnyxUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 2f8127f9..155e0d67 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1358,6 +1358,7 @@ function setWithRetry({key, value, options}: SetParams