react-native-onyx 3.0.99 → 3.0.100

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
@@ -302,7 +302,7 @@ function merge(key, changes) {
302
302
  * @param collection Object collection keyed by individual collection member keys and values
303
303
  */
304
304
  function mergeCollection(collectionKey, collection) {
305
- return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection, isProcessingCollectionUpdate: true }));
305
+ return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection }));
306
306
  }
307
307
  /**
308
308
  * Clear out all the data in the store
@@ -516,7 +516,6 @@ function update(data) {
516
516
  collectionKey,
517
517
  collection: batchedCollectionUpdates.merge,
518
518
  mergeReplaceNullPatches: batchedCollectionUpdates.mergeReplaceNullPatches,
519
- isProcessingCollectionUpdate: true,
520
519
  }));
521
520
  }
522
521
  if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) {
@@ -15,6 +15,11 @@ declare const METHOD: {
15
15
  /** Test-only: clears the disk-pressure log throttle so each test observes its own alert. */
16
16
  declare function resetDiskPressureLogThrottle(): void;
17
17
  type OnyxMethod = ValueOf<typeof METHOD>;
18
+ /** Result of `prepareKeyValuePairsForStorage`: pairs to write and keys whose `null` value marks them for removal. */
19
+ type PreparedKeyValuePairs = {
20
+ pairs: StorageKeyValuePair[];
21
+ keysToRemove: OnyxKey[];
22
+ };
18
23
  declare function getSnapshotKey(): OnyxKey | null;
19
24
  /**
20
25
  * Getter - returns the merge queue.
@@ -121,7 +126,7 @@ declare function keysChanged<TKey extends CollectionKeyBase>(collectionKey: TKey
121
126
  /**
122
127
  * When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks
123
128
  */
124
- declare function keyChanged<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, canUpdateSubscriber?: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean, isProcessingCollectionUpdate?: boolean): void;
129
+ declare function keyChanged<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, canUpdateSubscriber?: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean): void;
125
130
  /**
126
131
  * Sends the data obtained from the keys to the connection.
127
132
  */
