react-native-onyx 2.0.53 → 2.0.55

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
@@ -97,6 +97,10 @@ function connect(connectOptions) {
97
97
  const callbackToStateMapping = OnyxUtils_1.default.getCallbackToStateMapping();
98
98
  callbackToStateMapping[connectionID] = mapping;
99
99
  callbackToStateMapping[connectionID].connectionID = connectionID;
100
+ // 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 connectionID
101
+ // to avoid having to loop through all the Subscribers all the time (even when just one connection belongs to one key),
102
+ // We create a mapping from key to lists of connectionIDs to access the specific list of connectionIDs.
103
+ OnyxUtils_1.default.storeKeyByConnections(mapping.key, callbackToStateMapping[connectionID].connectionID);
100
104
  if (mapping.initWithStoredValues === false) {
101
105
  return connectionID;
102
106
  }
@@ -146,10 +150,11 @@ function connect(connectOptions) {
146
150
  return;
147
151
  }
148
152
  // We did not opt into using waitForCollectionCallback mode so the callback is called for every matching key.
149
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
150
- for (let i = 0; i < matchingKeys.length; i++) {
151
- OnyxUtils_1.default.get(matchingKeys[i]).then((val) => OnyxUtils_1.default.sendDataToConnection(mapping, val, matchingKeys[i], true));
152
- }
153
+ OnyxUtils_1.default.multiGet(matchingKeys).then((values) => {
154
+ values.forEach((val, key) => {
155
+ OnyxUtils_1.default.sendDataToConnection(mapping, val, key, true);
156
+ });
157
+ });
153
158
  return;
154
159
  }
155
160
  // If we are not subscribed to a collection key then there's only a single key to send an update for.
@@ -190,6 +195,7 @@ function disconnect(connectionID, keyToRemoveFromEvictionBlocklist) {
190
195
  if (keyToRemoveFromEvictionBlocklist) {
191
196
  OnyxUtils_1.default.removeFromEvictionBlockList(keyToRemoveFromEvictionBlocklist, connectionID);
192
197
  }
198
+ OnyxUtils_1.default.deleteKeyByConnections(lastConnectionID);
193
199
  delete callbackToStateMapping[connectionID];
194
200
  }
195
201
  /**
@@ -386,7 +392,8 @@ function mergeCollection(collectionKey, collection) {
386
392
  const mergedCollection = collection;
387
393
  // Confirm all the collection keys belong to the same parent
388
394
  let hasCollectionKeyCheckFailed = false;
389
- Object.keys(mergedCollection).forEach((dataKey) => {
395
+ const mergedCollectionKeys = Object.keys(mergedCollection);
396
+ mergedCollectionKeys.forEach((dataKey) => {
390
397
  if (OnyxUtils_1.default.isKeyMatch(collectionKey, dataKey)) {
391
398
  return;
392
399
  }
@@ -403,7 +410,7 @@ function mergeCollection(collectionKey, collection) {
403
410
  return OnyxUtils_1.default.getAllKeys()
404
411
  .then((persistedKeys) => {
405
412
  // Split to keys that exist in storage and keys that don't
406
- const keys = Object.keys(mergedCollection).filter((key) => {
413
+ const keys = mergedCollectionKeys.filter((key) => {
407
414
  if (mergedCollection[key] === null) {
408
415
  OnyxUtils_1.default.remove(key);
409
416
  return false;
@@ -412,7 +419,6 @@ function mergeCollection(collectionKey, collection) {
412
419
  });
413
420
  const existingKeys = keys.filter((key) => persistedKeys.has(key));
414
421
  const cachedCollectionForExistingKeys = OnyxUtils_1.default.getCachedCollection(collectionKey, existingKeys);
415
- const newKeys = keys.filter((key) => !persistedKeys.has(key));
416
422
  const existingKeyCollection = existingKeys.reduce((obj, key) => {
417
423
  const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(mergedCollection[key], cachedCollectionForExistingKeys[key]);
418
424
  if (!isCompatible) {
@@ -423,11 +429,13 @@ function mergeCollection(collectionKey, collection) {
423
429
  obj[key] = mergedCollection[key];
424
430
  return obj;
425
431
  }, {});
426
- const newCollection = newKeys.reduce((obj, key) => {
427
- // eslint-disable-next-line no-param-reassign
428
- obj[key] = mergedCollection[key];
429
- return obj;
430
- }, {});
432
+ const newCollection = {};
433
+ keys.forEach((key) => {
434
+ if (persistedKeys.has(key)) {
435
+ return;
436
+ }
437
+ newCollection[key] = mergedCollection[key];
438
+ });
431
439
  // When (multi-)merging the values with the existing values in storage,
432
440
  // we don't want to remove nested null values from the data that we pass to the storage layer,
433
441
  // because the storage layer uses them to remove nested keys from storage natively.
@@ -54,6 +54,20 @@ declare function maybeFlushBatchUpdates(): Promise<void>;
54
54
  declare function batchUpdates(updates: () => void): Promise<void>;
55
55
  /** Get some data from the store */
