react-native-onyx 3.0.115 → 3.0.117

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/OnyxUtils.js CHANGED
@@ -37,13 +37,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.clearOnyxUtilsInternals = clearOnyxUtilsInternals;
40
- const fast_equals_1 = require("fast-equals");
40
+ exports.resetDeferredInitTask = resetDeferredInitTask;
41
41
  const underscore_1 = __importDefault(require("underscore"));
42
42
  const DevTools_1 = __importDefault(require("./DevTools"));
43
43
  const Logger = __importStar(require("./Logger"));
44
44
  const OnyxCache_1 = __importStar(require("./OnyxCache"));
45
45
  const OnyxKeys_1 = __importDefault(require("./OnyxKeys"));
46
+ const PendingWrites_1 = __importDefault(require("./PendingWrites"));
46
47
  const StorageCircuitBreaker_1 = __importDefault(require("./StorageCircuitBreaker"));
48
+ const OnyxSubscriptionManager_1 = __importDefault(require("./OnyxSubscriptionManager"));
47
49
  const storage_1 = __importDefault(require("./storage"));
48
50
  const errors_1 = require("./storage/errors");
49
51
  const utils_1 = __importDefault(require("./utils"));
@@ -68,25 +70,24 @@ let lastDiskPressureLogTime = 0;
68
70
  function resetDiskPressureLogThrottle() {
69
71
  lastDiskPressureLogTime = 0;
70
72
  }
71
- function formatCaughtError(error) {
72
- return error instanceof Error ? error.toString() : String(error);
73
- }
74
73
  // Key/value store of Onyx key and arrays of values to merge
75
74
  let mergeQueue = {};
76
75
  let mergeQueuePromise = {};
77
- // Holds a mapping of all the React components that want their state subscribed to a store key
78
- let callbackToStateMapping = {};
79
- // Holds a mapping of the connected key to the subscriptionID for faster lookups
80
- let onyxKeyToSubscriptionIDs = new Map();
76
+ const mergeQueuesWithStaleRead = new WeakSet();
81
77
  // Optional user-provided key value states set when Onyx initializes or clears
82
78
  let defaultKeyStates = {};
83
- // Used for comparison with a new update to avoid invoking the Onyx.connect callback with the same data.
84
- let lastConnectionCallbackData = new Map();
85
79
  let snapshotKey = null;
86
- // Keeps track of the last subscriptionID that was used so we can keep incrementing it
87
- let lastSubscriptionID = 0;
88
- // Connections can be made before `Onyx.init`. They would wait for this task before resolving
89
- const deferredInitTask = (0, createDeferredTask_1.default)();
80
+ // Connections can be made before `Onyx.init`. They would wait for this task before resolving.
81
+ let deferredInitTask = (0, createDeferredTask_1.default)();
82
+ /**
83
+ * Sentinel for "nothing delivered yet" in `connect()`'s per-subscription dedup. A Symbol
84
+ * can't collide with any real Onyx value, so the first `Object.is` check never matches and
85
+ * the initial fire runs even when a key's genuine first value is `undefined`. It only needs
86
+ * to be distinct from real values, not unique per subscription, so one module-level instance
87
+ * is reused by every connection.
88
+ */
89
+ // eslint-disable-next-line rulesdir/no-negated-variables
90
+ const NOT_DELIVERED = Symbol('NOT_DELIVERED');
90
91
  // Collection member IDs that Onyx should silently ignore across all operations — reads, writes, cache, and subscriber
91
92
  // notifications. This is used to filter out keys formed from invalid/default IDs (e.g. "-1", "0",
92
93
  // "undefined", "null", "NaN") that can appear when an ID variable is accidentally coerced to string.
@@ -348,31 +349,6 @@ function multiGet(keys) {
348
349
  function tupleGet(keys) {
349
350
  return Promise.all(keys.map((key) => get(key)));
350
351
  }
351
- /**
352
- * Stores a subscription ID associated with a given key.
353
- *
354
- * @param subscriptionID - A subscription ID of the subscriber.
355
- * @param key - A key that the subscriber is subscribed to.
356
- */
357
- function storeKeyBySubscriptions(key, subscriptionID) {
358
- if (!onyxKeyToSubscriptionIDs.has(key)) {
359
- onyxKeyToSubscriptionIDs.set(key, []);
360
- }
361
- onyxKeyToSubscriptionIDs.get(key).push(subscriptionID);
362
- }
363
- /**
364
- * Deletes a subscription ID associated with its corresponding key.
365
- *
366
- * @param subscriptionID - The subscription ID to be deleted.
367
- */
368
- function deleteKeyBySubscriptions(subscriptionID) {
369
- const subscriber = callbackToStateMapping[subscriptionID];
370
- if (subscriber && onyxKeyToSubscriptionIDs.has(subscriber.key)) {
371
- const updatedSubscriptionsIDs = onyxKeyToSubscriptionIDs.get(subscriber.key).filter((id) => id !== subscriptionID);
372
- onyxKeyToSubscriptionIDs.set(subscriber.key, updatedSubscriptionsIDs);
373
- }
374
- lastConnectionCallbackData.delete(subscriptionID);
375
- }
376
352
  /** Returns current key names stored in persisted storage */
377
353
  function getAllKeys() {
378
354
  // When we've already read stored keys, resolve right away
@@ -395,28 +371,6 @@ function getAllKeys() {
395
371
  });
396
372
  return OnyxCache_1.default.captureTask(OnyxCache_1.TASK.GET_ALL_KEYS, promise);
397
373
  }
