react-native-onyx 3.0.11 → 3.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Onyx.js CHANGED
@@ -145,62 +145,7 @@ function disconnect(connection) {
145
145
  * @param options optional configuration object
146
146
  */
147
147
  function set(key, value, options) {
148
- // When we use Onyx.set to set a key we want to clear the current delta changes from Onyx.merge that were queued
149
- // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes.
150
- if (OnyxUtils_1.default.hasPendingMergeForKey(key)) {
151
- delete OnyxUtils_1.default.getMergeQueue()[key];
152
- }
153
- const skippableCollectionMemberIDs = OnyxUtils_1.default.getSkippableCollectionMemberIDs();
154
- if (skippableCollectionMemberIDs.size) {
155
- try {
156
- const [, collectionMemberID] = OnyxUtils_1.default.splitCollectionMemberKey(key);
157
- if (skippableCollectionMemberIDs.has(collectionMemberID)) {
158
- // The key is a skippable one, so we set the new value to null.
159
- // eslint-disable-next-line no-param-reassign
160
- value = null;
161
- }
162
- }
163
- catch (e) {
164
- // The key is not a collection one or something went wrong during split, so we proceed with the function's logic.
165
- }
166
- }
167
- // Onyx.set will ignore `undefined` values as inputs, therefore we can return early.
168
- if (value === undefined) {
169
- return Promise.resolve();
170
- }
171
- const existingValue = OnyxCache_1.default.get(key, false);
172
- // If the existing value as well as the new value are null, we can return early.
173
- if (existingValue === undefined && value === null) {
174
- return Promise.resolve();
175
- }
176
- // Check if the value is compatible with the existing value in the storage
177
- const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(value, existingValue);
178
- if (!isCompatible) {
179
- Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'set', existingValueType, newValueType));
180
- return Promise.resolve();
181
- }
182
- // If the change is null, we can just delete the key.
183
- // Therefore, we don't need to further broadcast and update the value so we can return early.
184
- if (value === null) {
185
- OnyxUtils_1.default.remove(key);
186
- OnyxUtils_1.default.logKeyRemoved(OnyxUtils_1.default.METHOD.SET, key);
187
- return Promise.resolve();
188
- }
189
- const valueWithoutNestedNullValues = utils_1.default.removeNestedNullValues(value);
190
- const hasChanged = (options === null || options === void 0 ? void 0 : options.skipCacheCheck) ? true : OnyxCache_1.default.hasValueChanged(key, valueWithoutNestedNullValues);
191
- OnyxUtils_1.default.logKeyChanged(OnyxUtils_1.default.METHOD.SET, key, value, hasChanged);
192
- // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
193
- const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged);
194
- // 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.
195
- if (!hasChanged) {
196
- return updatePromise;
197
- }
198
- return storage_1.default.setItem(key, valueWithoutNestedNullValues)
199
- .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, set, key, valueWithoutNestedNullValues))
200
- .then(() => {
201
- OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.SET, key, valueWithoutNestedNullValues);
202
- return updatePromise;
203
- });
148
+ return OnyxUtils_1.default.setWithRetry({ key, value, options });
204
149
  }
205
150
  /**
206
151
  * Sets multiple keys and values
@@ -210,42 +155,7 @@ function set(key, value, options) {
210
155
  * @param data object keyed by ONYXKEYS and the values to set
211
156
  */
212
157
  function multiSet(data) {
213
- let newData = data;
214
- const skippableCollectionMemberIDs = OnyxUtils_1.default.getSkippableCollectionMemberIDs();
215
- if (skippableCollectionMemberIDs.size) {
216
- newData = Object.keys(newData).reduce((result, key) => {
217
- try {
218
- const [, collectionMemberID] = OnyxUtils_1.default.splitCollectionMemberKey(key);
219
- // If the collection member key is a skippable one we set its value to null.
220
- // eslint-disable-next-line no-param-reassign
221
- result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? newData[key] : null;
222
- }
223
- catch (_a) {
224
- // The key is not a collection one or something went wrong during split, so we assign the data to result anyway.
225
- // eslint-disable-next-line no-param-reassign
226
- result[key] = newData[key];
227
- }
228
- return result;
229
- }, {});
230
- }
231
- const keyValuePairsToSet = OnyxUtils_1.default.prepareKeyValuePairsForStorage(newData, true);
232
- const updatePromises = keyValuePairsToSet.map(([key, value]) => {
233
- // When we use multiSet to set a key we want to clear the current delta changes from Onyx.merge that were queued
234
- // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes.
235
- if (OnyxUtils_1.default.hasPendingMergeForKey(key)) {
236
- delete OnyxUtils_1.default.getMergeQueue()[key];
237
- }
238
- // Update cache and optimistically inform subscribers on the next tick
239
- OnyxCache_1.default.set(key, value);
240
- return OnyxUtils_1.default.scheduleSubscriberUpdate(key, value);
241
- });
242
- return storage_1.default.multiSet(keyValuePairsToSet)
243
- .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, multiSet, newData))
244
- .then(() => {
245
- OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MULTI_SET, undefined, newData);
246
- return Promise.all(updatePromises);
247
- })
248
- .then(() => undefined);
158
+ return OnyxUtils_1.default.multiSetWithRetry(data);
249
159
  }
