react-native-onyx 1.0.16 → 1.0.18

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/lib/Onyx.js CHANGED
@@ -1,13 +1,12 @@
1
+ /* eslint-disable no-continue */
1
2
  import _ from 'underscore';
2
3
  import Str from 'expensify-common/lib/str';
3
- import lodashMerge from 'lodash/merge';
4
- import lodashMergeWith from 'lodash/mergeWith';
5
4
  import lodashGet from 'lodash/get';
6
5
  import Storage from './storage';
7
6
  import * as Logger from './Logger';
8
7
  import cache from './OnyxCache';
9
8
  import createDeferredTask from './createDeferredTask';
10
- import customizerForMergeWith from './customizerForMergeWith';
9
+ import mergeWithCustomized from './mergeWithCustomized';
11
10
 
12
11
  // Keeps track of the last connectionID that was used so we can keep incrementing it
13
12
  let lastConnectionID = 0;
@@ -108,8 +107,17 @@ function isCollectionKey(key) {
108
107
  }
109
108
 
110
109
  /**
111
- * Checks to see if a given key matches with the
112
- * configured key of our connected subscriber
110
+ * @param {String} collectionKey
111
+ * @param {String} key
112
+ * @returns {Boolean}
113
+ */
114
+ function isCollectionMemberKey(collectionKey, key) {
115
+ return Str.startsWith(key, collectionKey) && key.length > collectionKey.length;
116
+ }
117
+
118
+ /**
119
+ * Checks to see if a provided key is the exact configured key of our connected subscriber
120
+ * or if the provided key is a collection member key (in case our configured key is a "collection key")
113
121
  *
114
122
  * @private
115
123
  * @param {String} configKey
@@ -226,7 +234,7 @@ function addAllSafeEvictionKeysToRecentlyAccessedList() {
226
234
  */