@@ -133,7 +138,7 @@ declare function getCollectionDataAndSendAsObject<TKey extends OnyxKey>(matching
133
138
  /**
134
139
  * Remove a key from Onyx and update the subscribers
135
140
  */
136
- declare function remove<TKey extends OnyxKey>(key: TKey, isProcessingCollectionUpdate?: boolean): Promise<void>;
141
+ declare function remove<TKey extends OnyxKey>(key: TKey): Promise<void>;
137
142
  declare function reportStorageQuota(error?: Error): Promise<void>;
138
143
  /**
139
144
  * Handles storage operation failures based on the error class (see lib/storage/errors.ts).
@@ -160,11 +165,10 @@ declare function hasPendingMergeForKey(key: OnyxKey): boolean;
160
165
  /**
161
166
  * Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
162
167
  * This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
163
- * to an array of key-value pairs in the above format and removes key-value pairs that are being set to null
164
- *
165
- * @return an array of key - value pairs <[key, value]>
168
+ * to an array of key-value pairs in the above format, and collects the keys of null values into
169
+ * `keysToRemove` for the caller to delete as one batch (cache drop + notification + batched storage removal).
166
170
  */
167
- declare function prepareKeyValuePairsForStorage(data: Record<OnyxKey, OnyxInput<OnyxKey>>, shouldRemoveNestedNulls?: boolean, replaceNullPatches?: MultiMergeReplaceNullPatches, isProcessingCollectionUpdate?: boolean): StorageKeyValuePair[];
171
+ declare function prepareKeyValuePairsForStorage(data: Record<OnyxKey, OnyxInput<OnyxKey>>, shouldRemoveNestedNulls?: boolean, replaceNullPatches?: MultiMergeReplaceNullPatches): PreparedKeyValuePairs;
168
172
  /**
169
173
  * Merges an array of changes with an existing value or creates a single change.
170
174
  *
@@ -249,10 +253,9 @@ declare function setCollectionWithRetry<TKey extends CollectionKeyBase>({ collec
249
253
  * @param params.collection Object collection keyed by individual collection member keys and values
250
254
  * @param params.mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
251
255
  * tuples that we'll use to replace the nested objects of that collection member record with something else.
252
- * @param params.isProcessingCollectionUpdate whether this is part of a collection update operation.
253
256
  * @param retryAttempt retry attempt
254
257
  */
255
- declare function mergeCollectionWithPatches<TKey extends CollectionKeyBase>({ collectionKey, collection, mergeReplaceNullPatches, isProcessingCollectionUpdate }: MergeCollectionWithPatchesParams<TKey>, retryAttempt?: number): Promise<void>;
258
+ declare function mergeCollectionWithPatches<TKey extends CollectionKeyBase>({ collectionKey, collection, mergeReplaceNullPatches }: MergeCollectionWithPatchesParams<TKey>, retryAttempt?: number): Promise<void>;
256
259
  /**
257
260
  * Sets keys in a collection by replacing all targeted collection members with new values.
258
261
  * Any existing collection members not included in the new data will not be removed.
package/dist/OnyxUtils.js CHANGED
@@ -522,7 +522,7 @@ function keysChanged(collectionKey, partialCollection, partialPreviousCollection
522
522
  /**
523
523
  * When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks
524
524
  */
525
- function keyChanged(key, value, canUpdateSubscriber = () => true, isProcessingCollectionUpdate = false) {
525
+ function keyChanged(key, value, canUpdateSubscriber = () => true) {
526
526
  var _a, _b;
527
527
  // Add or remove this key from the recentlyAccessedKeys list
528
528
  if (value !== null && value !== undefined) {
@@ -562,11 +562,6 @@ function keyChanged(key, value, canUpdateSubscriber = () => true, isProcessingCo
562
562
  continue;
563
563
  }
564
564
  if (OnyxKeys_1.default.isCollectionKey(subscriber.key)) {
565
- // Skip individual key changes during collection updates to prevent duplicate
566
- // callbacks - the collection update will handle this properly.
567
- if (isProcessingCollectionUpdate) {
568
- continue;
569
- }
570
565
  // Cache once per dispatch to ensure all subscribers see a consistent snapshot
571
566
  // even if a previous callback synchronously wrote to the same collection.
572
567
  let cachedCollection = cachedCollections[subscriber.key];
@@ -637,9 +632,9 @@ function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
637
632
  /**
638
633
  * Remove a key from Onyx and update the subscribers
639
634
  */
640
- function remove(key, isProcessingCollectionUpdate) {
635
+ function remove(key) {
641
636
  OnyxCache_1.default.drop(key);
642
- keyChanged(key, undefined, undefined, isProcessingCollectionUpdate);
637
+ keyChanged(key, undefined);
643
638
  if (OnyxKeys_1.default.isRamOnlyKey(key)) {
644
639
  return Promise.resolve();
645
640
  }
@@ -770,15 +765,15 @@ function hasPendingMergeForKey(key) {
770
765
  /**
771
766
  * Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
772
767
  * This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
773
- * to an array of key-value pairs in the above format and removes key-value pairs that are being set to null
774
- *
775
- * @return an array of key - value pairs <[key, value]>
768
+ * to an array of key-value pairs in the above format, and collects the keys of null values into
769
+ * `keysToRemove` for the caller to delete as one batch (cache drop + notification + batched storage removal).
776
770
  */
777
- function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls, replaceNullPatches, isProcessingCollectionUpdate) {
771
+ function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls, replaceNullPatches) {
778
772
  const pairs = [];
773
+ const keysToRemove = [];
779
774
  for (const [key, value] of Object.entries(data)) {
780
775
  if (value === null) {
781
- remove(key, isProcessingCollectionUpdate);
776
+ keysToRemove.push(key);
782
777
  continue;
783
778
  }
784
779
  const valueWithoutNestedNullValues = (shouldRemoveNestedNulls !== null && shouldRemoveNestedNulls !== void 0 ? shouldRemoveNestedNulls : true) ? utils_1.default.removeNestedNullValues(value) : value;
@@ -786,7 +781,7 @@ function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls, replaceNu
786
781
  pairs.push([key, valueWithoutNestedNullValues, replaceNullPatches === null || replaceNullPatches === void 0 ? void 0 : replaceNullPatches[key]]);
787
782
  }
788
783
  }
789
- return pairs;
784
+ return { pairs, keysToRemove };
790
785
  }
791
786
  /**
792
787
  * Merges an array of changes with an existing value or creates a single change.
@@ -1172,7 +1167,11 @@ function multiSetWithRetry(data, retryAttempt) {
1172
1167
  return result;
1173
1168
  }, {});
1174
1169
  }
1175
- const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(newData, true);
1170
+ const { pairs: keyValuePairsToSet, keysToRemove: removalCandidates } = OnyxUtils.prepareKeyValuePairsForStorage(newData, true);
1171
+ // Removals of keys that are neither cached nor persisted are no-ops and skipped. When the key
1172
+ // index has not been loaded yet (empty set), keep the removal to be safe.
1173
+ const persistedKeys = OnyxCache_1.default.getAllKeys();
1174
+ const keysToRemove = removalCandidates.filter((key) => OnyxCache_1.default.get(key) !== undefined || persistedKeys.size === 0 || persistedKeys.has(key));
1176
1175
  // Group collection members by their parent collection key so each collection can be notified
1177
1176
  // via a single batched keysChanged() call instead of one keyChanged() per member. For each
1178
1177
  // collection, `partial` holds the new values being set and `previous` holds the cached values
@@ -1210,6 +1209,26 @@ function multiSetWithRetry(data, retryAttempt) {
1210
1209
  }
1211
1210
  }
1212
1211
  }
1212
+ // Null keys join the same per-collection batches (as undefined) and are deleted from storage
1213
+ // in one batched call below, so cross-tab sync raises a single event instead of one per key.
1214
+ for (const key of keysToRemove) {
1215
+ const previousValue = OnyxCache_1.default.get(key);
1216
+ OnyxCache_1.default.drop(key);
1217
+ const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
1218
+ if (collectionKey && OnyxKeys_1.default.isCollectionMemberKey(collectionKey, key)) {
1219
+ let batch = collectionBatches.get(collectionKey);
1220
+ if (!batch) {
1221
+ batch = { partial: {}, previous: {} };
1222
+ collectionBatches.set(collectionKey, batch);
1223
+ }
1224
+ batch.partial[key] = undefined;
1225
+ batch.previous[key] = previousValue;
1226
+ }
1227
+ else if (!retryAttempt) {
1228
+ // Skip subscriber notification on retry — already notified on attempt 0.
1229
+ keyChanged(key, undefined);
1230
+ }
1231
+ }
1213
1232
  // One keysChanged() per collection — fires each collection-level subscriber once and lets
1214
1233
  // keysChanged() internally decide which individual member subscribers need notification.
1215
1234
  // Skip on retry — already notified on attempt 0 (see same-reason comment above).
@@ -1223,8 +1242,14 @@ function multiSetWithRetry(data, retryAttempt) {
1223
1242
  // Filter out the RAM-only key value pairs, as they should not be saved to storage
1224
1243
  return !OnyxKeys_1.default.isRamOnlyKey(key);
1225
1244
  });
1245
+ const keysToRemoveFromStorage = keysToRemove.filter((key) => !OnyxKeys_1.default.isRamOnlyKey(key));
1226
1246
  const inFlightKeys = new Set(keyValuePairsToSet.map(([key]) => key));
1227
- return storage_1.default.multiSet(keyValuePairsToStore)
1247
+ // A failed removal is logged, not retried — keysToRemove cannot be re-derived after the cache update.
1248
+ const storagePromises = [storage_1.default.multiSet(keyValuePairsToStore)];
1249
+ if (keysToRemoveFromStorage.length > 0) {
1250
+ storagePromises.push(storage_1.default.removeItems(keysToRemoveFromStorage).catch((error) => Logger.logAlert(`multiSet failed to remove keys from storage. Error: ${error}`)));
1251
+ }
1252
+ return Promise.all(storagePromises)
1228
1253
  .then(() => StorageCircuitBreaker_1.default.recordWriteSuccess())
1229
1254
  .catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt, inFlightKeys))
1230
1255
  .then(() => {
@@ -1278,14 +1303,21 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
1278
1303
  }
1279
1304
  mutableCollection[key] = null;
1280
1305
  }
1281
- const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
1306
+ const { pairs: keyValuePairs, keysToRemove: removalCandidates } = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true);
1307
+ // Removals of keys that are neither cached nor persisted are no-ops and skipped.
1308
+ const keysToRemove = removalCandidates.filter((key) => OnyxCache_1.default.get(key) !== undefined || persistedKeys.has(key));
1309
+ // Snapshot before cache mutations so keysChanged() can diff removed members.
1282
1310
  const previousCollection = OnyxUtils.getCachedCollection(collectionKey);
1283
1311
  for (const [key, value] of keyValuePairs)
1284
1312
  OnyxCache_1.default.set(key, value);
1313
+ for (const key of keysToRemove)
1314
+ OnyxCache_1.default.drop(key);
1285
1315
  // Skip subscriber notification on retry — already notified on attempt 0.
1286
1316
  // Collection-root subscribers re-fire on every keysChanged by contract.
1287
1317
  if (!retryAttempt) {
1288
- keysChanged(collectionKey, mutableCollection, previousCollection);
1318
+ // Removed members are notified as undefined, matching mergeCollection/multiSet.
1319
+ const partialForNotify = Object.fromEntries(Object.entries(mutableCollection).map(([key, value]) => [key, value !== null && value !== void 0 ? value : undefined]));
1320
+ keysChanged(collectionKey, partialForNotify, previousCollection);
1289
1321
  }
1290
1322
  // RAM-only keys are not supposed to be saved to storage
1291
1323
  if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) {
@@ -1293,7 +1325,13 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
1293
1325
  return;
1294
1326
  }
1295
1327
  const inFlightKeys = new Set(keyValuePairs.map(([key]) => key));
1296
- return storage_1.default.multiSet(keyValuePairs)
1328
+ // One batched removal = one cross-tab sync event instead of one per key. A failed removal is
1329
+ // logged, not retried — keysToRemove cannot be re-derived after the cache update.
1330
+ const storagePromises = [storage_1.default.multiSet(keyValuePairs)];
1331
+ if (keysToRemove.length > 0) {
1332
+ storagePromises.push(storage_1.default.removeItems(keysToRemove).catch((error) => Logger.logAlert(`setCollection failed to remove keys from storage. Error: ${error}`)));
1333
+ }
1334
+ return Promise.all(storagePromises)
1297
1335
  .then(() => StorageCircuitBreaker_1.default.recordWriteSuccess())
1298
1336
  .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, { collectionKey, collection }, retryAttempt, inFlightKeys))
1299
1337
  .then(() => {
@@ -1311,10 +1349,9 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
1311
1349
  * @param params.collection Object collection keyed by individual collection member keys and values
1312
1350
  * @param params.mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
1313
1351
  * tuples that we'll use to replace the nested objects of that collection member record with something else.
1314
- * @param params.isProcessingCollectionUpdate whether this is part of a collection update operation.
1315
1352
  * @param retryAttempt retry attempt
1316
1353
  */
1317
- function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNullPatches, isProcessingCollectionUpdate = false }, retryAttempt) {
1354
+ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNullPatches }, retryAttempt) {
1318
1355
  if (!isValidNonEmptyCollectionForMerge(collection)) {
1319
1356
  Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
1320
1357
  return Promise.resolve();
@@ -1344,14 +1381,30 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1344
1381
  resultCollectionKeys = Object.keys(resultCollection);
1345
1382
  return getAllKeys()
1346
1383
  .then((persistedKeys) => {
1347
- // Split to keys that exist in storage and keys that don't
1384
+ // Split to keys that exist in storage and keys that don't. Null members are collected
1385
+ // for one batched removal below; nulls that are neither cached nor persisted are no-ops and skipped.
1386
+ const keysToRemove = [];
1348
1387
  const keys = resultCollectionKeys.filter((key) => {
1349
1388
  if (resultCollection[key] === null) {
1350
- remove(key, isProcessingCollectionUpdate);
1389
+ if (OnyxCache_1.default.get(key) !== undefined || persistedKeys.has(key)) {
1390
+ keysToRemove.push(key);
1391
+ }
1351
1392
  return false;
1352
1393
  }
1353
1394
  return true;
1354
1395
  });
1396
+ // Drop removed members before the pre-warm await below, so a concurrent write to one of
1397
+ // these keys during the pre-warm is not wiped out by a late drop.
1398
+ const removedPreviousValues = {};
1399
+ for (const key of keysToRemove) {
1400
+ removedPreviousValues[key] = OnyxCache_1.default.get(key);
1401
+ OnyxCache_1.default.drop(key);
1402
+ }
1403
+ // One batched removal = one cross-tab sync event instead of one per key. Issued at drop time
1404
+ // so a concurrent later write to a removed key persists after the removal.
1405
+ const removalPromise = !OnyxKeys_1.default.isRamOnlyKey(collectionKey) && keysToRemove.length > 0
1406
+ ? storage_1.default.removeItems(keysToRemove).catch((error) => Logger.logAlert(`mergeCollection failed to remove keys from storage. Error: ${error}`))
1407
+ : undefined;
1355
1408
  const existingKeys = keys.filter((key) => persistedKeys.has(key));
1356
1409
  const cachedCollectionForExistingKeys = getCachedCollection(collectionKey, existingKeys);
1357
1410
  const existingKeyCollection = existingKeys.reduce((obj, key) => {
@@ -1380,10 +1433,10 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1380
1433
  // When (multi-)merging the values with the existing values in storage,
1381
1434
  // we don't want to remove nested null values from the data that we pass to the storage layer,
1382
1435
  // because the storage layer uses them to remove nested keys from storage natively.
1383
- const keyValuePairsForExistingCollection = prepareKeyValuePairsForStorage(existingKeyCollection, false, mergeReplaceNullPatches);
1436
+ const { pairs: keyValuePairsForExistingCollection } = prepareKeyValuePairsForStorage(existingKeyCollection, false, mergeReplaceNullPatches);
1384
1437
  // We can safely remove nested null values when using (multi-)set,
1385
1438
  // because we will simply overwrite the existing values in storage.
1386
- const keyValuePairsForNewCollection = prepareKeyValuePairsForStorage(newCollection, true);
1439
+ const { pairs: keyValuePairsForNewCollection } = prepareKeyValuePairsForStorage(newCollection, true);
1387
1440
  // finalMergedCollection contains all the keys that were merged, without the keys of incompatible updates
1388
1441
  const finalMergedCollection = Object.assign(Object.assign({}, existingKeyCollection), newCollection);
1389
1442
  // Pre-warm cache for cache-miss existingKeys so cache.merge() merges the new delta into
@@ -1408,9 +1461,16 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1408
1461
  // Skip subscriber notification on retry — already notified on attempt 0.
1409
1462
  // Collection-root subscribers re-fire on every keysChanged by contract.
1410
1463
  if (!retryAttempt) {
1411
- keysChanged(collectionKey, finalMergedCollection, previousCollection);
1464
+ const partialForNotify = keysToRemove.length > 0 ? Object.assign(Object.assign({}, finalMergedCollection), Object.fromEntries(keysToRemove.map((key) => [key, undefined]))) : finalMergedCollection;
1465
+ const previousForNotify = keysToRemove.length > 0 ? Object.assign(Object.assign({}, previousCollection), removedPreviousValues) : previousCollection;
1466
+ if (Object.keys(partialForNotify).length > 0) {
1467
+ keysChanged(collectionKey, partialForNotify, previousForNotify);
1468
+ }
1412
1469
  }
1413
1470
  const promises = [];
1471
+ if (removalPromise) {
1472
+ promises.push(removalPromise);
1473
+ }
1414
1474
  // New keys go through multiSet and existing keys through multiMerge. multiMerge on a
1415
1475
  // missing key stores the value just like multiSet across all backends; splitting them lets
1416
1476
  // multiSet strip nested nulls (the merge layer keeps them to delete nested storage keys).
@@ -1429,7 +1489,6 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1429
1489
  collectionKey,
1430
1490
  collection: resultCollection,
1431
1491
  mergeReplaceNullPatches,
1432
- isProcessingCollectionUpdate,
1433
1492
  }, retryAttempt, inFlightKeys))
1434
1493
  .then(() => {
1435
1494
  sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection);
@@ -1476,21 +1535,34 @@ function partialSetCollection({ collectionKey, collection }, retryAttempt) {
1476
1535
  return getAllKeys().then((persistedKeys) => {
1477
1536
  const mutableCollection = Object.assign({}, resultCollection);
1478
1537
  const existingKeys = resultCollectionKeys.filter((key) => persistedKeys.has(key));
1538
+ const { pairs: keyValuePairs, keysToRemove: removalCandidates } = prepareKeyValuePairsForStorage(mutableCollection, true);
1539
+ // Removals of keys that are neither cached nor persisted are no-ops and skipped.
1540
+ const keysToRemove = removalCandidates.filter((key) => OnyxCache_1.default.get(key) !== undefined || persistedKeys.has(key));
1541
+ // Snapshot before cache mutations so keysChanged() can diff removed members.
1479
1542
  const previousCollection = getCachedCollection(collectionKey, existingKeys);
1480
- const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
1481
1543
  for (const [key, value] of keyValuePairs)
1482
1544
  OnyxCache_1.default.set(key, value);
1545
+ for (const key of keysToRemove)
1546
+ OnyxCache_1.default.drop(key);
1483
1547
  // Skip subscriber notification on retry — already notified on attempt 0.
1484
1548
  // Collection-root subscribers re-fire on every keysChanged by contract.
1485
1549
  if (!retryAttempt) {
1486
- keysChanged(collectionKey, mutableCollection, previousCollection);
1550
+ // Removed members are notified as undefined, matching mergeCollection/multiSet.
1551
+ const partialForNotify = Object.fromEntries(Object.entries(mutableCollection).map(([key, value]) => [key, value !== null && value !== void 0 ? value : undefined]));
1552
+ keysChanged(collectionKey, partialForNotify, previousCollection);
1487
1553
  }
1488
1554
  if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) {
1489
1555
  sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
1490
1556
  return;
1491
1557
  }
1492
1558
  const inFlightKeys = new Set(keyValuePairs.map(([key]) => key));
1493
- return storage_1.default.multiSet(keyValuePairs)
1559
+ // One batched removal = one cross-tab sync event instead of one per key. A failed removal is
1560
+ // logged, not retried — keysToRemove cannot be re-derived after the cache update.
1561
+ const storagePromises = [storage_1.default.multiSet(keyValuePairs)];
1562
+ if (keysToRemove.length > 0) {
1563
+ storagePromises.push(storage_1.default.removeItems(keysToRemove).catch((error) => Logger.logAlert(`setCollection failed to remove keys from storage. Error: ${error}`)));
1564
+ }
1565
+ return Promise.all(storagePromises)
1494
1566
  .then(() => StorageCircuitBreaker_1.default.recordWriteSuccess())
1495
1567
  .catch((error) => retryOperation(error, partialSetCollection, { collectionKey, collection }, retryAttempt, inFlightKeys))
1496
1568
  .then(() => {
@@ -126,6 +126,9 @@ const InstanceSync = {
126
126
  */
127
127
  init: (onStorageKeysChanged, store) => {
128
128
  storage = store;
129
+ // Coalesce storage events into one dispatch per tick: a per-key sender would otherwise re-run
130
+ // the whole notification pipeline once per key and can flood the receiving tab into unresponsiveness.
131
+ let pendingSyncKeys = null;
129
132
  // This listener will only be triggered by events coming from other tabs
130
133
  global.addEventListener('storage', (event) => {
131
134
  // Ignore events that don't originate from the SYNC_ONYX logic
@@ -133,7 +136,18 @@ const InstanceSync = {
133
136
  return;
134
137
  }
135
138
  const onyxKeys = parseSyncOnyxStorageEventValue(event.newValue);
136
- storage.multiGet(onyxKeys).then((pairs) => onStorageKeysChanged(pairs));
139
+ if (pendingSyncKeys) {
140
+ for (const onyxKey of onyxKeys) {
141
+ pendingSyncKeys.add(onyxKey);
142
+ }
143
+ return;
144
+ }
145
+ pendingSyncKeys = new Set(onyxKeys);
146
+ setTimeout(() => {
147
+ const keys = Array.from(pendingSyncKeys !== null && pendingSyncKeys !== void 0 ? pendingSyncKeys : []);
148
+ pendingSyncKeys = null;
149
+ storage.multiGet(keys).then((pairs) => onStorageKeysChanged(pairs));
150
+ }, 0);
137
151
  });
138
152
  },
139
153
  setItem: raiseStorageSyncEvent,
package/dist/types.d.ts CHANGED
@@ -303,7 +303,6 @@ type MergeCollectionWithPatchesParams<TKey extends CollectionKeyBase> = {
303
303
  collectionKey: TKey;
304
304
  collection: OnyxMergeCollectionInput<TKey>;
305
305
  mergeReplaceNullPatches?: MultiMergeReplaceNullPatches;
306
- isProcessingCollectionUpdate?: boolean;
307
306
  };
308
307
  type RetriableOnyxOperation = typeof OnyxUtils.setWithRetry | typeof OnyxUtils.multiSetWithRetry | typeof OnyxUtils.setCollectionWithRetry | typeof OnyxUtils.mergeCollectionWithPatches | typeof OnyxUtils.partialSetCollection;
309
308
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.99",
3
+ "version": "3.0.100",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",