react-native-onyx 2.0.56 → 2.0.57

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
@@ -385,26 +385,14 @@ function merge(key, changes) {
385
385
  * @param collection Object collection keyed by individual collection member keys and values
386
386
  */
387
387
  function mergeCollection(collectionKey, collection) {
388
- if (typeof collection !== 'object' || Array.isArray(collection) || utils_1.default.isEmptyObject(collection)) {
388
+ if (!OnyxUtils_1.default.isValidNonEmptyCollectionForMerge(collection)) {
389
389
  Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
390
390
  return Promise.resolve();
391
391
  }
392
392
  const mergedCollection = collection;
393
393
  // Confirm all the collection keys belong to the same parent
394
- let hasCollectionKeyCheckFailed = false;
395
394
  const mergedCollectionKeys = Object.keys(mergedCollection);
396
- mergedCollectionKeys.forEach((dataKey) => {
397
- if (OnyxUtils_1.default.isKeyMatch(collectionKey, dataKey)) {
398
- return;
399
- }
400
- if (process.env.NODE_ENV === 'development') {
401
- throw new Error(`Provided collection doesn't have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
402
- }
403
- hasCollectionKeyCheckFailed = true;
404
- Logger.logAlert(`Provided collection doesn't have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
405
- });
406
- // Gracefully handle bad mergeCollection updates so it doesn't block the merge queue
407
- if (hasCollectionKeyCheckFailed) {
395
+ if (!OnyxUtils_1.default.doAllCollectionItemsBelongToSameParent(collectionKey, mergedCollectionKeys)) {
408
396
  return Promise.resolve();
409
397
  }
410
398
  return OnyxUtils_1.default.getAllKeys()
@@ -620,22 +608,53 @@ function update(data) {
620
608
  throw new Error(`Invalid ${typeof key} key provided in Onyx update. Onyx key must be of type string.`);
621
609
  }
622
610
  });
611
+ // The queue of operations within a single `update` call in the format of <item key - list of operations updating the item>.
612
+ // This allows us to batch the operations per item and merge them into one operation in the order they were requested.
613
+ const updateQueue = {};
614
+ const enqueueSetOperation = (key, value) => {
615
+ // If a `set` operation is enqueued, we should clear the whole queue.
616
+ // Since the `set` operation replaces the value entirely, there's no need to perform any previous operations.
617
+ // To do this, we first put `null` in the queue, which removes the existing value, and then merge the new value.
618
+ updateQueue[key] = [null, value];
619
+ };
620
+ const enqueueMergeOperation = (key, value) => {
621
+ if (value === null) {
622
+ // If we merge `null`, the value is removed and all the previous operations are discarded.
623
+ updateQueue[key] = [null];
624
+ }
625
+ else if (!updateQueue[key]) {
626
+ updateQueue[key] = [value];
627
+ }
628
+ else {
629
+ updateQueue[key].push(value);
630
+ }
631
+ };
623
632
  const promises = [];
624
633
  let clearPromise = Promise.resolve();
625
634
  data.forEach(({ onyxMethod, key, value }) => {
626
635
  switch (onyxMethod) {
627
636
  case OnyxUtils_1.default.METHOD.SET:
628
- promises.push(() => set(key, value));
637
+ enqueueSetOperation(key, value);
629
638
  break;
630
639
  case OnyxUtils_1.default.METHOD.MERGE:
631
- promises.push(() => merge(key, value));
640
+ enqueueMergeOperation(key, value);
632
641
  break;
633
- case OnyxUtils_1.default.METHOD.MERGE_COLLECTION:
634
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- We validated that the value is a collection
635
- promises.push(() => mergeCollection(key, value));
642
+ case OnyxUtils_1.default.METHOD.MERGE_COLLECTION: {
643
+ const collection = value;
644
+ if (!OnyxUtils_1.default.isValidNonEmptyCollectionForMerge(collection)) {
645
+ Logger.logInfo('mergeCollection enqueued within update() with invalid or empty value. Skipping this operation.');
646
+ break;
647
+ }
648
+ // Confirm all the collection keys belong to the same parent
649
+ const collectionKeys = Object.keys(collection);
650
+ if (OnyxUtils_1.default.doAllCollectionItemsBelongToSameParent(key, collectionKeys)) {
651
+ const mergedCollection = collection;
652
+ collectionKeys.forEach((collectionKey) => enqueueMergeOperation(collectionKey, mergedCollection[collectionKey]));
653
+ }
636
654
  break;
655
+ }
637
656
  case OnyxUtils_1.default.METHOD.MULTI_SET:
638
- promises.push(() => multiSet(value));
657
+ Object.entries(value).forEach(([entryKey, entryValue]) => enqueueSetOperation(entryKey, entryValue));
639
658
  break;
640
659
  case OnyxUtils_1.default.METHOD.CLEAR:
641
660
  clearPromise = clear();
@@ -644,6 +663,50 @@ function update(data) {
644
663
  break;
645
664
  }
646
665
  });
666
+ // Group all the collection-related keys and update each collection in a single `mergeCollection` call.
667
+ // This is needed to prevent multiple `mergeCollection` calls for the same collection and `merge` calls for the individual items of the said collection.
668
+ // This way, we ensure there is no race condition in the queued updates of the same key.
669
+ OnyxUtils_1.default.getCollectionKeys().forEach((collectionKey) => {
670
+ const collectionItemKeys = Object.keys(updateQueue).filter((key) => OnyxUtils_1.default.isKeyMatch(collectionKey, key));
671
+ if (collectionItemKeys.length <= 1) {
672
+ // If there are no items of this collection in the updateQueue, we should skip it.
673
+ // If there is only one item, we should update it individually, therefore retain it in the updateQueue.
674
+ return;
675
+ }
676
+ const batchedCollectionUpdates = collectionItemKeys.reduce((queue, key) => {
677
+ const operations = updateQueue[key];
678
+ // Remove the collection-related key from the updateQueue so that it won't be processed individually.
679
+ delete updateQueue[key];
680
+ const updatedValue = OnyxUtils_1.default.applyMerge(undefined, operations, false);
681
+ if (operations[0] === null) {
682
+ // eslint-disable-next-line no-param-reassign
683
+ queue.set[key] = updatedValue;
684
+ }
685
+ else {
686
+ // eslint-disable-next-line no-param-reassign
687
+ queue.merge[key] = updatedValue;
688
+ }
689
+ return queue;
690
+ }, {
691
+ merge: {},
692
+ set: {},
693
+ });
694
+ if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.merge)) {
695
+ promises.push(() => mergeCollection(collectionKey, batchedCollectionUpdates.merge));
696
+ }
697
+ if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) {
698
+ promises.push(() => multiSet(batchedCollectionUpdates.set));
699
+ }
700
+ });
701
+ Object.entries(updateQueue).forEach(([key, operations]) => {
702
+ const batchedChanges = OnyxUtils_1.default.applyMerge(undefined, operations, false);
703
+ if (operations[0] === null) {
704
+ promises.push(() => set(key, batchedChanges));
705
+ }
706
+ else {
707
+ promises.push(() => merge(key, batchedChanges));
708
+ }
709
+ });
647
710
  return clearPromise
648
711
  .then(() => Promise.all(promises.map((p) => p())))
649
712
  .then(() => updateSnapshots(data))
@@ -1,6 +1,6 @@
1
1
  import type { ValueOf } from 'type-fest';
2
2
  import type Onyx from './Onyx';
3
- import type { CollectionKey, CollectionKeyBase, DeepRecord, KeyValueMapping, Mapping, OnyxCollection, OnyxEntry, OnyxInput, OnyxKey, OnyxValue, WithOnyxConnectOptions } from './types';
3
+ import type { CollectionKey, CollectionKeyBase, DeepRecord, KeyValueMapping, Mapping, OnyxCollection, OnyxEntry, OnyxInput, OnyxKey, OnyxMergeCollectionInput, OnyxValue, WithOnyxConnectOptions } from './types';
4
4
  declare const METHOD: {
5
5
  readonly SET: "set";
6
6
  readonly MERGE: "merge";
@@ -71,7 +71,11 @@ declare function deleteKeyByConnections(connectionID: number): void;
71
71
  /** Returns current key names stored in persisted storage */
72
72
  declare function getAllKeys(): Promise<Set<OnyxKey>>;
73
73
  /**
74
- * Checks to see if the a subscriber's supplied key
74
+ * Returns set of all registered collection keys
75
+ */
76
+ declare function getCollectionKeys(): Set<OnyxKey>;
77
+ /**
78
+ * Checks to see if the subscriber's supplied key
75
79
  * is associated with a collection of keys.
76
80
  */
77
81
  declare function isCollectionKey(key: OnyxKey): key is CollectionKeyBase;
@@ -216,6 +220,14 @@ declare function applyMerge<TValue extends OnyxInput<OnyxKey> | undefined, TChan
216
220
  * Merge user provided default key value pairs.
217
221
  */
218
222
  declare function initializeWithDefaultKeyStates(): Promise<void>;
223
+ /**
224
+ * Validate the collection is not empty and has a correct type before applying mergeCollection()
225
+ */
226
+ declare function isValidNonEmptyCollectionForMerge<TKey extends CollectionKeyBase, TMap>(collection: OnyxMergeCollectionInput<TKey, TMap>): boolean;
227
+ /**
228
+ * Verify if all the collection keys belong to the same parent
229
+ */
230
+ declare function doAllCollectionItemsBelongToSameParent<TKey extends CollectionKeyBase>(collectionKey: TKey, collectionKeys: string[]): boolean;
219
231
  declare const OnyxUtils: {
220
232
  METHOD: {
221
233
  readonly SET: "set";
@@ -234,6 +246,7 @@ declare const OnyxUtils: {
234
246
  batchUpdates: typeof batchUpdates;
235
247
  get: typeof get;
236
248
  getAllKeys: typeof getAllKeys;
249
+ getCollectionKeys: typeof getCollectionKeys;
237
250
  isCollectionKey: typeof isCollectionKey;
238
251
  isCollectionMemberKey: typeof isCollectionMemberKey;
239
252
  splitCollectionMemberKey: typeof splitCollectionMemberKey;
@@ -267,5 +280,7 @@ declare const OnyxUtils: {
267
280
  deleteKeyByConnections: typeof deleteKeyByConnections;
268
281
  getSnapshotKey: typeof getSnapshotKey;
269
282
  multiGet: typeof multiGet;
283
+ isValidNonEmptyCollectionForMerge: typeof isValidNonEmptyCollectionForMerge;
284
+ doAllCollectionItemsBelongToSameParent: typeof doAllCollectionItemsBelongToSameParent;
270
285
  };
271
286
  export default OnyxUtils;
package/dist/OnyxUtils.js CHANGED
@@ -49,10 +49,10 @@ const METHOD = {
49
49
  // Key/value store of Onyx key and arrays of values to merge
50
50
  const mergeQueue = {};
51
51
  const mergeQueuePromise = {};
52
- // Holds a mapping of all the react components that want their state subscribed to a store key
52
+ // Holds a mapping of all the React components that want their state subscribed to a store key
53
53
  const callbackToStateMapping = {};
54
54
  // Keeps a copy of the values of the onyx collection keys as a map for faster lookups
55
- let onyxCollectionKeyMap = new Map();
55
+ let onyxCollectionKeySet = new Set();
56
56
  // Holds a mapping of the connected key to the connectionID for faster lookups
57
57
  const onyxKeyToConnectionIDs = new Map();
58
58
  // Holds a list of keys that have been directly subscribed to or recently modified from least to most recent
@@ -107,10 +107,10 @@ function initStoreValues(keys, initialKeyStates, safeEvictionKeys) {
107
107
  // We need the value of the collection keys later for checking if a
108
108
  // key is a collection. We store it in a map for faster lookup.
109
109
  const collectionValues = Object.values((_a = keys.COLLECTION) !== null && _a !== void 0 ? _a : {});
110
- onyxCollectionKeyMap = collectionValues.reduce((acc, val) => {
111
- acc.set(val, true);
110
+ onyxCollectionKeySet = collectionValues.reduce((acc, val) => {
111
+ acc.add(val);
112
112
  return acc;
113
- }, new Map());
113
+ }, new Set());
114
114
  // Set our default key states to use when initializing and clearing Onyx data
115
115
  defaultKeyStates = initialKeyStates;
116
116
  DevTools_1.default.initState(initialKeyStates);
@@ -300,11 +300,17 @@ function getAllKeys() {
300
300
  return OnyxCache_1.default.captureTask(taskName, promise);
301
301
  }
302
302
  /**
303
- * Checks to see if the a subscriber's supplied key
303
+ * Returns set of all registered collection keys
304
+ */
305
+ function getCollectionKeys() {
306
+ return onyxCollectionKeySet;
307
+ }
308
+ /**
309
+ * Checks to see if the subscriber's supplied key
304
310
  * is associated with a collection of keys.
305
311
  */
306
312
  function isCollectionKey(key) {
307
- return onyxCollectionKeyMap.has(key);
313
+ return onyxCollectionKeySet.has(key);
308
314
  }
309
315
  function isCollectionMemberKey(collectionKey, key) {
310
316
  return Str.startsWith(key, collectionKey) && key.length > collectionKey.length;
@@ -955,6 +961,29 @@ function initializeWithDefaultKeyStates() {
955
961
  Object.entries(merged !== null && merged !== void 0 ? merged : {}).forEach(([key, value]) => keyChanged(key, value, existingDataAsObject));
956
962
  });
957
963
  }
964
+ /**
965
+ * Validate the collection is not empty and has a correct type before applying mergeCollection()
966
+ */
967
+ function isValidNonEmptyCollectionForMerge(collection) {
968
+ return typeof collection === 'object' && !Array.isArray(collection) && !utils_1.default.isEmptyObject(collection);
969
+ }
970
+ /**
971
+ * Verify if all the collection keys belong to the same parent
972
+ */
973
+ function doAllCollectionItemsBelongToSameParent(collectionKey, collectionKeys) {
974
+ let hasCollectionKeyCheckFailed = false;
975
+ collectionKeys.forEach((dataKey) => {
976
+ if (OnyxUtils.isKeyMatch(collectionKey, dataKey)) {
977
+ return;
978
+ }
979
+ if (process.env.NODE_ENV === 'development') {
980
+ throw new Error(`Provided collection doesn't have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
981
+ }
982
+ hasCollectionKeyCheckFailed = true;
983
+ Logger.logAlert(`Provided collection doesn't have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
984
+ });
985
+ return !hasCollectionKeyCheckFailed;
986
+ }
958
987
  const OnyxUtils = {
959
988
  METHOD,
960
989
  getMergeQueue,
@@ -967,6 +996,7 @@ const OnyxUtils = {
967
996
  batchUpdates,
968
997
  get,
969
998
  getAllKeys,
999
+ getCollectionKeys,
970
1000
  isCollectionKey,
971
1001
  isCollectionMemberKey,
972
1002
  splitCollectionMemberKey,
@@ -1000,5 +1030,7 @@ const OnyxUtils = {
1000
1030
  deleteKeyByConnections,
1001
1031
  getSnapshotKey,
1002
1032
  multiGet,
1033
+ isValidNonEmptyCollectionForMerge,
1034
+ doAllCollectionItemsBelongToSameParent,
1003
1035
  };
1004
1036
  exports.default = OnyxUtils;
package/dist/types.d.ts CHANGED
@@ -365,4 +365,11 @@ type InitOptions = {
365
365
  debugSetState?: boolean;
366
366
  };
367
367
  type GenericFunction = (...args: any[]) => any;
368
- export type { BaseConnectOptions, Collection, CollectionConnectCallback, CollectionConnectOptions, CollectionKey, CollectionKeyBase, ConnectOptions, CustomTypeOptions, DeepRecord, DefaultConnectCallback, DefaultConnectOptions, ExtractOnyxCollectionValue, GenericFunction, InitOptions, Key, KeyValueMapping, Mapping, NonNull, NonUndefined, OnyxInputKeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxInputValue, OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, OnyxMergeCollectionInput, OnyxUpdate, OnyxValue, Selector, WithOnyxConnectOptions, };
368
+ /**
369
+ * Represents a combination of Merge and Set operations that should be executed in Onyx
370
+ */
371
+ type MixedOperationsQueue = {
372
+ merge: OnyxInputKeyValueMapping;
373
+ set: OnyxInputKeyValueMapping;
374
+ };
375
+ export type { BaseConnectOptions, Collection, CollectionConnectCallback, CollectionConnectOptions, CollectionKey, CollectionKeyBase, ConnectOptions, CustomTypeOptions, DeepRecord, DefaultConnectCallback, DefaultConnectOptions, ExtractOnyxCollectionValue, GenericFunction, InitOptions, Key, KeyValueMapping, Mapping, NonNull, NonUndefined, OnyxInputKeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxInputValue, OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, OnyxMergeCollectionInput, OnyxUpdate, OnyxValue, Selector, WithOnyxConnectOptions, MixedOperationsQueue, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "2.0.56",
3
+ "version": "2.0.57",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",