56
56
  declare function get<TKey extends OnyxKey, TValue extends OnyxValue<TKey>>(key: TKey): Promise<TValue>;
57
+ declare function multiGet<TKey extends OnyxKey>(keys: CollectionKeyBase[]): Promise<Map<OnyxKey, OnyxValue<TKey>>>;
58
+ /**
59
+ * Stores a connection ID associated with a given key.
60
+ *
61
+ * @param connectionID - a connection ID of the subscriber
62
+ * @param key - a key that the subscriber is connected to
63
+ */
64
+ declare function storeKeyByConnections(key: OnyxKey, connectionID: number): void;
65
+ /**
66
+ * Deletes a connection ID associated with its corresponding key.
67
+ *
68
+ * @param {number} connectionID - The connection ID to be deleted.
69
+ */
70
+ declare function deleteKeyByConnections(connectionID: number): void;
57
71
  /** Returns current key names stored in persisted storage */
58
72
  declare function getAllKeys(): Promise<Set<OnyxKey>>;
59
73
  /**
@@ -75,6 +89,18 @@ declare function splitCollectionMemberKey<TKey extends CollectionKey>(key: TKey)
75
89
  declare function isKeyMatch(configKey: OnyxKey, key: OnyxKey): boolean;
76
90
  /** Checks to see if this key has been flagged as safe for removal. */
77
91
  declare function isSafeEvictionKey(testKey: OnyxKey): boolean;