250
160
  /**
251
161
  * Merge a new value into an existing value at a key.
@@ -344,7 +254,7 @@ function merge(key, changes) {
344
254
  * @param collection Object collection keyed by individual collection member keys and values
345
255
  */
346
256
  function mergeCollection(collectionKey, collection) {
347
- return OnyxUtils_1.default.mergeCollectionWithPatches(collectionKey, collection, undefined, true);
257
+ return OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection, isProcessingCollectionUpdate: true });
348
258
  }
349
259
  /**
350
260
  * Clear out all the data in the store
@@ -553,10 +463,15 @@ function update(data) {
553
463
  set: {},
554
464
  });
555
465
  if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.merge)) {
556
- promises.push(() => OnyxUtils_1.default.mergeCollectionWithPatches(collectionKey, batchedCollectionUpdates.merge, batchedCollectionUpdates.mergeReplaceNullPatches, true));
466
+ promises.push(() => OnyxUtils_1.default.mergeCollectionWithPatches({
467
+ collectionKey,
468
+ collection: batchedCollectionUpdates.merge,
469
+ mergeReplaceNullPatches: batchedCollectionUpdates.mergeReplaceNullPatches,
470
+ isProcessingCollectionUpdate: true,
471
+ }));
557
472
  }
558
473
  if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) {
559
- promises.push(() => OnyxUtils_1.default.partialSetCollection(collectionKey, batchedCollectionUpdates.set));
474
+ promises.push(() => OnyxUtils_1.default.partialSetCollection({ collectionKey, collection: batchedCollectionUpdates.set }));
560
475
  }
561
476
  });
562
477
  Object.entries(updateQueue).forEach(([key, operations]) => {
@@ -588,54 +503,7 @@ function update(data) {
588
503
  * @param collection Object collection keyed by individual collection member keys and values
589
504
  */
590
505
  function setCollection(collectionKey, collection) {
591
- let resultCollection = collection;
592
- let resultCollectionKeys = Object.keys(resultCollection);
593
- // Confirm all the collection keys belong to the same parent
594
- if (!OnyxUtils_1.default.doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) {
595
- Logger.logAlert(`setCollection called with keys that do not belong to the same parent ${collectionKey}. Skipping this update.`);
596
- return Promise.resolve();
597
- }
598
- const skippableCollectionMemberIDs = OnyxUtils_1.default.getSkippableCollectionMemberIDs();
599
- if (skippableCollectionMemberIDs.size) {
600
- resultCollection = resultCollectionKeys.reduce((result, key) => {
601
- try {
602
- const [, collectionMemberID] = OnyxUtils_1.default.splitCollectionMemberKey(key, collectionKey);
603
- // If the collection member key is a skippable one we set its value to null.
604
- // eslint-disable-next-line no-param-reassign
605
- result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null;
606
- }
607
- catch (_a) {
608
- // Something went wrong during split, so we assign the data to result anyway.
609
- // eslint-disable-next-line no-param-reassign
610
- result[key] = resultCollection[key];
611
- }
612
- return result;
613
- }, {});
614
- }
615
- resultCollectionKeys = Object.keys(resultCollection);
616
- return OnyxUtils_1.default.getAllKeys().then((persistedKeys) => {
617
- const mutableCollection = Object.assign({}, resultCollection);
618
- persistedKeys.forEach((key) => {
619
- if (!key.startsWith(collectionKey)) {
620
- return;
621
- }
622
- if (resultCollectionKeys.includes(key)) {
623
- return;
624
- }
625
- mutableCollection[key] = null;
626
- });
627
- const keyValuePairs = OnyxUtils_1.default.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
628
- const previousCollection = OnyxUtils_1.default.getCachedCollection(collectionKey);
629
- // Preserve references for unchanged items in setCollection
630
- const preservedCollection = OnyxUtils_1.default.preserveCollectionReferences(keyValuePairs);
631
- const updatePromise = OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(collectionKey, preservedCollection, previousCollection);
632
- return storage_1.default.multiSet(keyValuePairs)
633
- .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, setCollection, collectionKey, collection))
634
- .then(() => {
635
- OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.SET_COLLECTION, undefined, mutableCollection);
636
- return updatePromise;
637
- });
638
- });
506
+ return OnyxUtils_1.default.setCollectionWithRetry({ collectionKey, collection });
639
507
  }