398
- /**
399
- * Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined.
400
- * If the requested key is a collection, it will return an object with all the collection members.
401
- */
402
- function tryGetCachedValue(key) {
403
- let val = OnyxCache_1.default.get(key);
404
- if (OnyxKeys_1.default.isCollectionKey(key)) {
405
- const collectionData = OnyxCache_1.default.getCollectionData(key);
406
- if (collectionData !== undefined) {
407
- val = collectionData;
408
- }
409
- else {
410
- // If we haven't loaded all keys yet, we can't determine if the collection exists
411
- if (OnyxCache_1.default.getAllKeys().size === 0) {
412
- return;
413
- }
414
- // Set an empty collection object for collections that exist but have no data
415
- val = {};
416
- }
417
- }
418
- return val;
419
- }
420
374
  function getCachedCollection(collectionKey, collectionMemberKeys) {
421
375
  // Use optimized collection data retrieval when cache is populated
422
376
  const collectionData = OnyxCache_1.default.getCollectionData(collectionKey);
@@ -457,187 +411,42 @@ function getCachedCollection(collectionKey, collectionMemberKeys) {
457
411
  return collection;
458
412
  }
459
413
  /**
460
- * When a collection of keys change, search for any callbacks matching the collection key and trigger those callbacks
461
- */
462
- function keysChanged(collectionKey, partialCollection, partialPreviousCollection) {
463
- var _a;
464
- const cachedCollection = getCachedCollection(collectionKey);
465
- const previousCollection = partialPreviousCollection !== null && partialPreviousCollection !== void 0 ? partialPreviousCollection : {};
466
- const changedMemberKeys = Object.keys(partialCollection !== null && partialCollection !== void 0 ? partialCollection : {});
467
- // Add or remove the keys from the recentlyAccessedKeys list
468
- for (const memberKey of changedMemberKeys) {
469
- const value = partialCollection === null || partialCollection === void 0 ? void 0 : partialCollection[memberKey];
470
- if (value !== null && value !== undefined) {
471
- OnyxCache_1.default.addLastAccessedKey(memberKey, false);
472
- }
473
- else {
474
- OnyxCache_1.default.removeLastAccessedKey(memberKey);
475
- }
476
- }
477
- // Use indexed lookup instead of scanning all subscribers.
478
- // We need subscribers for: (1) the collection key itself, and (2) individual changed member keys.
479
- const collectionSubscriberIDs = (_a = onyxKeyToSubscriptionIDs.get(collectionKey)) !== null && _a !== void 0 ? _a : [];
480
- const memberSubscriberIDs = [];
481
- for (const memberKey of changedMemberKeys) {
482
- const ids = onyxKeyToSubscriptionIDs.get(memberKey);
483
- if (ids) {
484
- for (const id of ids) {
485
- memberSubscriberIDs.push(id);
486
- }
487
- }
488
- }
489
- // Notify collection-level subscribers
490
- for (const subID of collectionSubscriberIDs) {
491
- const subscriber = callbackToStateMapping[subID];
492
- if (!subscriber || typeof subscriber.callback !== 'function') {
493
- continue;
494
- }
495
- try {
496
- lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection, matchedKey: subscriber.key });
497
- subscriber.callback(cachedCollection, subscriber.key);
498
- }
499
- catch (error) {
500
- Logger.logAlert(`[OnyxUtils.keysChanged] Subscriber callback threw an error for key '${collectionKey}': ${formatCaughtError(error)}`);
501
- }
502
- }
503
- // Notify member-level subscribers (e.g. subscribed to `report_123`)
504
- for (const subID of memberSubscriberIDs) {
505
- const subscriber = callbackToStateMapping[subID];
506
- if (!subscriber || typeof subscriber.callback !== 'function') {
507
- continue;
508
- }
509
- if (cachedCollection[subscriber.key] === previousCollection[subscriber.key]) {
510
- continue;
511
- }
512
- try {
513
- const subscriberCallback = subscriber.callback;
514
- subscriberCallback(cachedCollection[subscriber.key], subscriber.key);
515
- lastConnectionCallbackData.set(subscriber.subscriptionID, {
516
- value: cachedCollection[subscriber.key],
517
- matchedKey: subscriber.key,
518
- });
519
- }
520
- catch (error) {
521
- Logger.logAlert(`[OnyxUtils.keysChanged] Subscriber callback threw an error for key '${collectionKey}': ${formatCaughtError(error)}`);
522
- }
523
- }
524
- }
525
- /**
526
- * When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks
414
+ * Notify subscribers of a single-key write. Wrapper over `onyxSubscriptionManager.notifyKey()`
415
+ * that also performs LRU bookkeeping for eviction.
527
416
  */