92
+ /**
93
+ * It extracts the non-numeric collection identifier of a given key.
94
+ *
95
+ * For example:
96
+ * - `getCollectionKey("report_123")` would return "report_"
97
+ * - `getCollectionKey("report")` would return "report"
98
+ * - `getCollectionKey("report_")` would return "report_"
99
+ *
100
+ * @param {OnyxKey} key - The key to process.
101
+ * @return {string} The pure key without any numeric
102
+ */
103
+ declare function getCollectionKey(key: OnyxKey): string;
78
104
  /**
79
105
  * Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined.
80
106
  * If the requested key is a collection, it will return an object with all the collection members.
@@ -224,6 +250,7 @@ declare const OnyxUtils: {
224
250
  keyChanged: typeof keyChanged;
225
251
  sendDataToConnection: typeof sendDataToConnection;
226
252
  addKeyToRecentlyAccessedIfNeeded: typeof addKeyToRecentlyAccessedIfNeeded;
253
+ getCollectionKey: typeof getCollectionKey;
227
254
  getCollectionDataAndSendAsObject: typeof getCollectionDataAndSendAsObject;
228
255
  scheduleSubscriberUpdate: typeof scheduleSubscriberUpdate;
229
256
  scheduleNotifyCollectionSubscribers: typeof scheduleNotifyCollectionSubscribers;
@@ -236,6 +263,9 @@ declare const OnyxUtils: {
236
263
  prepareKeyValuePairsForStorage: typeof prepareKeyValuePairsForStorage;
237
264
  applyMerge: typeof applyMerge;
238
265
  initializeWithDefaultKeyStates: typeof initializeWithDefaultKeyStates;
266
+ storeKeyByConnections: typeof storeKeyByConnections;
267
+ deleteKeyByConnections: typeof deleteKeyByConnections;
239
268
  getSnapshotKey: typeof getSnapshotKey;
269
+ multiGet: typeof multiGet;
240
270
  };
241
271
  export default OnyxUtils;
package/dist/OnyxUtils.js CHANGED
@@ -53,6 +53,8 @@ const mergeQueuePromise = {};
53
53
  const callbackToStateMapping = {};
54
54
  // Keeps a copy of the values of the onyx collection keys as a map for faster lookups
55
55
  let onyxCollectionKeyMap = new Map();
56
+ // Holds a mapping of the connected key to the connectionID for faster lookups
57
+ const onyxKeyToConnectionIDs = new Map();
56
58
  // Holds a list of keys that have been directly subscribed to or recently modified from least to most recent
57
59
  let recentlyAccessedKeys = [];
58
60
  // Holds a list of keys that are safe to remove when we reach max storage. If a key does not match with
@@ -190,6 +192,93 @@ function get(key) {
190
192
  .catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
191
193
  return OnyxCache_1.default.captureTask(taskName, promise);
192
194
  }
195
+ // multiGet the data first from the cache and then from the storage for the missing keys.
196
+ function multiGet(keys) {
197
+ // Keys that are not in the cache
198
+ const missingKeys = [];
199
+ // Tasks that are pending
200
+ const pendingTasks = [];
201
+ // Keys for the tasks that are pending
202
+ const pendingKeys = [];
203
+ // Data to be sent back to the invoker
204
+ const dataMap = new Map();
205
+ /**
206
+ * We are going to iterate over all the matching keys and check if we have the data in the cache.
207
+ * If we do then we add it to the data object. If we do not have them, then we check if there is a pending task
208
+ * for the key. If there is such task, then we add the promise to the pendingTasks array and the key to the pendingKeys
209
+ * array. If there is no pending task then we add the key to the missingKeys array.
210
+ *
211
+ * These missingKeys will be later used to multiGet the data from the storage.
212
+ */
213
+ keys.forEach((key) => {
214
+ const cacheValue = OnyxCache_1.default.get(key);
215
+ if (cacheValue) {
216
+ dataMap.set(key, cacheValue);
217
+ return;
218
+ }
219
+ const pendingKey = `get:${key}`;
220
+ if (OnyxCache_1.default.hasPendingTask(pendingKey)) {
221
+ pendingTasks.push(OnyxCache_1.default.getTaskPromise(pendingKey));
222
+ pendingKeys.push(key);
223
+ }
224
+ else {
225
+ missingKeys.push(key);
226
+ }
227
+ });
228
+ return (Promise.all(pendingTasks)
229
+ // Wait for all the pending tasks to resolve and then add the data to the data map.
230
+ .then((values) => {
231
+ values.forEach((value, index) => {
232
+ dataMap.set(pendingKeys[index], value);
233
+ });
234
+ return Promise.resolve();
235
+ })
236
+ // Get the missing keys using multiGet from the storage.
237
+ .then(() => {
238
+ if (missingKeys.length === 0) {
239
+ return Promise.resolve(undefined);
240
+ }
241
+ return storage_1.default.multiGet(missingKeys);
242
+ })
243
+ // Add the data from the missing keys to the data map and also merge it to the cache.
244
+ .then((values) => {
245
+ if (!values || values.length === 0) {
246
+ return dataMap;
247
+ }
248
+ // temp object is used to merge the missing data into the cache
249
+ const temp = {};
250
+ values.forEach(([key, value]) => {
251
+ dataMap.set(key, value);
252
+ temp[key] = value;
253
+ });
254
+ OnyxCache_1.default.merge(temp);
255
+ return dataMap;
256
+ }));
257
+ }
258
+ /**
259
+ * Stores a connection ID associated with a given key.
260
+ *
261
+ * @param connectionID - a connection ID of the subscriber
262
+ * @param key - a key that the subscriber is connected to
263
+ */
264
+ function storeKeyByConnections(key, connectionID) {
265
+ if (!onyxKeyToConnectionIDs.has(key)) {
266
+ onyxKeyToConnectionIDs.set(key, []);
267
+ }
268
+ onyxKeyToConnectionIDs.get(key).push(connectionID);
269
+ }
270
+ /**
271
+ * Deletes a connection ID associated with its corresponding key.
272
+ *
273
+ * @param {number} connectionID - The connection ID to be deleted.
274
+ */
275
+ function deleteKeyByConnections(connectionID) {
276
+ const subscriber = callbackToStateMapping[connectionID];
277
+ if (subscriber && onyxKeyToConnectionIDs.has(subscriber.key)) {
278
+ const updatedConnectionIDs = onyxKeyToConnectionIDs.get(subscriber.key).filter((id) => id !== connectionID);
279
+ onyxKeyToConnectionIDs.set(subscriber.key, updatedConnectionIDs);
280
+ }
281
+ }
193
282
  /** Returns current key names stored in persisted storage */
