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:
+
+- 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
@@ -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