640
508
  const Onyx = {
641
509
  METHOD: OnyxUtils_1.default.METHOD,
@@ -1,6 +1,6 @@
1
1
  import type { ValueOf } from 'type-fest';
2
2
  import type Onyx from './Onyx';
3
- import type { CollectionKey, CollectionKeyBase, ConnectOptions, DeepRecord, KeyValueMapping, CallbackToStateMapping, MultiMergeReplaceNullPatches, OnyxCollection, OnyxEntry, OnyxInput, OnyxInputKeyValueMapping, OnyxKey, OnyxMergeCollectionInput, OnyxUpdate, OnyxValue, Selector, OnyxSetCollectionInput } from './types';
3
+ import type { CollectionKey, CollectionKeyBase, ConnectOptions, DeepRecord, KeyValueMapping, CallbackToStateMapping, MultiMergeReplaceNullPatches, OnyxCollection, OnyxEntry, OnyxInput, OnyxInputKeyValueMapping, OnyxKey, OnyxMergeCollectionInput, OnyxUpdate, OnyxValue, Selector, MergeCollectionWithPatchesParams, SetCollectionParams, SetParams, OnyxMultiSetInput, RetriableOnyxOperation } from './types';
4
4
  import type { FastMergeResult } from './utils';
5
5
  import type { DeferredTask } from './createDeferredTask';
6
6
  import type { StorageKeyValuePair } from './storage/providers/types';
@@ -194,11 +194,12 @@ declare function scheduleNotifyCollectionSubscribers<TKey extends OnyxKey>(key:
194
194
  declare function remove<TKey extends OnyxKey>(key: TKey, isProcessingCollectionUpdate?: boolean): Promise<void>;
195
195
  declare function reportStorageQuota(): Promise<void>;
196
196
  /**
197
- * If we fail to set or merge we must handle this by
198
- * evicting some data from Onyx and then retrying to do
199
- * whatever it is we attempted to do.
197
+ * Handles storage operation failures based on the error type:
198
+ * - Storage capacity errors: evicts data and retries the operation
199
+ * - Invalid data errors: logs an alert and throws an error
200
+ * - Other errors: retries the operation
200
201
  */
201
- declare function evictStorageAndRetry<TMethod extends typeof Onyx.set | typeof Onyx.multiSet | typeof Onyx.mergeCollection | typeof Onyx.setCollection>(error: Error, onyxMethod: TMethod, ...args: Parameters<TMethod>): Promise<void>;
202
+ declare function retryOperation<TMethod extends RetriableOnyxOperation>(error: Error, onyxMethod: TMethod, defaultParams: Parameters<TMethod>[0], retryAttempt: number | undefined): Promise<void>;
202
203
  /**
203
204
  * Notifies subscribers and writes current value to cache
204
205
  */
@@ -253,25 +254,64 @@ declare function subscribeToKey<TKey extends OnyxKey>(connectOptions: ConnectOpt
253
254
  */
254
255
  declare function unsubscribeFromKey(subscriptionID: number): void;
255
256
  declare function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<() => Promise<void>>;
257
+ /**
258
+ * Writes a value to our store with the given key.
259
+ * Serves as core implementation for `Onyx.set()` public function, the difference being
260
+ * that this internal function allows passing an additional `retryAttempt` parameter to retry on failure.
261
+ *
262
+ * @param params - set parameters
263
+ * @param params.key ONYXKEY to set
264
+ * @param params.value value to store
265
+ * @param params.options optional configuration object
266
+ * @param retryAttempt retry attempt
267
+ */
268
+ declare function setWithRetry<TKey extends OnyxKey>({ key, value, options }: SetParams<TKey>, retryAttempt?: number): Promise<void>;
269
+ /**
270
+ * Sets multiple keys and values.
271
+ * Serves as core implementation for `Onyx.multiSet()` public function, the difference being
272
+ * that this internal function allows passing an additional `retryAttempt` parameter to retry on failure.
273
+ *
274
+ * @param data object keyed by ONYXKEYS and the values to set
275
+ * @param retryAttempt retry attempt
276
+ */
277
+ declare function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Promise<void>;
278
+ /**
279
+ * Sets a collection by replacing all existing collection members with new values.
280
+ * Any existing collection members not included in the new data will be removed.
281
+ * Serves as core implementation for `Onyx.setCollection()` public function, the difference being
282
+ * that this internal function allows passing an additional `retryAttempt` parameter to retry on failure.
283
+ *
284
+ * @param params - collection parameters
285
+ * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
286
+ * @param params.collection Object collection keyed by individual collection member keys and values
287
+ * @param retryAttempt retry attempt
288
+ */
289
+ declare function setCollectionWithRetry<TKey extends CollectionKeyBase>({ collectionKey, collection }: SetCollectionParams<TKey>, retryAttempt?: number): Promise<void>;
256
290
  /**
257
291
  * Merges a collection based on their keys.
258
292
  * Serves as core implementation for `Onyx.mergeCollection()` public function, the difference being
259
- * that this internal function allows passing an additional `mergeReplaceNullPatches` parameter.
293
+ * that this internal function allows passing an additional `mergeReplaceNullPatches` parameter and retries on failure.
260
294
  *
261
- * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
262
- * @param collection Object collection keyed by individual collection member keys and values
263
- * @param mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
295
+ * @param params - mergeCollection parameters
296
+ * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
297
+ * @param params.collection Object collection keyed by individual collection member keys and values
298
+ * @param params.mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
264
299
  * tuples that we'll use to replace the nested objects of that collection member record with something else.
300
+ * @param params.isProcessingCollectionUpdate whether this is part of a collection update operation.
301
+ * @param retryAttempt retry attempt
265
302
  */
266
- declare function mergeCollectionWithPatches<TKey extends CollectionKeyBase>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey>, mergeReplaceNullPatches?: MultiMergeReplaceNullPatches, isProcessingCollectionUpdate?: boolean): Promise<void>;
303
+ declare function mergeCollectionWithPatches<TKey extends CollectionKeyBase>({ collectionKey, collection, mergeReplaceNullPatches, isProcessingCollectionUpdate }: MergeCollectionWithPatchesParams<TKey>, retryAttempt?: number): Promise<void>;
267
304
  /**
268
305
  * Sets keys in a collection by replacing all targeted collection members with new values.
269
306
  * Any existing collection members not included in the new data will not be removed.
307
+ * Retries on failure.
270
308
  *
271
- * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
272
- * @param collection Object collection keyed by individual collection member keys and values
309
+ * @param params - collection parameters
310
+ * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
311
+ * @param params.collection Object collection keyed by individual collection member keys and values
312
+ * @param retryAttempt retry attempt
273
313
  */
274
- declare function partialSetCollection<TKey extends CollectionKeyBase>(collectionKey: TKey, collection: OnyxSetCollectionInput<TKey>): Promise<void>;
314
+ declare function partialSetCollection<TKey extends CollectionKeyBase>({ collectionKey, collection }: SetCollectionParams<TKey>, retryAttempt?: number): Promise<void>;
275
315
  declare function logKeyChanged(onyxMethod: Extract<OnyxMethod, 'set' | 'merge'>, key: OnyxKey, value: unknown, hasChanged: boolean): void;
276
316
  declare function logKeyRemoved(onyxMethod: Extract<OnyxMethod, 'set' | 'merge'>, key: OnyxKey): void;
277
317
  /**
@@ -313,7 +353,7 @@ declare const OnyxUtils: {
313
353
  scheduleNotifyCollectionSubscribers: typeof scheduleNotifyCollectionSubscribers;
314
354
  remove: typeof remove;
315
355
  reportStorageQuota: typeof reportStorageQuota;
316
- evictStorageAndRetry: typeof evictStorageAndRetry;
356
+ retryOperation: typeof retryOperation;
317
357
  broadcastUpdate: typeof broadcastUpdate;
318
358
  hasPendingMergeForKey: typeof hasPendingMergeForKey;
319
359
  prepareKeyValuePairsForStorage: typeof prepareKeyValuePairsForStorage;
@@ -340,6 +380,9 @@ declare const OnyxUtils: {
340
380
  preserveCollectionReferencesAfterMerge: typeof preserveCollectionReferencesAfterMerge;
341
381
  logKeyChanged: typeof logKeyChanged;
342
382
  logKeyRemoved: typeof logKeyRemoved;
383
+ setWithRetry: typeof setWithRetry;
384
+ multiSetWithRetry: typeof multiSetWithRetry;
385
+ setCollectionWithRetry: typeof setCollectionWithRetry;
343
386
  };
344
387
  export type { OnyxMethod };
345
388
  export default OnyxUtils;
package/dist/OnyxUtils.js CHANGED
@@ -61,6 +61,19 @@ const METHOD = {
61
61
  MULTI_SET: 'multiset',
62
62
  CLEAR: 'clear',
63
63
  };
64
+ // IndexedDB errors that indicate storage capacity issues where eviction can help
65
+ const IDB_STORAGE_ERRORS = [
66
+ 'quotaexceedederror', // Browser storage quota exceeded
67
+ ];
68
+ // SQLite errors that indicate storage capacity issues where eviction can help
69
+ const SQLITE_STORAGE_ERRORS = [
70
+ 'database or disk is full', // Device storage is full
71
+ 'disk I/O error', // File system I/O failure, often due to insufficient space or corrupted storage
72
+ 'out of memory', // Insufficient RAM or storage space to complete the operation
73
+ ];
74
+ const STORAGE_ERRORS = [...IDB_STORAGE_ERRORS, ...SQLITE_STORAGE_ERRORS];
75
+ // Max number of retries for failed storage operations
76
+ const MAX_STORAGE_OPERATION_RETRY_ATTEMPTS = 5;
64
77
  // Key/value store of Onyx key and arrays of values to merge
65
78
  let mergeQueue = {};
66
79
  let mergeQueuePromise = {};
@@ -757,16 +770,31 @@ function reportStorageQuota() {
757
770
  });
758
771
  }
759
772
  /**
760
- * If we fail to set or merge we must handle this by
761
- * evicting some data from Onyx and then retrying to do
762
- * whatever it is we attempted to do.
773
+ * Handles storage operation failures based on the error type:
774
+ * - Storage capacity errors: evicts data and retries the operation
775
+ * - Invalid data errors: logs an alert and throws an error
776
+ * - Other errors: retries the operation
763
777
  */
764
- function evictStorageAndRetry(error, onyxMethod, ...args) {
765
- Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}`);
778
+ function retryOperation(error, onyxMethod, defaultParams, retryAttempt) {
779
+ var _a, _b, _c, _d;
780
+ const currentRetryAttempt = retryAttempt !== null && retryAttempt !== void 0 ? retryAttempt : 0;
781
+ const nextRetryAttempt = currentRetryAttempt + 1;
782
+ Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}. retryAttempt: ${currentRetryAttempt}/${MAX_STORAGE_OPERATION_RETRY_ATTEMPTS}`);
766
783
  if (error && Str.startsWith(error.message, "Failed to execute 'put' on 'IDBObjectStore'")) {
767
784
  Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.');
768
785
  throw error;
769
786
  }
787
+ const errorMessage = (_b = (_a = error === null || error === void 0 ? void 0 : error.message) === null || _a === void 0 ? void 0 : _a.toLowerCase) === null || _b === void 0 ? void 0 : _b.call(_a);
788
+ const errorName = (_d = (_c = error === null || error === void 0 ? void 0 : error.name) === null || _c === void 0 ? void 0 : _c.toLowerCase) === null || _d === void 0 ? void 0 : _d.call(_c);
789
+ const isStorageCapacityError = STORAGE_ERRORS.some((storageError) => (errorName === null || errorName === void 0 ? void 0 : errorName.includes(storageError)) || (errorMessage === null || errorMessage === void 0 ? void 0 : errorMessage.includes(storageError)));
790
+ if (nextRetryAttempt > MAX_STORAGE_OPERATION_RETRY_ATTEMPTS) {
791
+ Logger.logAlert(`Storage operation failed after 5 retries. Error: ${error}. onyxMethod: ${onyxMethod.name}.`);
792
+ return Promise.resolve();
793
+ }
794
+ if (!isStorageCapacityError) {
795
+ // @ts-expect-error No overload matches this call.
796
+ return onyxMethod(defaultParams, nextRetryAttempt);
797
+ }
770
798
  // Find the first key that we can remove that has no subscribers in our blocklist
771
799
  const keyForRemoval = OnyxCache_1.default.getKeyForEviction();
772
800
  if (!keyForRemoval) {
@@ -780,7 +808,7 @@ function evictStorageAndRetry(error, onyxMethod, ...args) {
780
808
  Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`);
781
809
  reportStorageQuota();
782
810
  // @ts-expect-error No overload matches this call.
783
- return remove(keyForRemoval).then(() => onyxMethod(...args));
811
+ return remove(keyForRemoval).then(() => onyxMethod(defaultParams, nextRetryAttempt));
784
812
  }
785
813
  /**
786
814
  * Notifies subscribers and writes current value to cache
@@ -1050,17 +1078,193 @@ function updateSnapshots(data, mergeFn) {
1050
1078
  });
1051
1079
  return promises;
1052
1080
  }
1081
+ /**
1082
+ * Writes a value to our store with the given key.
1083
+ * Serves as core implementation for `Onyx.set()` public function, the difference being
1084
+ * that this internal function allows passing an additional `retryAttempt` parameter to retry on failure.
1085
+ *
1086
+ * @param params - set parameters
1087
+ * @param params.key ONYXKEY to set
1088
+ * @param params.value value to store
1089
+ * @param params.options optional configuration object
1090
+ * @param retryAttempt retry attempt
1091
+ */
1092
+ function setWithRetry({ key, value, options }, retryAttempt) {
1093
+ // When we use Onyx.set to set a key we want to clear the current delta changes from Onyx.merge that were queued
1094
+ // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes.
1095
+ if (OnyxUtils.hasPendingMergeForKey(key)) {
1096
+ delete OnyxUtils.getMergeQueue()[key];
1097
+ }
1098
+ if (skippableCollectionMemberIDs.size) {
1099
+ try {
1100
+ const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key);
1101
+ if (skippableCollectionMemberIDs.has(collectionMemberID)) {
1102
+ // The key is a skippable one, so we set the new value to null.
1103
+ // eslint-disable-next-line no-param-reassign
1104
+ value = null;
1105
+ }
1106
+ }
1107
+ catch (e) {
1108
+ // The key is not a collection one or something went wrong during split, so we proceed with the function's logic.
1109
+ }
1110
+ }
1111
+ // Onyx.set will ignore `undefined` values as inputs, therefore we can return early.
1112
+ if (value === undefined) {
1113
+ return Promise.resolve();
1114
+ }
1115
+ const existingValue = OnyxCache_1.default.get(key, false);
1116
+ // If the existing value as well as the new value are null, we can return early.
1117
+ if (existingValue === undefined && value === null) {
1118
+ return Promise.resolve();
1119
+ }
1120
+ // Check if the value is compatible with the existing value in the storage
1121
+ const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(value, existingValue);
1122
+ if (!isCompatible) {
1123
+ Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'set', existingValueType, newValueType));
1124
+ return Promise.resolve();
1125
+ }
1126
+ // If the change is null, we can just delete the key.
1127
+ // Therefore, we don't need to further broadcast and update the value so we can return early.
1128
+ if (value === null) {
1129
+ OnyxUtils.remove(key);
1130
+ OnyxUtils.logKeyRemoved(OnyxUtils.METHOD.SET, key);
1131
+ return Promise.resolve();
1132
+ }
1133
+ const valueWithoutNestedNullValues = utils_1.default.removeNestedNullValues(value);
1134
+ const hasChanged = (options === null || options === void 0 ? void 0 : options.skipCacheCheck) ? true : OnyxCache_1.default.hasValueChanged(key, valueWithoutNestedNullValues);
1135
+ OnyxUtils.logKeyChanged(OnyxUtils.METHOD.SET, key, value, hasChanged);
1136
+ // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
1137
+ const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged);
1138
+ // If the value has not changed and this isn't a retry attempt, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
1139
+ if (!hasChanged && !retryAttempt) {
1140
+ return updatePromise;
1141
+ }
1142
+ return storage_1.default.setItem(key, valueWithoutNestedNullValues)
1143
+ .catch((error) => OnyxUtils.retryOperation(error, setWithRetry, { key, value: valueWithoutNestedNullValues, options }, retryAttempt))
1144
+ .then(() => {
1145
+ OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues);
1146
+ return updatePromise;
1147
+ });
1148
+ }
1149
+ /**
1150
+ * Sets multiple keys and values.
1151
+ * Serves as core implementation for `Onyx.multiSet()` public function, the difference being
1152
+ * that this internal function allows passing an additional `retryAttempt` parameter to retry on failure.
1153
+ *
1154
+ * @param data object keyed by ONYXKEYS and the values to set
1155
+ * @param retryAttempt retry attempt
1156
+ */
1157
+ function multiSetWithRetry(data, retryAttempt) {
1158
+ let newData = data;
1159
+ if (skippableCollectionMemberIDs.size) {
1160
+ newData = Object.keys(newData).reduce((result, key) => {
1161
+ try {
1162
+ const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key);
1163
+ // If the collection member key is a skippable one we set its value to null.
1164
+ // eslint-disable-next-line no-param-reassign
1165
+ result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? newData[key] : null;
1166
+ }
1167
+ catch (_a) {
1168
+ // The key is not a collection one or something went wrong during split, so we assign the data to result anyway.
1169
+ // eslint-disable-next-line no-param-reassign
1170
+ result[key] = newData[key];
1171
+ }
1172
+ return result;
1173
+ }, {});
1174
+ }
1175
+ const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(newData, true);
1176
+ const updatePromises = keyValuePairsToSet.map(([key, value]) => {
1177
+ // When we use multiSet to set a key we want to clear the current delta changes from Onyx.merge that were queued
1178
+ // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes.
1179
+ if (OnyxUtils.hasPendingMergeForKey(key)) {
1180
+ delete OnyxUtils.getMergeQueue()[key];
1181
+ }
1182
+ // Update cache and optimistically inform subscribers on the next tick
1183
+ OnyxCache_1.default.set(key, value);
1184
+ return OnyxUtils.scheduleSubscriberUpdate(key, value);
1185
+ });
1186
+ return storage_1.default.multiSet(keyValuePairsToSet)
1187
+ .catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt))
1188
+ .then(() => {
1189
+ OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData);
1190
+ return Promise.all(updatePromises);
1191
+ })
1192
+ .then(() => undefined);
1193
+ }
1194
+ /**
1195
+ * Sets a collection by replacing all existing collection members with new values.
1196
+ * Any existing collection members not included in the new data will be removed.
1197
+ * Serves as core implementation for `Onyx.setCollection()` public function, the difference being
1198
+ * that this internal function allows passing an additional `retryAttempt` parameter to retry on failure.
1199
+ *
1200
+ * @param params - collection parameters
1201
+ * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
1202
+ * @param params.collection Object collection keyed by individual collection member keys and values
1203
+ * @param retryAttempt retry attempt
1204
+ */
1205
+ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
1206
+ let resultCollection = collection;
1207
+ let resultCollectionKeys = Object.keys(resultCollection);
1208
+ // Confirm all the collection keys belong to the same parent
1209
+ if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) {
1210
+ Logger.logAlert(`setCollection called with keys that do not belong to the same parent ${collectionKey}. Skipping this update.`);
1211
+ return Promise.resolve();
1212
+ }
1213
+ if (skippableCollectionMemberIDs.size) {
1214
+ resultCollection = resultCollectionKeys.reduce((result, key) => {
1215
+ try {
1216
+ const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key, collectionKey);
1217
+ // If the collection member key is a skippable one we set its value to null.
1218
+ // eslint-disable-next-line no-param-reassign
1219
+ result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null;
1220
+ }
1221
+ catch (_a) {
1222
+ // Something went wrong during split, so we assign the data to result anyway.
1223
+ // eslint-disable-next-line no-param-reassign
1224
+ result[key] = resultCollection[key];
1225
+ }
1226
+ return result;
1227
+ }, {});
1228
+ }
1229
+ resultCollectionKeys = Object.keys(resultCollection);
1230
+ return OnyxUtils.getAllKeys().then((persistedKeys) => {
1231
+ const mutableCollection = Object.assign({}, resultCollection);
1232
+ persistedKeys.forEach((key) => {
1233
+ if (!key.startsWith(collectionKey)) {
1234
+ return;
1235
+ }
1236
+ if (resultCollectionKeys.includes(key)) {
1237
+ return;
1238
+ }
1239
+ mutableCollection[key] = null;
1240
+ });
1241
+ const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
1242
+ const previousCollection = OnyxUtils.getCachedCollection(collectionKey);
1243
+ // Preserve references for unchanged items in setCollection
1244
+ const preservedCollection = OnyxUtils.preserveCollectionReferences(keyValuePairs);
1245
+ const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, preservedCollection, previousCollection);
1246
+ return storage_1.default.multiSet(keyValuePairs)
1247
+ .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, { collectionKey, collection }, retryAttempt))
1248
+ .then(() => {
1249
+ OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection);
1250
+ return updatePromise;
1251
+ });
1252
+ });
1253
+ }
1053
1254
  /**
1054
1255
  * Merges a collection based on their keys.
1055
1256
  * Serves as core implementation for `Onyx.mergeCollection()` public function, the difference being
1056
- * that this internal function allows passing an additional `mergeReplaceNullPatches` parameter.
1257
+ * that this internal function allows passing an additional `mergeReplaceNullPatches` parameter and retries on failure.
1057
1258
  *
1058
- * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
1059
- * @param collection Object collection keyed by individual collection member keys and values
1060
- * @param mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
1259
+ * @param params - mergeCollection parameters
1260
+ * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
1261
+ * @param params.collection Object collection keyed by individual collection member keys and values
1262
+ * @param params.mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
1061
1263
  * tuples that we'll use to replace the nested objects of that collection member record with something else.
1264
+ * @param params.isProcessingCollectionUpdate whether this is part of a collection update operation.
1265
+ * @param retryAttempt retry attempt
1062
1266
  */
1063
- function mergeCollectionWithPatches(collectionKey, collection, mergeReplaceNullPatches, isProcessingCollectionUpdate = false) {
1267
+ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNullPatches, isProcessingCollectionUpdate = false }, retryAttempt) {
1064
1268
  if (!isValidNonEmptyCollectionForMerge(collection)) {
1065
1269
  Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
1066
1270
  return Promise.resolve();
@@ -1153,7 +1357,7 @@ function mergeCollectionWithPatches(collectionKey, collection, mergeReplaceNullP
1153
1357
  return scheduleNotifyCollectionSubscribers(collectionKey, preservedCollection, previousCollection);
1154
1358
  });
1155
1359
  return Promise.all(promises)
1156
- .catch((error) => evictStorageAndRetry(error, mergeCollectionWithPatches, collectionKey, resultCollection))
1360
+ .catch((error) => retryOperation(error, mergeCollectionWithPatches, { collectionKey, collection: resultCollection, mergeReplaceNullPatches, isProcessingCollectionUpdate }, retryAttempt))
1157
1361
  .then(() => {
1158
1362
  sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection);
1159
1363
  return promiseUpdate;
@@ -1164,11 +1368,14 @@ function mergeCollectionWithPatches(collectionKey, collection, mergeReplaceNullP
1164
1368
  /**
1165
1369
  * Sets keys in a collection by replacing all targeted collection members with new values.
1166
1370
  * Any existing collection members not included in the new data will not be removed.
1371
+ * Retries on failure.
1167
1372
  *
1168
- * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
1169
- * @param collection Object collection keyed by individual collection member keys and values
1373
+ * @param params - collection parameters
1374
+ * @param params.collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
1375
+ * @param params.collection Object collection keyed by individual collection member keys and values
1376
+ * @param retryAttempt retry attempt
1170
1377
  */
1171
- function partialSetCollection(collectionKey, collection) {
1378
+ function partialSetCollection({ collectionKey, collection }, retryAttempt) {
1172
1379
  let resultCollection = collection;
1173
1380
  let resultCollectionKeys = Object.keys(resultCollection);
1174
1381
  // Confirm all the collection keys belong to the same parent
@@ -1202,7 +1409,7 @@ function partialSetCollection(collectionKey, collection) {
1202
1409
  const preservedCollection = preserveCollectionReferences(keyValuePairs);
1203
1410
  const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, preservedCollection, previousCollection);
1204
1411
  return storage_1.default.multiSet(keyValuePairs)
1205
- .catch((error) => evictStorageAndRetry(error, partialSetCollection, collectionKey, collection))
1412
+ .catch((error) => retryOperation(error, partialSetCollection, { collectionKey, collection }, retryAttempt))
1206
1413
  .then(() => {
1207
1414
  sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
1208
1415
  return updatePromise;
@@ -1254,7 +1461,7 @@ const OnyxUtils = {
1254
1461
  scheduleNotifyCollectionSubscribers,
1255
1462
  remove,
1256
1463
  reportStorageQuota,
1257
- evictStorageAndRetry,
1464
+ retryOperation,
1258
1465
  broadcastUpdate,
1259
1466
  hasPendingMergeForKey,
1260
1467
  prepareKeyValuePairsForStorage,
@@ -1281,6 +1488,9 @@ const OnyxUtils = {
1281
1488
  preserveCollectionReferencesAfterMerge,
1282
1489
  logKeyChanged,
1283
1490
  logKeyRemoved,
1491
+ setWithRetry,
1492
+ multiSetWithRetry,
1493
+ setCollectionWithRetry,
1284
1494
  };
1285
1495
  GlobalSettings.addGlobalSettingsChangeListener(({ enablePerformanceMetrics }) => {
1286
1496
  if (!enablePerformanceMetrics) {
@@ -1314,7 +1524,7 @@ GlobalSettings.addGlobalSettingsChangeListener(({ enablePerformanceMetrics }) =>
1314
1524
  // @ts-expect-error Reassign
1315
1525
  reportStorageQuota = (0, metrics_1.default)(reportStorageQuota, 'OnyxUtils.reportStorageQuota');
1316
1526
  // @ts-expect-error Complex type signature
1317
- evictStorageAndRetry = (0, metrics_1.default)(evictStorageAndRetry, 'OnyxUtils.evictStorageAndRetry');
1527
+ retryOperation = (0, metrics_1.default)(retryOperation, 'OnyxUtils.retryOperation');
1318
1528
  // @ts-expect-error Reassign
1319
1529
  broadcastUpdate = (0, metrics_1.default)(broadcastUpdate, 'OnyxUtils.broadcastUpdate');
1320
1530
  // @ts-expect-error Reassign
@@ -1325,5 +1535,11 @@ GlobalSettings.addGlobalSettingsChangeListener(({ enablePerformanceMetrics }) =>
1325
1535
  tupleGet = (0, metrics_1.default)(tupleGet, 'OnyxUtils.tupleGet');
1326
1536
  // @ts-expect-error Reassign
1327
1537
  subscribeToKey = (0, metrics_1.default)(subscribeToKey, 'OnyxUtils.subscribeToKey');
1538
+ // @ts-expect-error Reassign
1539
+ setWithRetry = (0, metrics_1.default)(setWithRetry, 'OnyxUtils.setWithRetry');
1540
+ // @ts-expect-error Reassign
1541
+ multiSetWithRetry = (0, metrics_1.default)(multiSetWithRetry, 'OnyxUtils.multiSetWithRetry');
1542
+ // @ts-expect-error Reassign
1543
+ setCollectionWithRetry = (0, metrics_1.default)(setCollectionWithRetry, 'OnyxUtils.setCollectionWithRetry');
1328
1544
  });
1329
1545
  exports.default = OnyxUtils;
package/dist/types.d.ts CHANGED
@@ -311,6 +311,22 @@ type SetOptions = {
311
311
  /** Skip the deep equality check against the cached value. Improves performance for large objects. */
312
312
  skipCacheCheck?: boolean;
313
313
  };
314
+ type SetParams<TKey extends OnyxKey> = {
315
+ key: TKey;
316
+ value: OnyxSetInput<TKey>;
317
+ options?: SetOptions;
318
+ };
319
+ type SetCollectionParams<TKey extends CollectionKeyBase> = {
320
+ collectionKey: TKey;
321
+ collection: OnyxSetCollectionInput<TKey>;
322
+ };
323
+ type MergeCollectionWithPatchesParams<TKey extends CollectionKeyBase> = {
324
+ collectionKey: TKey;
325
+ collection: OnyxMergeCollectionInput<TKey>;
326
+ mergeReplaceNullPatches?: MultiMergeReplaceNullPatches;
327
+ isProcessingCollectionUpdate?: boolean;
328
+ };
329
+ type RetriableOnyxOperation = typeof OnyxUtils.setWithRetry | typeof OnyxUtils.multiSetWithRetry | typeof OnyxUtils.setCollectionWithRetry | typeof OnyxUtils.mergeCollectionWithPatches | typeof OnyxUtils.partialSetCollection;
314
330
  /**
315
331
  * Represents the options used in `Onyx.init()` method.
316
332
  */
@@ -368,4 +384,4 @@ type MixedOperationsQueue = {
368
384
  mergeReplaceNullPatches: MultiMergeReplaceNullPatches;
369
385
  set: OnyxInputKeyValueMapping;
370
386
  };
371
- export type { BaseConnectOptions, Collection, CollectionConnectCallback, CollectionConnectOptions, CollectionKey, CollectionKeyBase, ConnectOptions, CustomTypeOptions, DeepRecord, DefaultConnectCallback, DefaultConnectOptions, ExtractOnyxCollectionValue, GenericFunction, InitOptions, Key, KeyValueMapping, CallbackToStateMapping, NonNull, NonUndefined, OnyxInputKeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxInputValue, OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, OnyxMergeCollectionInput, OnyxSetCollectionInput, OnyxMethod, OnyxMethodMap, OnyxUpdate, OnyxValue, Selector, SetOptions, MultiMergeReplaceNullPatches, MixedOperationsQueue, };
387
+ export type { BaseConnectOptions, Collection, CollectionConnectCallback, CollectionConnectOptions, CollectionKey, CollectionKeyBase, ConnectOptions, CustomTypeOptions, DeepRecord, DefaultConnectCallback, DefaultConnectOptions, ExtractOnyxCollectionValue, GenericFunction, InitOptions, Key, KeyValueMapping, CallbackToStateMapping, NonNull, NonUndefined, OnyxInputKeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxInputValue, OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, OnyxMergeCollectionInput, OnyxSetCollectionInput, OnyxMethod, OnyxMethodMap, OnyxUpdate, OnyxValue, Selector, SetOptions, SetParams, SetCollectionParams, MergeCollectionWithPatchesParams, MultiMergeReplaceNullPatches, MixedOperationsQueue, RetriableOnyxOperation, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.11",
3
+ "version": "3.0.12",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",