528
- function keyChanged(key, value, canUpdateSubscriber = () => true) {
529
- var _a, _b;
530
- // Add or remove this key from the recentlyAccessedKeys list
417
+ function notifyKey(key, value) {
531
418
  if (value !== null && value !== undefined) {
532
419
  OnyxCache_1.default.addLastAccessedKey(key, OnyxKeys_1.default.isCollectionKey(key));
533
420
  }
534
421
  else {
535
422
  OnyxCache_1.default.removeLastAccessedKey(key);
536
423
  }
537
- // We get the subscribers interested in the key that has just changed. If the subscriber's key is a collection key then we will
538
- // notify them if the key that changed is a collection member. Or if it is a regular key notify them when there is an exact match.
539
- // Given the amount of times this function is called we need to make sure we are not iterating over all subscribers every time. On the other hand, we don't need to
540
- // do the same in keysChanged, because we only call that function when a collection key changes, and it doesn't happen that often.
541
- // For performance reason, we look for the given key and later if don't find it we look for the collection key, instead of checking if it is a collection key first.
542
- let stateMappingKeys = (_a = onyxKeyToSubscriptionIDs.get(key)) !== null && _a !== void 0 ? _a : [];
543
- const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
544
- if (collectionKey) {
545
- // Getting the collection key from the specific key because only collection keys were stored in the mapping.
546
- stateMappingKeys = [...stateMappingKeys, ...((_b = onyxKeyToSubscriptionIDs.get(collectionKey)) !== null && _b !== void 0 ? _b : [])];
547
- if (stateMappingKeys.length === 0) {
548
- return;
549
- }
550
- }
551
- // Cache the collection snapshot per dispatch so all subscribers to the same collection
552
- // see a consistent view, even if an earlier subscriber's callback synchronously writes
553
- // to the same collection.
554
- const cachedCollections = {};
555
- for (const stateMappingKey of stateMappingKeys) {
556
- const subscriber = callbackToStateMapping[stateMappingKey];
557
- if (!subscriber || !OnyxKeys_1.default.isKeyMatch(subscriber.key, key) || !canUpdateSubscriber(subscriber)) {
558
- continue;
559
- }
560
- // Subscriber is a regular call to connect() and provided a callback
561
- if (typeof subscriber.callback === 'function') {
562
- try {
563
- const lastData = lastConnectionCallbackData.get(subscriber.subscriptionID);
564
- if (lastData && lastData.matchedKey === key && lastData.value === value) {
565
- continue;
566
- }
567
- if (OnyxKeys_1.default.isCollectionKey(subscriber.key)) {
568
- // Cache once per dispatch to ensure all subscribers see a consistent snapshot
569
- // even if a previous callback synchronously wrote to the same collection.
570
- let cachedCollection = cachedCollections[subscriber.key];
571
- if (!cachedCollection) {
572
- cachedCollection = getCachedCollection(subscriber.key);
573
- cachedCollections[subscriber.key] = cachedCollection;
574
- }
575
- lastConnectionCallbackData.set(subscriber.subscriptionID, { value: cachedCollection, matchedKey: subscriber.key });
576
- subscriber.callback(cachedCollection, subscriber.key);
577
- continue;
578
- }
579
- const subscriberCallback = subscriber.callback;
580
- subscriberCallback(value, key);
581
- lastConnectionCallbackData.set(subscriber.subscriptionID, {
582
- value,
583
- matchedKey: key,
584
- });
585
- continue;
586
- }
587
- catch (error) {
588
- Logger.logAlert(`[OnyxUtils.keyChanged] Subscriber callback threw an error for key '${key}': ${formatCaughtError(error)}`);
589
- }
590
- continue;
591
- }
592
- console.error('Warning: Found a matching subscriber to a key that changed, but no callback could be found.');
593
- }
424
+ OnyxSubscriptionManager_1.default.notifyKey(key, value);
594
425
  }
595
426
  /**
596
- * Sends the data obtained from the keys to the connection.
427
+ * Notify subscribers of a batch collection update. Wrapper over
428
+ * `onyxSubscriptionManager.notifyCollection()` that also performs LRU bookkeeping per
429
+ * changed member.
597
430
  */
598
- function sendDataToConnection(mapping, matchedKey) {
599
- var _a;
600
- // If the mapping no longer exists then we should not send any data.
601
- // This means our subscriber was disconnected.
602
- if (!callbackToStateMapping[mapping.subscriptionID]) {
603
- return;
604
- }
605
- // Always read the latest value from cache to avoid stale or duplicate data.
606
- // For collection-root subscribers, read the full collection.
607
- // For individual key subscribers, read just that key's value.
608
- let value;
609
- if (OnyxKeys_1.default.isCollectionKey(mapping.key)) {
610
- const collection = getCachedCollection(mapping.key);
611
- value = Object.keys(collection).length > 0 ? collection : undefined;
612
- }
613
- else {
614
- value = OnyxCache_1.default.get(matchedKey !== null && matchedKey !== void 0 ? matchedKey : mapping.key);
615
- }
616
- // For regular callbacks, we never want to pass null values, but always just undefined if a value is not set in cache or storage.
617
- value = value === null ? undefined : value;
618
- const lastData = lastConnectionCallbackData.get(mapping.subscriptionID);
619
- // If the value has not changed for the same key we do not need to trigger the callback.
620
- // We compare matchedKey to avoid suppressing callbacks for different collection members
621
- // that happen to have shallow-equal values (e.g. during hydration racing with set()).
622
- if (lastData && lastData.matchedKey === matchedKey && (0, fast_equals_1.shallowEqual)(lastData.value, value)) {
623
- return;
431
+ function notifyCollection(collectionKey, partialCollection, partialPreviousCollection) {
432
+ const changedKeys = Object.keys(partialCollection !== null && partialCollection !== void 0 ? partialCollection : {});
433
+ for (const memberKey of changedKeys) {
434
+ const value = partialCollection === null || partialCollection === void 0 ? void 0 : partialCollection[memberKey];
435
+ if (value !== null && value !== undefined) {
436
+ OnyxCache_1.default.addLastAccessedKey(memberKey, false);
437
+ }
438
+ else {
439
+ OnyxCache_1.default.removeLastAccessedKey(memberKey);
440
+ }
624
441
  }
625
- (_a = mapping.callback) === null || _a === void 0 ? void 0 : _a.call(mapping, value, matchedKey);
626
- }
627
- /**
628
- * Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.
629
- */
630
- function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
631
- multiGet(matchingKeys).then(() => {
632
- sendDataToConnection(mapping, mapping.key);
633
- });
442
+ OnyxSubscriptionManager_1.default.notifyCollection(collectionKey, partialCollection, partialPreviousCollection);
634
443
  }
635
444
  /**
636
- * Remove a key from Onyx and update the subscribers
445
+ * Remove a key from Onyx and update the subscribers.
637
446
  */
638
447
  function remove(key) {
639
448
  OnyxCache_1.default.drop(key);
640
- keyChanged(key, undefined);
449
+ notifyKey(key, undefined);
641
450
  if (OnyxKeys_1.default.isRamOnlyKey(key)) {
642
451
  return Promise.resolve();
643
452
  }
@@ -768,11 +577,57 @@ function broadcastUpdate(key, value, hasChanged) {
768
577
  return;
769
578
  }
770
579
  OnyxCache_1.default.set(key, value);
771
- keyChanged(key, value);
580
+ notifyKey(key, value);
772
581
  }
773
582
  function hasPendingMergeForKey(key) {
774
583
  return !!mergeQueue[key];
775
584
  }
585
+ function cancelPendingMergesForKey(key) {
586
+ delete mergeQueue[key];
587
+ delete mergeQueuePromise[key];
588
+ }
589
+ function cancelPendingMergesForCollection(collectionKey) {
590
+ for (const key of Object.keys(mergeQueue)) {
591
+ if (!OnyxKeys_1.default.isCollectionMemberKey(collectionKey, key)) {
592
+ continue;
593
+ }
594
+ cancelPendingMergesForKey(key);
595
+ }
596
+ }
597
+ function getPendingMergeEntries(keysToPreserve) {
598
+ return Object.entries(mergeQueue)
599
+ .filter(([key]) => !keysToPreserve.some((preserveKey) => OnyxKeys_1.default.isKeyMatch(preserveKey, key)))
600
+ .map(([key, queuedChanges]) => [key, queuedChanges, queuedChanges.length]);
601
+ }
602
+ function cancelPendingMerges(entries) {
603
+ for (const [key, queuedChanges, capturedLength] of entries) {
604
+ if (mergeQueue[key] !== queuedChanges) {
605
+ continue;
606
+ }
607
+ if (queuedChanges.length === capturedLength) {
608
+ cancelPendingMergesForKey(key);
609
+ continue;
610
+ }
611
+ queuedChanges.splice(0, capturedLength);
612
+ mergeQueuesWithStaleRead.add(queuedChanges);
613
+ }
614
+ }
615
+ function hasStaleMergeRead(queuedChanges) {
616
+ return mergeQueuesWithStaleRead.has(queuedChanges);
617
+ }
618
+ function cancelPendingMergesForKeys(keys) {
619
+ for (const key of keys) {
620
+ cancelPendingMergesForKey(key);
621
+ }
622
+ }
623
+ function cancelPendingMergesForNullMembers(collection) {
624
+ for (const [key, value] of Object.entries(collection)) {
625
+ if (value !== null) {
626
+ continue;
627
+ }
628
+ cancelPendingMergesForKey(key);
629
+ }
630
+ }
776
631
  /**
777
632
  * Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
778
633
  * This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
@@ -892,7 +747,7 @@ function initializeWithDefaultKeyStates() {
892
747
  // Notify subscribers about default key states so that any subscriber that connected
893
748
  // before init (e.g. during module load) receives the merged default values immediately
894
749
  for (const [key, value] of Object.entries(merged !== null && merged !== void 0 ? merged : {})) {
895
- keyChanged(key, value);
750
+ notifyKey(key, value);
896
751
  }
897
752
  })
898
753
  .catch((error) => {
@@ -907,7 +762,7 @@ function initializeWithDefaultKeyStates() {
907
762
  // Notify subscribers about default key states so that any subscriber that connected
908
763
  // before init (e.g. during module load) receives the merged default values immediately
909
764
  for (const [key, value] of Object.entries(defaultKeyStates)) {
910
- keyChanged(key, value);
765
+ notifyKey(key, value);
911
766
  }
912
767
  });
913
768
  }
@@ -934,97 +789,6 @@ function doAllCollectionItemsBelongToSameParent(collectionKey, collectionKeys) {
934
789
  }
935
790
  return !hasCollectionKeyCheckFailed;
936
791
  }
937
- /**
938
- * Subscribes to an Onyx key and listens to its changes.
939
- *
940
- * @param connectOptions The options object that will define the behavior of the connection.
941
- * @returns The subscription ID to use when calling `OnyxUtils.unsubscribeFromKey()`.
942
- */
943
- function subscribeToKey(connectOptions) {
944
- const mapping = connectOptions;
945
- const subscriptionID = lastSubscriptionID++;
946
- callbackToStateMapping[subscriptionID] = mapping;
947
- callbackToStateMapping[subscriptionID].subscriptionID = subscriptionID;
948
- // When keyChanged is called, a key is passed and the method looks through all the Subscribers in callbackToStateMapping for the matching key to get the subscriptionID
949
- // to avoid having to loop through all the Subscribers all the time (even when just one connection belongs to one key),
950
- // We create a mapping from key to lists of subscriptionIDs to access the specific list of subscriptionIDs.
951
- storeKeyBySubscriptions(mapping.key, callbackToStateMapping[subscriptionID].subscriptionID);
952
- // Commit connection only after init passes
953
- deferredInitTask.promise
954
- // This first .then() adds a microtask tick for compatibility reasons and
955
- // to ensure subscribers don't receive an extra initial callback before Onyx.update() data arrives.
956
- .then(() => undefined)
957
- .then(() => {
958
- // Performance improvement
959
- // If the mapping is connected to an onyx key that is not a collection
960
- // we can skip the call to getAllKeys() and return an array with a single item
961
- if (!!mapping.key && typeof mapping.key === 'string' && !OnyxKeys_1.default.isCollectionKey(mapping.key) && OnyxCache_1.default.getAllKeys().has(mapping.key)) {
962
- return new Set([mapping.key]);
963
- }
964
- return getAllKeys();
965
- })
966
- .then((keys) => {
967
- // We search all the keys in storage to see if any are a "match" for the subscriber we are connecting so that we
968
- // can send data back to the subscriber. Note that multiple keys can match as a subscriber could either be
969
- // subscribed to a "collection key" or a single key.
970
- const matchingKeys = [];
971
- // Performance optimization: For single key subscriptions, avoid O(n) iteration
972
- if (!OnyxKeys_1.default.isCollectionKey(mapping.key)) {
973
- if (keys.has(mapping.key)) {
974
- matchingKeys.push(mapping.key);
975
- }
976
- }
977
- else {
978
- // Collection case - need to iterate through all keys to find matches (O(n))
979
- for (const key of keys) {
980
- if (!OnyxKeys_1.default.isKeyMatch(mapping.key, key)) {
981
- continue;
982
- }
983
- matchingKeys.push(key);
984
- }
985
- }
986
- // If the key being connected to does not exist we initialize the value with null. For subscribers that connected
987
- // directly via connect() they will simply get a null value sent to them without any information about which key matched
988
- // since there are none matched.
989
- if (matchingKeys.length === 0) {
990
- if (mapping.key) {
991
- OnyxCache_1.default.addNullishStorageKey(mapping.key);
992
- }
993
- const matchedKey = OnyxKeys_1.default.isCollectionKey(mapping.key) ? mapping.key : undefined;
994
- // Here we cannot use batching because the nullish value is expected to be set immediately for default props
995
- // or they will be undefined.
996
- sendDataToConnection(mapping, matchedKey);
997
- return;
998
- }
999
- // When using a callback subscriber, a subscription to a collection key combines all matching
1000
- // member values into a single object and makes one call with the whole collection object.
1001
- if (typeof mapping.callback === 'function') {
1002
- if (OnyxKeys_1.default.isCollectionKey(mapping.key)) {
1003
- getCollectionDataAndSendAsObject(matchingKeys, mapping);
1004
- return;
1005
- }
1006
- // If we are not subscribed to a collection key then there's only a single key to send an update for.
1007
- get(mapping.key).then(() => sendDataToConnection(mapping, mapping.key));
1008
- return;
1009
- }
1010
- console.error('Warning: Onyx.connect() was found without a callback');
1011
- });
1012
- // The subscriptionID is returned back to the caller so that it can be used to clean up the connection when it's no longer needed
1013
- // by calling OnyxUtils.unsubscribeFromKey(subscriptionID).
1014
- return subscriptionID;
1015
- }
1016
- /**
1017
- * Disconnects and removes the listener from the Onyx key.
1018
- *
1019
- * @param subscriptionID Subscription ID returned by calling `OnyxUtils.subscribeToKey()`.
1020
- */
1021
- function unsubscribeFromKey(subscriptionID) {
1022
- if (!callbackToStateMapping[subscriptionID]) {
1023
- return;
1024
- }
1025
- deleteKeyBySubscriptions(subscriptionID);
1026
- delete callbackToStateMapping[subscriptionID];
1027
- }
1028
792
  function updateSnapshots(data, mergeFn) {
1029
793
  const snapshotCollectionKey = getSnapshotKey();
1030
794
  if (!snapshotCollectionKey)
@@ -1197,14 +961,17 @@ function multiSetWithRetry(data, retryAttempt) {
1197
961
  }, {});
1198
962
  }
1199
963
  const { pairs: keyValuePairsToSet, keysToRemove: removalCandidates } = OnyxUtils.prepareKeyValuePairsForStorage(newData, true);
964
+ if (!retryAttempt) {
965
+ cancelPendingMergesForKeys(removalCandidates);
966
+ }
1200
967
  // Removals of keys that are neither cached nor persisted are no-ops and skipped. When the key
1201
968
  // index has not been loaded yet (empty set), keep the removal to be safe.
1202
969
  const persistedKeys = OnyxCache_1.default.getAllKeys();
1203
970
  const keysToRemove = removalCandidates.filter((key) => OnyxCache_1.default.get(key) !== undefined || persistedKeys.size === 0 || persistedKeys.has(key));
1204
971
  // Group collection members by their parent collection key so each collection can be notified
1205
- // via a single batched keysChanged() call instead of one keyChanged() per member. For each
972
+ // via a single batched notifyCollection() call instead of one notifyKey() per member. For each
1206
973
  // collection, `partial` holds the new values being set and `previous` holds the cached values
1207
- // from before the set, which keysChanged() uses to skip subscribers whose value didn't change.
974
+ // from before the set, which notifyCollection() uses to skip subscribers whose value didn't change.
1208
975
  const collectionBatches = new Map();
1209
976
  for (const [key, value] of keyValuePairsToSet) {
1210
977
  // When we use multiSet to set a key we want to clear the current delta changes from Onyx.merge that were queued
@@ -1214,7 +981,7 @@ function multiSetWithRetry(data, retryAttempt) {
1214
981
  }
1215
982
  const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
1216
983
  if (collectionKey && OnyxKeys_1.default.isCollectionMemberKey(collectionKey, key)) {
1217
- // Capture the previous cached value BEFORE calling cache.set() so keysChanged()
984
+ // Capture the previous cached value before calling cache.set() so notifyCollection()
1218
985
  // can diff old vs new per-member.
1219
986
  const previousValue = OnyxCache_1.default.get(key);
1220
987
  OnyxCache_1.default.set(key, value);
@@ -1227,14 +994,13 @@ function multiSetWithRetry(data, retryAttempt) {
1227
994
  batch.previous[key] = previousValue;
1228
995
  }
1229
996
  else {
1230
- // Non-collection keys are notified inline (cache.set + keyChanged in iteration order)
997
+ // Non-collection keys are notified inline (cache.set + notifyKey in iteration order)
1231
998
  // so re-entrant callbacks (e.g. Onyx.set inside a callback) see consistent cache
1232
999
  // and subscriber state, matching the original per-key notification semantics.
1233
1000
  OnyxCache_1.default.set(key, value);
1234
1001
  // Skip subscriber notification on retry — already notified on attempt 0.
1235
- // Collection-root subscribers re-fire on every keyChanged by contract.
1236
1002
  if (!retryAttempt) {
1237
- keyChanged(key, value);
1003
+ notifyKey(key, value);
1238
1004
  }
1239
1005
  }
1240
1006
  }
@@ -1255,15 +1021,15 @@ function multiSetWithRetry(data, retryAttempt) {
1255
1021
  }
1256
1022
  else if (!retryAttempt) {
1257
1023
  // Skip subscriber notification on retry — already notified on attempt 0.
1258
- keyChanged(key, undefined);
1024
+ notifyKey(key, undefined);
1259
1025
  }
1260
1026
  }
1261
- // One keysChanged() per collection — fires each collection-level subscriber once and lets
1262
- // keysChanged() internally decide which individual member subscribers need notification.
1027
+ // One notifyCollection() per collection: fires each collection-level subscriber once and lets
1028
+ // notifyCollection() internally decide which individual member subscribers need notification.
1263
1029
  // Skip on retry — already notified on attempt 0 (see same-reason comment above).
1264
1030
  if (!retryAttempt) {
1265
1031
  for (const [collectionKey, batch] of collectionBatches) {
1266
- keysChanged(collectionKey, batch.partial, batch.previous);
1032
+ notifyCollection(collectionKey, batch.partial, batch.previous);
1267
1033
  }
1268
1034
  }
1269
1035
  const keyValuePairsToStore = keyValuePairsToSet.filter((keyValuePair) => {
@@ -1321,6 +1087,9 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
1321
1087
  }, {});
1322
1088
  }