227
235
  function getCachedCollection(collectionKey) {
228
236
  const collectionMemberKeys = _.filter(cache.getAllKeys(), (
229
- storedKey => isKeyMatch(collectionKey, storedKey)
237
+ storedKey => isCollectionMemberKey(collectionKey, storedKey)
230
238
  ));
231
239
 
232
240
  return _.reduce(collectionMemberKeys, (prev, curr) => {
@@ -246,73 +254,103 @@ function getCachedCollection(collectionKey) {
246
254
  *
247
255
  * @private
248
256
  * @param {String} collectionKey
249
- * @param {Object} collection
257
+ * @param {Object} partialCollection - a partial collection of grouped member keys
250
258
  */
251
- function keysChanged(collectionKey, collection) {
252
- // Find all subscribers that were added with connect() and trigger the callback or setState() with the new data
253
- _.each(callbackToStateMapping, (subscriber) => {
259
+ function keysChanged(collectionKey, partialCollection) {
260
+ // We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or
261
+ // individual collection key member for the collection that is being updated. It is important to note that the collection parameter cane be a PARTIAL collection
262
+ // and does not represent all of the combined keys and values for a collection key. It is just the "new" data that was merged in via mergeCollection().
263
+ const stateMappingKeys = _.keys(callbackToStateMapping);
264
+ for (let i = stateMappingKeys.length; i--;) {
265
+ const subscriber = callbackToStateMapping[stateMappingKeys[i]];
254
266
  if (!subscriber) {
255
- return;
267
+ continue;
268
+ }
269
+
270
+ // Skip iteration if we do not have a collection key or a collection member key on this subscriber
271
+ if (!Str.startsWith(subscriber.key, collectionKey)) {
272
+ continue;
256
273
  }
257
274
 
258
275
  /**
259
276
  * e.g. Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT, callback: ...});
260
277
  */
261
- const isSubscribedToCollectionKey = isKeyMatch(subscriber.key, collectionKey)
262
- && isCollectionKey(subscriber.key);
278
+ const isSubscribedToCollectionKey = subscriber.key === collectionKey;
263
279
 
264
280
  /**
265
281
  * e.g. Onyx.connect({key: `${ONYXKEYS.COLLECTION.REPORT}{reportID}`, callback: ...});
266
282
  */
267
- const isSubscribedToCollectionMemberKey = subscriber.key.startsWith(collectionKey);
283
+ const isSubscribedToCollectionMemberKey = isCollectionMemberKey(collectionKey, subscriber.key);
268
284
 
269
- if (isSubscribedToCollectionKey) {
270
- if (_.isFunction(subscriber.callback)) {
271
- const cachedCollection = getCachedCollection(collectionKey);
285
+ // We prepare the "cached collection" which is the entire collection + the new partial data that
286
+ // was merged in via mergeCollection().
287
+ const cachedCollection = getCachedCollection(collectionKey);
272
288
 
289
+ // Regular Onyx.connect() subscriber found.
290
+ if (_.isFunction(subscriber.callback)) {
291
+ // If they are subscribed to the collection key and using waitForCollectionCallback then we'll
292
+ // send the whole cached collection.
293
+ if (isSubscribedToCollectionKey) {
273
294
  if (subscriber.waitForCollectionCallback) {
274
295
  subscriber.callback(cachedCollection);
275
- return;
296
+ continue;
276
297
  }
277
298
 
278
- _.each(collection, (data, dataKey) => {
299
+ // If they are not using waitForCollectionCallback then we notify the subscriber with
300
+ // the new merged data but only for any keys in the partial collection.
301
+ const dataKeys = _.keys(partialCollection);
302
+ for (let j = 0; j < dataKeys.length; j++) {
303
+ const dataKey = dataKeys[j];
279
304
  subscriber.callback(cachedCollection[dataKey], dataKey);
280
- });
281
- } else if (subscriber.withOnyxInstance) {
305
+ }
306
+ continue;
307
+ }
308
+
309
+ // And if the subscriber is specifically only tracking a particular collection member key then we will
310
+ // notify them with the cached data for that key only.
311
+ if (isSubscribedToCollectionMemberKey) {
312
+ subscriber.callback(cachedCollection[subscriber.key], subscriber.key);
313
+ continue;
314
+ }
315
+
316
+ continue;
317
+ }
318
+
319
+ // React component subscriber found.
320
+ if (subscriber.withOnyxInstance) {
321
+ // We are subscribed to a collection key so we must update the data in state with the new
322
+ // collection member key values from the partial update.
323
+ if (isSubscribedToCollectionKey) {
282
324
  subscriber.withOnyxInstance.setState((prevState) => {
283
325
  const finalCollection = _.clone(prevState[subscriber.statePropertyName] || {});
284
- _.each(collection, (data, dataKey) => {
285
- if (finalCollection[dataKey]) {
286
- lodashMerge(finalCollection[dataKey], data);
287
- } else {
288
- finalCollection[dataKey] = data;
289
- }
290
- });
291
326
 
327
+ const dataKeys = _.keys(partialCollection);
328
+ for (let j = 0; j < dataKeys.length; j++) {
329
+ const dataKey = dataKeys[j];
330
+ finalCollection[dataKey] = cachedCollection[dataKey];
331
+ }
292
332
  return {
293
333
  [subscriber.statePropertyName]: finalCollection,
294
334
  };
295
335
  });
336
+ continue;
296
337
  }
297
- } else if (isSubscribedToCollectionMemberKey) {
298
- const dataFromCollection = collection[subscriber.key];
299
338
 
300
- // If `dataFromCollection` happens to not exist, then return early so that there are no unnecessary
301
- // re-renderings of the component
302
- if (_.isUndefined(dataFromCollection)) {
303
- return;
304
- }
339
+ // If a React component is only interested in a single key then we can set the cached value directly to the state name.
340
+ if (isSubscribedToCollectionMemberKey) {
341
+ // However, we only want to update this subscriber if the partial data contains a change.
342
+ // Otherwise, we would update them with a value they already have and trigger an unnecessary re-render.
343
+ const dataFromCollection = partialCollection[subscriber.key];
344
+ if (_.isUndefined(dataFromCollection)) {
345
+ continue;
346
+ }
305
347
 
306
- subscriber.withOnyxInstance.setState(prevState => ({
307
- [subscriber.statePropertyName]: _.isObject(dataFromCollection)
308
- ? {
309
- ...prevState[subscriber.statePropertyName],
310
- ...dataFromCollection,
311
- }
312
- : dataFromCollection,
313
- }));
348
+ subscriber.withOnyxInstance.setState({
349
+ [subscriber.statePropertyName]: cachedCollection[subscriber.key],
350
+ });
351
+ }
314
352
  }
315
- });
353
+ }
316
354
  }
317
355
 
318
356
  /**
@@ -330,43 +368,54 @@ function keyChanged(key, data) {
330
368
  removeLastAccessedKey(key);
331
369
  }
332
370
 
333
- // Find all subscribers that were added with connect() and trigger the callback or setState() with the new data
334
- _.each(callbackToStateMapping, (subscriber) => {
371
+ // 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
372
+ // 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
373
+ // 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.
374
+ const stateMappingKeys = _.keys(callbackToStateMapping);
375
+ for (let i = stateMappingKeys.length; i--;) {
376
+ const subscriber = callbackToStateMapping[stateMappingKeys[i]];
335
377
  if (!subscriber || !isKeyMatch(subscriber.key, key)) {
336
- return;
378
+ continue;
337
379
  }
338
380
 
381
+ // Subscriber is a regular call to connect() and provided a callback
339
382
  if (_.isFunction(subscriber.callback)) {
340
- if (subscriber.waitForCollectionCallback) {
383
+ if (isCollectionKey(subscriber.key) && subscriber.waitForCollectionCallback) {
341
384
  const cachedCollection = getCachedCollection(subscriber.key);
342
385
  cachedCollection[key] = data;
343
386
  subscriber.callback(cachedCollection);
344
- return;
387
+ continue;
345
388
  }
346
389
 
347
390
  subscriber.callback(data, key);
348
- return;
391
+ continue;
349
392
  }
350
393
 
351
- if (!subscriber.withOnyxInstance) {
352
- return;
353
- }
394
+ // Subscriber connected via withOnyx() HOC
395
+ if (subscriber.withOnyxInstance) {
396
+ // Check if we are subscribing to a collection key and overwrite the collection member key value in state
397
+ if (isCollectionKey(subscriber.key)) {
398
+ subscriber.withOnyxInstance.setState((prevState) => {
399
+ const collection = prevState[subscriber.statePropertyName] || {};
400
+ return {
401
+ [subscriber.statePropertyName]: {
402
+ ...collection,
403
+ [key]: data,
404
+ },
405
+ };
406
+ });
407
+ continue;
408
+ }
354
409
 
355
- // Check if we are subscribing to a collection key and add this item as a collection
356
- if (isCollectionKey(subscriber.key)) {
357
- subscriber.withOnyxInstance.setState((prevState) => {
358
- const collection = _.clone(prevState[subscriber.statePropertyName] || {});
359
- collection[key] = data;
360
- return {
361
- [subscriber.statePropertyName]: collection,
362
- };
363
- });
364
- } else {
410
+ // If we did not match on a collection key then we just set the new data to the state property
365
411
  subscriber.withOnyxInstance.setState({
366
412
  [subscriber.statePropertyName]: data,
367
413
  });
414
+ continue;
368
415
  }
369
- });
416
+
417
+ console.error('Warning: Found a matching subscriber to a key that changed, but no callback or withOnyxInstance could be found.');
418
+ }
370
419
  }
371
420
 
372
421
  /**
@@ -380,9 +429,9 @@ function keyChanged(key, data) {
380
429
  * @param {string} [config.statePropertyName]
381
430
  * @param {function} [config.callback]
382
431
  * @param {*|null} val
383
- * @param {String} key
432
+ * @param {String} matchedKey
384
433
  */
385
- function sendDataToConnection(config, val, key) {
434
+ function sendDataToConnection(config, val, matchedKey) {
386
435
  // If the mapping no longer exists then we should not send any data.
387
436
  // This means our subscriber disconnected or withOnyx wrapped component unmounted.
388
437
  if (!callbackToStateMapping[config.connectionID]) {
@@ -392,10 +441,54 @@ function sendDataToConnection(config, val, key) {
392
441
  if (config.withOnyxInstance) {
393
442
  config.withOnyxInstance.setWithOnyxState(config.statePropertyName, val);
394
443
  } else if (_.isFunction(config.callback)) {
395
- config.callback(val, key);
444
+ config.callback(val, matchedKey);
396
445
  }
397
446
  }
398
447
 
448
+ /**
449
+ * We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we
450
+ * run out of storage the least recently accessed key can be removed.
451
+ *
452
+ * @private
453
+ * @param {Object} mapping
454
+ */
455
+ function addKeyToRecentlyAccessedIfNeeded(mapping) {
456
+ if (!isSafeEvictionKey(mapping.key)) {
457
+ return;
458
+ }
459
+
460
+ // Try to free some cache whenever we connect to a safe eviction key
461
+ cache.removeLeastRecentlyUsedKeys();
462
+
463
+ if (mapping.withOnyxInstance && !isCollectionKey(mapping.key)) {
464
+ // All React components subscribing to a key flagged as a safe eviction key must implement the canEvict property.
465
+ if (_.isUndefined(mapping.canEvict)) {
466
+ throw new Error(
467
+ `Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`,
468
+ );
469
+ }
470
+
471
+ addLastAccessedKey(mapping.key);
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.
477
+ *
478
+ * @private
479
+ * @param {Array} matchingKeys
480
+ * @param {Object} mapping
481
+ */
482
+ function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
483
+ Promise.all(_.map(matchingKeys, key => get(key)))
484
+ .then(values => _.reduce(values, (finalObject, value, i) => {
485
+ // eslint-disable-next-line no-param-reassign
486
+ finalObject[matchingKeys[i]] = value;
487
+ return finalObject;
488
+ }, {}))
489
+ .then(val => sendDataToConnection(mapping, val));
490
+ }
491
+
399
492
  /**
400
493
  * Subscribes a react component's state directly to a store key
401
494
  *
@@ -428,58 +521,63 @@ function connect(mapping) {
428
521
 
429
522
  // Commit connection only after init passes
430
523
  deferredInitTask.promise
431
- .then(() => {
432
- // Check to see if this key is flagged as a safe eviction key and add it to the recentlyAccessedKeys list
433
- if (!isSafeEvictionKey(mapping.key)) {
524
+ .then(() => addKeyToRecentlyAccessedIfNeeded(mapping))
525
+ .then(getAllKeys)
526
+ .then((keys) => {
527
+ // We search all the keys in storage to see if any are a "match" for the subscriber we are connecting so that we
528
+ // can send data back to the subscriber. Note that multiple keys can match as a subscriber could either be
529
+ // subscribed to a "collection key" or a single key.
530
+ const matchingKeys = _.filter(keys, key => isKeyMatch(mapping.key, key));
531
+
532
+ // If the key being connected to does not exist we initialize the value with null. For subscribers that connected
533
+ // directly via connect() they will simply get a null value sent to them without any information about which key matched
534
+ // since there are none matched. In withOnyx() we wait for all connected keys to return a value before rendering the child
535
+ // component. This null value will be filtered out so that the connected component can utilize defaultProps.
536
+ if (matchingKeys.length === 0) {
537
+ sendDataToConnection(mapping, null);
434
538
  return;
435
539
  }
436
540
 
437
- // Try to free some cache whenever we connect to a safe eviction key
438
- cache.removeLeastRecentlyUsedKeys();
541
+ // When using a callback subscriber we will either trigger the provided callback for each key we find or combine all values
542
+ // into an object and just make a single call. The latter behavior is enabled by providing a waitForCollectionCallback key
543
+ // combined with a subscription to a collection key.
544
+ if (_.isFunction(mapping.callback)) {
545
+ if (isCollectionKey(mapping.key)) {
546
+ if (mapping.waitForCollectionCallback) {
547
+ getCollectionDataAndSendAsObject(matchingKeys, mapping);
548
+ return;
549
+ }
439
550
 
440
- if (mapping.withOnyxInstance && !isCollectionKey(mapping.key)) {
441
- // All React components subscribing to a key flagged as a safe eviction
442
- // key must implement the canEvict property.
443
- if (_.isUndefined(mapping.canEvict)) {
444
- throw new Error(
445
- `Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`,
446
- );
551
+ // We did not opt into using waitForCollectionCallback mode so the callback is called for every matching key.
552
+ for (let i = 0; i < matchingKeys.length; i++) {
553
+ get(matchingKeys[i]).then(val => sendDataToConnection(mapping, val, matchingKeys[i]));
554
+ }
555
+ return;
447
556
  }
448
557
 
449
- addLastAccessedKey(mapping.key);
558
+ // If we are not subscribed to a collection key then there's only a single key to send an update for.
559
+ get(mapping.key).then(val => sendDataToConnection(mapping, val, mapping.key));
560
+ return;
450
561
  }
451
- })
452
- .then(getAllKeys)
453
- .then((keys) => {
454
- // Find all the keys matched by the config key
455
- const matchingKeys = _.filter(keys, key => isKeyMatch(mapping.key, key));
456
562
 
457
- // If the key being connected to does not exist, initialize the value with null
458
- if (matchingKeys.length === 0) {
459
- sendDataToConnection(mapping, null);
563
+ // If we have a withOnyxInstance that means a React component has subscribed via the withOnyx() HOC and we need to
564
+ // group collection key member data into an object.
565
+ if (mapping.withOnyxInstance) {
566
+ if (isCollectionKey(mapping.key)) {
567
+ getCollectionDataAndSendAsObject(matchingKeys, mapping);
568
+ return;
569
+ }
570
+
571
+ // If the subscriber is not using a collection key then we just send a single value back to the subscriber
572
+ get(mapping.key).then(val => sendDataToConnection(mapping, val, mapping.key));
460
573
  return;
461
574
  }
462
575
 
463
- // When using a callback subscriber we will trigger the callback
464
- // for each key we find. It's up to the subscriber to know whether
465
- // to expect a single key or multiple keys in the case of a collection.
466
- // React components are an exception since we'll want to send their
467
- // initial data as a single object when using collection keys.
468
- if ((mapping.withOnyxInstance && isCollectionKey(mapping.key)) || mapping.waitForCollectionCallback) {
469
- Promise.all(_.map(matchingKeys, key => get(key)))
470
- .then(values => _.reduce(values, (finalObject, value, i) => {
471
- // eslint-disable-next-line no-param-reassign
472
- finalObject[matchingKeys[i]] = value;
473
- return finalObject;
474
- }, {}))
475
- .then(val => sendDataToConnection(mapping, val));
476
- } else {
477
- _.each(matchingKeys, (key) => {
478
- get(key).then(val => sendDataToConnection(mapping, val, key));
479
- });
480
- }
576
+ console.error('Warning: Onyx.connect() was found without a callback or withOnyxInstance');
481
577
  });
482
578
 
579
+ // The connectionID is returned back to the caller so that it can be used to clean up the connection when it's no longer needed
580
+ // by calling Onyx.disconnect(connectionID).
483
581
  return connectionID;
484
582
  }
485
583
 
@@ -663,7 +761,7 @@ function applyMerge(key, data) {
663
761
  if (_.isObject(data) || _.every(mergeValues, _.isObject)) {
664
762
  // Object values are merged one after the other
665
763
  return _.reduce(mergeValues, (modifiedData, mergeValue) => {
666
- const newData = lodashMergeWith({}, modifiedData, mergeValue, customizerForMergeWith);
764
+ const newData = mergeWithCustomized({}, modifiedData, mergeValue);
667
765
 
668
766
  // We will also delete any object keys that are undefined or null.
669
767
  // Deleting keys is not supported by AsyncStorage so we do it this way.
@@ -734,7 +832,7 @@ function initializeWithDefaultKeyStates() {
734
832
  .then((pairs) => {
735
833
  const asObject = _.object(pairs);
736
834
 
737
- const merged = lodashMergeWith(asObject, defaultKeyStates, customizerForMergeWith);
835
+ const merged = mergeWithCustomized(asObject, defaultKeyStates);
738
836
  cache.merge(merged);
739
837
  _.each(merged, (val, key) => keyChanged(key, val));
740
838
  });
@@ -789,7 +887,7 @@ function clear() {
789
887
  */
790
888
  function mergeCollection(collectionKey, collection) {
791
889
  // Confirm all the collection keys belong to the same parent
792
- _.each(collection, (data, dataKey) => {
890
+ _.each(collection, (_data, dataKey) => {
793
891
  if (isKeyMatch(collectionKey, dataKey)) {
794
892
  return;
795
893
  }
package/lib/OnyxCache.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import _ from 'underscore';
2
- import lodashMergeWith from 'lodash/mergeWith';
3
- import customizerForMergeWith from './customizerForMergeWith';
2
+ import mergeWithCustomized from './mergeWithCustomized';
4
3
 
5
4
  const isDefined = _.negate(_.isUndefined);
6
5
 
@@ -111,7 +110,7 @@ class OnyxCache {
111
110
  * @param {Record<string, *>} data - a map of (cache) key - values
112
111
  */
113
112
  merge(data) {
114
- this.storageMap = lodashMergeWith({}, this.storageMap, data, customizerForMergeWith);
113
+ this.storageMap = mergeWithCustomized({}, this.storageMap, data);
115
114
 
116
115
  const storageKeys = this.getAllKeys();
117
116
  const mergedKeys = _.keys(data);
@@ -1,4 +1,4 @@
1
- import _ from 'underscore';
1
+ import lodashMergeWith from 'lodash/mergeWith';
2
2
 
3
3
  /**
4
4
  * When merging 2 objects into onyx that contain an array, we want to completely replace the array instead of the default
@@ -13,9 +13,14 @@ import _ from 'underscore';
13
13
  */
14
14
  // eslint-disable-next-line rulesdir/prefer-early-return
15
15
  function customizerForMergeWith(objValue, srcValue) {
16
- if (_.isArray(objValue)) {
16
+ // eslint-disable-next-line rulesdir/prefer-underscore-method
17
+ if (Array.isArray(objValue)) {
17
18
  return srcValue;
18
19
  }
19
20
  }
20
21
 
21
- export default customizerForMergeWith;
22
+ function mergeWithCustomized(...args) {
23
+ return lodashMergeWith(...args, customizerForMergeWith);
24
+ }
25
+
26
+ export default mergeWithCustomized;
@@ -6,9 +6,8 @@
6
6
 
7
7
  import localforage from 'localforage';
8
8
  import _ from 'underscore';
9
- import lodashMergeWith from 'lodash/mergeWith';
10
9
  import SyncQueue from '../../SyncQueue';
11
- import customizerForMergeWith from '../../customizerForMergeWith';
10
+ import mergeWithCustomized from '../../mergeWithCustomized';
12
11
 
13
12
  localforage.config({
14
13
  name: 'OnyxDB',
@@ -25,7 +24,7 @@ const provider = {
25
24
  return localforage.getItem(key)
26
25
  .then((existingValue) => {
27
26
  const newValue = _.isObject(existingValue)
28
- ? lodashMergeWith({}, existingValue, value, customizerForMergeWith)
27
+ ? mergeWithCustomized({}, existingValue, value)
29
28
  : value;
30
29
  return localforage.setItem(key, newValue);
31
30
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",