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 +1 -2
- package/dist/OnyxUtils.d.ts +11 -8
- package/dist/OnyxUtils.js +102 -30
- package/dist/storage/InstanceSync/index.web.js +15 -1
- package/dist/types.d.ts +0 -1
- package/package.json +1 -1
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
|
|
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)) {
|
package/dist/OnyxUtils.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
635
|
+
function remove(key) {
|
|
641
636
|
OnyxCache_1.default.drop(key);
|
|
642
|
-
keyChanged(key, undefined
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|