1323
1089
  resultCollectionKeys = Object.keys(resultCollection);
1090
+ if (!retryAttempt) {
1091
+ cancelPendingMergesForCollection(collectionKey);
1092
+ }
1324
1093
  return OnyxUtils.getAllKeys().then((persistedKeys) => {
1325
1094
  const mutableCollection = Object.assign({}, resultCollection);
1326
1095
  for (const key of persistedKeys) {
@@ -1335,18 +1104,17 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
1335
1104
  const { pairs: keyValuePairs, keysToRemove: removalCandidates } = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true);
1336
1105
  // Removals of keys that are neither cached nor persisted are no-ops and skipped.
1337
1106
  const keysToRemove = removalCandidates.filter((key) => OnyxCache_1.default.get(key) !== undefined || persistedKeys.has(key));
1338
- // Snapshot before cache mutations so keysChanged() can diff removed members.
1107
+ // Capture the previous collection before cache mutations so notifyCollection() can diff removed members.
1339
1108
  const previousCollection = OnyxUtils.getCachedCollection(collectionKey);
1340
1109
  for (const [key, value] of keyValuePairs)
1341
1110
  OnyxCache_1.default.set(key, value);
1342
1111
  for (const key of keysToRemove)
1343
1112
  OnyxCache_1.default.drop(key);
1344
1113
  // Skip subscriber notification on retry — already notified on attempt 0.
1345
- // Collection-root subscribers re-fire on every keysChanged by contract.
1346
1114
  if (!retryAttempt) {
1347
1115
  // Removed members are notified as undefined, matching mergeCollection/multiSet.
1348
1116
  const partialForNotify = Object.fromEntries(Object.entries(mutableCollection).map(([key, value]) => [key, value !== null && value !== void 0 ? value : undefined]));
1349
- keysChanged(collectionKey, partialForNotify, previousCollection);
1117
+ notifyCollection(collectionKey, partialForNotify, previousCollection);
1350
1118
  }
1351
1119
  // RAM-only keys are not supposed to be saved to storage
1352
1120
  if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) {
@@ -1408,6 +1176,9 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1408
1176
  }, {});