194
283
  function getAllKeys() {
195
284
  // When we've already read stored keys, resolve right away
@@ -226,7 +315,7 @@ function isCollectionMemberKey(collectionKey, key) {
226
315
  * @returns A tuple where the first element is the collection part and the second element is the ID part.
227
316
  */
228
317
  function splitCollectionMemberKey(key) {
229
- const underscoreIndex = key.indexOf('_');
318
+ const underscoreIndex = key.lastIndexOf('_');
230
319
  if (underscoreIndex === -1) {
231
320
  throw new Error(`Invalid ${key} key provided, only collection keys are allowed.`);
232
321
  }
@@ -243,6 +332,24 @@ function isKeyMatch(configKey, key) {
243
332
  function isSafeEvictionKey(testKey) {
244
333
  return evictionAllowList.some((key) => isKeyMatch(key, testKey));
245
334
  }
335
+ /**
336
+ * It extracts the non-numeric collection identifier of a given key.
337
+ *
338
+ * For example:
339
+ * - `getCollectionKey("report_123")` would return "report_"
340
+ * - `getCollectionKey("report")` would return "report"
341
+ * - `getCollectionKey("report_")` would return "report_"
342
+ *
343
+ * @param {OnyxKey} key - The key to process.
344
+ * @return {string} The pure key without any numeric
345
+ */
346
+ function getCollectionKey(key) {
347
+ const underscoreIndex = key.lastIndexOf('_');
348
+ if (underscoreIndex === -1) {
349
+ return key;
350
+ }
351
+ return key.substring(0, underscoreIndex + 1);
352
+ }
246
353
  /**
247
354
  * Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined.
248
355
  * If the requested key is a collection, it will return an object with all the collection members.
@@ -513,6 +620,7 @@ function keysChanged(collectionKey, partialCollection, partialPreviousCollection
513
620
  * keyChanged(key, value, subscriber => subscriber.initWithStoredValues === false)
514
621
  */
515
622
  function keyChanged(key, value, previousValue, canUpdateSubscriber = () => true, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true) {
623
+ var _a, _b;
516
624
  // Add or remove this key from the recentlyAccessedKeys lists
517
625
  if (value !== null) {
518
626
  addLastAccessedKey(key);
@@ -520,10 +628,22 @@ function keyChanged(key, value, previousValue, canUpdateSubscriber = () => true,
520
628
  else {
521
629
  removeLastAccessedKey(key);
522
630
  }
523
- // We are iterating over all subscribers to see if they are interested in the key that has just changed. If the subscriber's key is a collection key then we will
631
+ // We get the subscribers interested in the key that has just changed. If the subscriber's key is a collection key then we will
524
632
  // 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. Depending on whether the subscriber
525
633
  // was connected via withOnyx we will call setState() directly on the withOnyx instance. If it is a regular connection we will pass the data to the provided callback.
526
- const stateMappingKeys = Object.keys(callbackToStateMapping);
634
+ // 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
635
+ // do the same in keysChanged, because we only call that function when a collection key changes, and it doesn't happen that often.
636
+ // 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.
637
+ let stateMappingKeys = (_a = onyxKeyToConnectionIDs.get(key)) !== null && _a !== void 0 ? _a : [];
638
+ const collectionKey = getCollectionKey(key);
639
+ const plainCollectionKey = collectionKey.lastIndexOf('_') !== -1 ? collectionKey : undefined;
640
+ if (plainCollectionKey) {
641
+ // Getting the collection key from the specific key because only collection keys were stored in the mapping.
642
+ stateMappingKeys = [...stateMappingKeys, ...((_b = onyxKeyToConnectionIDs.get(plainCollectionKey)) !== null && _b !== void 0 ? _b : [])];
643
+ if (stateMappingKeys.length === 0) {
644
+ return;
645
+ }
646
+ }
527
647
  for (let i = 0; i < stateMappingKeys.length; i++) {
528
648
  const subscriber = callbackToStateMapping[stateMappingKeys[i]];
529
649
  if (!subscriber || !isKeyMatch(subscriber.key, key) || !canUpdateSubscriber(subscriber)) {
@@ -682,68 +802,8 @@ function addKeyToRecentlyAccessedIfNeeded(mapping) {
682
802
  * Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.
683
803
  */
684
804
  function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
685
- // Keys that are not in the cache
686
- const missingKeys = [];
687
- // Tasks that are pending
688
- const pendingTasks = [];
689
- // Keys for the tasks that are pending
690
- const pendingKeys = [];
691
- // We are going to combine all the data from the matching keys into a single object
692
- const data = {};
693
- /**
694
- * We are going to iterate over all the matching keys and check if we have the data in the cache.
695
- * If we do then we add it to the data object. If we do not then we check if there is a pending task
696
- * for the key. If there is then we add the promise to the pendingTasks array and the key to the pendingKeys
697
- * array. If there is no pending task then we add the key to the missingKeys array.
698
- *
699
- * These missingKeys will be later to use to multiGet the data from the storage.
700
- */
701
- matchingKeys.forEach((key) => {
702
- const cacheValue = OnyxCache_1.default.get(key);
703
- if (cacheValue) {
704
- data[key] = cacheValue;
705
- return;
706
- }
707
- const pendingKey = `get:${key}`;
708
- if (OnyxCache_1.default.hasPendingTask(pendingKey)) {
709
- pendingTasks.push(OnyxCache_1.default.getTaskPromise(pendingKey));
710
- pendingKeys.push(key);
711
- }
712
- else {
713
- missingKeys.push(key);
714
- }
715
- });
716
- Promise.all(pendingTasks)
717
- // We are going to wait for all the pending tasks to resolve and then add the data to the data object.
718
- .then((values) => {
719
- values.forEach((value, index) => {
720
- data[pendingKeys[index]] = value;
721
- });
722
- return Promise.resolve();
723
- })
724
- // We are going to get the missing keys using multiGet from the storage.
725
- .then(() => {
726
- if (missingKeys.length === 0) {
727
- return Promise.resolve(undefined);
728
- }
729
- return storage_1.default.multiGet(missingKeys);
730
- })
731
- // We are going to add the data from the missing keys to the data object and also merge it to the cache.
732
- .then((values) => {
733
- if (!values || values.length === 0) {
734
- return Promise.resolve();
735
- }
736
- // temp object is used to merge the missing data into the cache
737
- const temp = {};
738
- values.forEach(([key, value]) => {
739
- data[key] = value;
740
- temp[key] = value;
741
- });
742
- OnyxCache_1.default.merge(temp);
743
- return Promise.resolve();
744
- })
745
- // We are going to send the data to the subscriber.
746
- .finally(() => {
805
+ multiGet(matchingKeys).then((dataMap) => {
806
+ const data = Object.fromEntries(dataMap.entries());
747
807
  sendDataToConnection(mapping, data, undefined, true);
748
808
  });
749
809
  }
@@ -923,6 +983,7 @@ const OnyxUtils = {
923
983
  keyChanged,
924
984
  sendDataToConnection,
925
985
  addKeyToRecentlyAccessedIfNeeded,
986
+ getCollectionKey,
926
987
  getCollectionDataAndSendAsObject,
927
988
  scheduleSubscriberUpdate,
928
989
  scheduleNotifyCollectionSubscribers,
@@ -935,6 +996,9 @@ const OnyxUtils = {
935
996
  prepareKeyValuePairsForStorage,
936
997
  applyMerge,
937
998
  initializeWithDefaultKeyStates,
999
+ storeKeyByConnections,
1000
+ deleteKeyByConnections,
938
1001
  getSnapshotKey,
1002
+ multiGet,
939
1003
  };
940
1004
  exports.default = OnyxUtils;
package/dist/utils.js CHANGED
@@ -11,7 +11,7 @@ function isEmptyObject(obj) {
11
11
  */
12
12
  function isMergeableObject(value) {
13
13
  const isNonNullObject = value != null ? typeof value === 'object' : false;
14
- return isNonNullObject && Object.prototype.toString.call(value) !== '[object RegExp]' && Object.prototype.toString.call(value) !== '[object Date]' && !Array.isArray(value);
14
+ return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value);
15
15
  }
16
16
  /**
17
17
  * Merges the source object into the target object.
@@ -28,9 +28,8 @@ function mergeObject(target, source, shouldRemoveNestedNulls = true) {
28
28
  // If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object
29
29
  // and therefore we need to omit keys where either the source or target value is null.
30
30
  if (targetObject) {
31
- const targetKeys = Object.keys(targetObject);
32
- for (let i = 0; i < targetKeys.length; ++i) {
33
- const key = targetKeys[i];
31
+ // eslint-disable-next-line no-restricted-syntax, guard-for-in
32
+ for (const key in targetObject) {
34
33
  const sourceValue = source === null || source === void 0 ? void 0 : source[key];
35
34
  const targetValue = targetObject === null || targetObject === void 0 ? void 0 : targetObject[key];
36
35
  // If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object.
@@ -46,9 +45,8 @@ function mergeObject(target, source, shouldRemoveNestedNulls = true) {
46
45
  }
47
46
  }
48
47
  // After copying over all keys from the target object, we want to merge the source object into the destination object.
49
- const sourceKeys = Object.keys(source);
50
- for (let i = 0; i < sourceKeys.length; ++i) {
51
- const key = sourceKeys[i];
48
+ // eslint-disable-next-line no-restricted-syntax, guard-for-in
49
+ for (const key in source) {
52
50
  const sourceValue = source === null || source === void 0 ? void 0 : source[key];
53
51
  const targetValue = targetObject === null || targetObject === void 0 ? void 0 : targetObject[key];
54
52
  // If undefined is passed as the source value for a key, we want to generally ignore it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "2.0.53",
3
+ "version": "2.0.55",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",