1409
1177
  }
1410
1178
  resultCollectionKeys = Object.keys(resultCollection);
1179
+ if (!retryAttempt) {
1180
+ cancelPendingMergesForNullMembers(resultCollection);
1181
+ }
1411
1182
  return getAllKeys()
1412
1183
  .then((persistedKeys) => {
1413
1184
  // Split to keys that exist in storage and keys that don't. Null members are collected
@@ -1473,13 +1244,13 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1473
1244
  // promise-chain depth; slow path batches the misses into one Storage.multiGet.
1474
1245
  const hasColdExistingKey = existingKeys.some((key) => !OnyxCache_1.default.hasCacheForKey(key));
1475
1246
  // Swallow pre-warm read failures so a transient Storage.multiGet rejection doesn't
1476
- // skip the cache.merge() + keysChanged() below. Subscribers still see the merge even
1247
+ // skip the cache.merge() + notifyCollection() below. Subscribers still see the merge even
1477
1248
  // when storage reads fail.
1478
1249
  const prewarmPromise = hasColdExistingKey
1479
1250
  ? multiGet(existingKeys).catch((err) => Logger.logInfo(`mergeCollectionWithPatches pre-warm failed; proceeding with cache-only merge. Error: ${err}`))
1480
1251
  : Promise.resolve();
1481
1252
  return prewarmPromise.then(() => {
1482
- // Snapshot previous values from the (now-warm) cache for keysChanged's diff, then update
1253
+ // Capture previous values from the (now-warm) cache for the subscriber diff, then update
1483
1254
  // cache and notify subscribers synchronously BEFORE issuing storage writes. This matches
1484
1255
  // the cache-first / storage-second invariant followed by every other Onyx write method
1485
1256
  // (setWithRetry, applyMerge, setCollectionWithRetry, partialSetCollection, clear),
@@ -1488,12 +1259,11 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
1488
1259
  const previousCollection = getCachedCollection(collectionKey, existingKeys);
1489
1260
  OnyxCache_1.default.merge(finalMergedCollection);
1490
1261
  // Skip subscriber notification on retry — already notified on attempt 0.
1491
- // Collection-root subscribers re-fire on every keysChanged by contract.
1492
1262
  if (!retryAttempt) {
1493
1263
  const partialForNotify = keysToRemove.length > 0 ? Object.assign(Object.assign({}, finalMergedCollection), Object.fromEntries(keysToRemove.map((key) => [key, undefined]))) : finalMergedCollection;
1494
1264
  const previousForNotify = keysToRemove.length > 0 ? Object.assign(Object.assign({}, previousCollection), removedPreviousValues) : previousCollection;
1495
1265
  if (Object.keys(partialForNotify).length > 0) {
1496
- keysChanged(collectionKey, partialForNotify, previousForNotify);
1266
+ notifyCollection(collectionKey, partialForNotify, previousForNotify);
1497
1267
  }
1498
1268
  }
1499
1269
  const promises = [];
@@ -1561,24 +1331,26 @@ function partialSetCollection({ collectionKey, collection }, retryAttempt) {
1561
1331
  }, {});
1562
1332
  }
1563
1333
  resultCollectionKeys = Object.keys(resultCollection);
1334
+ if (!retryAttempt) {
1335
+ cancelPendingMergesForKeys(resultCollectionKeys);
1336
+ }
1564
1337
  return getAllKeys().then((persistedKeys) => {
1565
1338
  const mutableCollection = Object.assign({}, resultCollection);
1566
1339
  const existingKeys = resultCollectionKeys.filter((key) => persistedKeys.has(key));
1567
1340
  const { pairs: keyValuePairs, keysToRemove: removalCandidates } = prepareKeyValuePairsForStorage(mutableCollection, true);
1568
1341
  // Removals of keys that are neither cached nor persisted are no-ops and skipped.
1569
1342
  const keysToRemove = removalCandidates.filter((key) => OnyxCache_1.default.get(key) !== undefined || persistedKeys.has(key));
1570
- // Snapshot before cache mutations so keysChanged() can diff removed members.
1343
+ // Capture the previous collection before cache mutations so notifyCollection() can diff removed members.
1571
1344
  const previousCollection = getCachedCollection(collectionKey, existingKeys);
1572
1345
  for (const [key, value] of keyValuePairs)
1573
1346
  OnyxCache_1.default.set(key, value);
1574
1347
  for (const key of keysToRemove)
1575
1348
  OnyxCache_1.default.drop(key);
1576
1349
  // Skip subscriber notification on retry — already notified on attempt 0.
1577
- // Collection-root subscribers re-fire on every keysChanged by contract.
1578
1350
  if (!retryAttempt) {
1579
1351
  // Removed members are notified as undefined, matching mergeCollection/multiSet.
1580
1352
  const partialForNotify = Object.fromEntries(Object.entries(mutableCollection).map(([key, value]) => [key, value !== null && value !== void 0 ? value : undefined]));
1581
- keysChanged(collectionKey, partialForNotify, previousCollection);
1353
+ notifyCollection(collectionKey, partialForNotify, previousCollection);
1582
1354
  }
1583
1355
  if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) {
1584
1356
  sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
@@ -1605,18 +1377,23 @@ function logKeyChanged(onyxMethod, key, value, hasChanged) {
1605
1377
  function logKeyRemoved(onyxMethod, key) {
1606
1378
  Logger.logInfo(`${onyxMethod} called for key: ${key} => null passed, so key was removed`);
1607
1379
  }
1380
+ /**
1381
+ * Put Onyx back to "not initialised" state, useful for tests that need a cold start.
1382
+ */
1383
+ function resetDeferredInitTask() {
1384
+ deferredInitTask = (0, createDeferredTask_1.default)();
1385
+ }
1608
1386
  /**
1609
1387
  * Clear internal variables used in this file, useful in test environments.
1610
1388
  */
1611
1389
  function clearOnyxUtilsInternals() {
1612
1390
  mergeQueue = {};
1613
1391
  mergeQueuePromise = {};
1614
- callbackToStateMapping = {};
1615
- onyxKeyToSubscriptionIDs = new Map();
1616
- lastConnectionCallbackData = new Map();
1392
+ PendingWrites_1.default.clearPendingWrites();
1617
1393
  }
1618
1394
  const OnyxUtils = {
1619
1395
  METHOD,
1396
+ NOT_DELIVERED,
1620
1397
  getMergeQueue,
1621
1398
  getMergeQueuePromise,
1622
1399
  getDefaultKeyStates,
@@ -1626,18 +1403,18 @@ const OnyxUtils = {
1626
1403
  sendActionToDevTools,
1627
1404
  get,
1628
1405
  getAllKeys,
1629
- tryGetCachedValue,
1630
1406
  getCachedCollection,
1631
- keysChanged,
1632
- keyChanged,
1633
- sendDataToConnection,
1634
- getCollectionDataAndSendAsObject,
1407
+ notifyKey,
1408
+ notifyCollection,
1635
1409
  remove,
1636
1410
  reportStorageQuota,
1637
1411
  resetDiskPressureLogThrottle,
1638
1412
  retryOperation,
1639
1413
  broadcastUpdate,
1640
1414
  hasPendingMergeForKey,
1415
+ getPendingMergeEntries,
1416
+ cancelPendingMerges,
1417
+ hasStaleMergeRead,
1641
1418
  prepareKeyValuePairsForStorage,
1642
1419
  mergeChanges,
1643
1420
  mergeAndMarkChanges,
@@ -1647,14 +1424,10 @@ const OnyxUtils = {
1647
1424
  tupleGet,
1648
1425
  isValidNonEmptyCollectionForMerge,
1649
1426
  doAllCollectionItemsBelongToSameParent,
1650
- subscribeToKey,
1651
- unsubscribeFromKey,
1652
1427
  getSkippableCollectionMemberIDs,
1653
1428
  setSkippableCollectionMemberIDs,
1654
1429
  getSnapshotMergeKeys,
1655
1430
  setSnapshotMergeKeys,
1656
- storeKeyBySubscriptions,
1657
- deleteKeyBySubscriptions,
1658
1431
  reduceCollectionWithSelector,
1659
1432
  updateSnapshots,
1660
1433
  mergeCollectionWithPatches,