react-native-onyx 1.0.77 → 1.0.79

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
@@ -8,6 +8,7 @@ import createDeferredTask from './createDeferredTask';
8
8
  import fastMerge from './fastMerge';
9
9
  import * as PerformanceUtils from './metrics/PerformanceUtils';
10
10
  import Storage from './storage';
11
+ import unstable_batchedUpdates from './batch';
11
12
 
12
13
  // Method constants
13
14
  const METHOD = {
@@ -20,6 +21,7 @@ const METHOD = {
20
21
 
21
22
  // Key/value store of Onyx key and arrays of values to merge
22
23
  const mergeQueue = {};
24
+ const mergeQueuePromise = {};
23
25
 
24
26
  // Keeps track of the last connectionID that was used so we can keep incrementing it
25
27
  let lastConnectionID = 0;
@@ -333,8 +335,10 @@ function getCachedCollection(collectionKey) {
333
335
  * @private
334
336
  * @param {String} collectionKey
335
337
  * @param {Object} partialCollection - a partial collection of grouped member keys
338
+ * @param {boolean} [notifyRegularSubscibers=true]
339
+ * @param {boolean} [notifyWithOnyxSubscibers=true]
336
340
  */
337
- function keysChanged(collectionKey, partialCollection) {
341
+ function keysChanged(collectionKey, partialCollection, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true) {
338
342
  // We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or
339
343
  // 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
340
344
  // 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().
@@ -366,6 +370,10 @@ function keysChanged(collectionKey, partialCollection) {
366
370
 
367
371
  // Regular Onyx.connect() subscriber found.
368
372
  if (_.isFunction(subscriber.callback)) {
373
+ if (!notifyRegularSubscibers) {
374
+ continue;
375
+ }
376
+
369
377
  // If they are subscribed to the collection key and using waitForCollectionCallback then we'll
370
378
  // send the whole cached collection.
371
379
  if (isSubscribedToCollectionKey) {
@@ -396,6 +404,10 @@ function keysChanged(collectionKey, partialCollection) {
396
404
 
397
405
  // React component subscriber found.
398
406
  if (subscriber.withOnyxInstance) {
407
+ if (!notifyWithOnyxSubscibers) {
408
+ continue;
409
+ }
410
+
399
411
  // We are subscribed to a collection key so we must update the data in state with the new
400
412
  // collection member key values from the partial update.
401
413
  if (isSubscribedToCollectionKey) {
@@ -487,8 +499,10 @@ function keysChanged(collectionKey, partialCollection) {
487
499
  * @param {String} key
488
500
  * @param {*} data
489
501
  * @param {Function} [canUpdateSubscriber] only subscribers that pass this truth test will be updated
502
+ * @param {boolean} [notifyRegularSubscibers=true]
503
+ * @param {boolean} [notifyWithOnyxSubscibers=true]
490
504
  */
491
- function keyChanged(key, data, canUpdateSubscriber) {
505
+ function keyChanged(key, data, canUpdateSubscriber, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true) {
492
506
  // Add or remove this key from the recentlyAccessedKeys lists
493
507
  if (!_.isNull(data)) {
494
508
  addLastAccessedKey(key);
@@ -508,6 +522,9 @@ function keyChanged(key, data, canUpdateSubscriber) {
508
522
 
509
523
  // Subscriber is a regular call to connect() and provided a callback
510
524
  if (_.isFunction(subscriber.callback)) {
525
+ if (!notifyRegularSubscibers) {
526
+ continue;
527
+ }
511
528
  if (isCollectionKey(subscriber.key) && subscriber.waitForCollectionCallback) {
512
529
  const cachedCollection = getCachedCollection(subscriber.key);
513
530
  cachedCollection[key] = data;
@@ -521,6 +538,10 @@ function keyChanged(key, data, canUpdateSubscriber) {
521
538
 
522
539
  // Subscriber connected via withOnyx() HOC
523
540
  if (subscriber.withOnyxInstance) {
541
+ if (!notifyWithOnyxSubscibers) {
542
+ continue;
543
+ }
544
+
524
545
  // Check if we are subscribing to a collection key and overwrite the collection member key value in state
525
546
  if (isCollectionKey(subscriber.key)) {
526
547
  // If the subscriber has a selector, then the consumer of this data must only be given the data
@@ -801,22 +822,62 @@ function disconnect(connectionID, keyToRemoveFromEvictionBlocklist) {
801
822
  delete callbackToStateMapping[connectionID];
802
823
  }
803
824
 
825
+ let batchUpdatesPromise = null;
826
+ let batchUpdatesQueue = [];
827
+
828
+ /**
829
+ * We are batching together onyx updates. This helps with use cases where we schedule onyx updates after each other.
830
+ * This happens for example in the Onyx.update function, where we process API responses that might contain a lot of
831
+ * update operations. Instead of calling the subscribers for each update operation, we batch them together which will
832
+ * cause react to schedule the updates at once instead of after each other. This is mainly a performance optimization.
833
+ * @returns {Promise}
834
+ */
835
+ function maybeFlushBatchUpdates() {
836
+ if (batchUpdatesPromise) {
837
+ return batchUpdatesPromise;
838
+ }
839
+
840
+ batchUpdatesPromise = new Promise((resolve) => {
841
+ /* We use (setTimeout, 0) here which should be called once native module calls are flushed (usually at the end of the frame)
842
+ * We may investigate if (setTimeout, 1) (which in React Native is equal to requestAnimationFrame) works even better
843
+ * then the batch will be flushed on next frame.
844
+ */
845
+ setTimeout(() => {
846
+ const updatesCopy = batchUpdatesQueue;
847
+ batchUpdatesQueue = [];
848
+ batchUpdatesPromise = null;
849
+ unstable_batchedUpdates(() => {
850
+ updatesCopy.forEach((applyUpdates) => {
851
+ applyUpdates();
852
+ });
853
+ });
854
+
855
+ resolve();
856
+ }, 0);
857
+ });
858
+ return batchUpdatesPromise;
859
+ }
860
+
861
+ function batchUpdates(updates) {
862
+ batchUpdatesQueue.push(updates);
863
+ return maybeFlushBatchUpdates();
864
+ }
865
+
804
866
  /**
805
- * This method mostly exists for historical reasons as this library was initially designed without a memory cache and one was added later.
806
- * For this reason, Onyx works more similar to what you might expect from a native AsyncStorage with reads, writes, etc all becoming
807
- * available async. Since we have code in our main applications that might expect things to work this way it's not safe to change this
808
- * behavior just yet.
867
+ * Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately).
809
868
  *
810
869
  * @example
811
- * notifySubscribersOnNextTick(key, value, subscriber => subscriber.initWithStoredValues === false)
870
+ * scheduleSubscriberUpdate(key, value, subscriber => subscriber.initWithStoredValues === false)
812
871
  *
813
872
  * @param {String} key
814
873
  * @param {*} value
815
874
  * @param {Function} [canUpdateSubscriber] only subscribers that pass this truth test will be updated
875
+ * @returns {Promise}
816
876
  */
817
- // eslint-disable-next-line rulesdir/no-negated-variables
818
- function notifySubscribersOnNextTick(key, value, canUpdateSubscriber) {
819
- Promise.resolve().then(() => keyChanged(key, value, canUpdateSubscriber));
877
+ function scheduleSubscriberUpdate(key, value, canUpdateSubscriber) {
878
+ const promise = Promise.resolve().then(() => keyChanged(key, value, canUpdateSubscriber, true, false));
879
+ batchUpdates(() => keyChanged(key, value, canUpdateSubscriber, false, true));
880
+ return Promise.all([maybeFlushBatchUpdates(), promise]);
820
881
  }
821
882
 
822
883
  /**
@@ -826,10 +887,12 @@ function notifySubscribersOnNextTick(key, value, canUpdateSubscriber) {
826
887
  *
827
888
  * @param {String} key
828
889
  * @param {*} value
890
+ * @returns {Promise}
829
891
  */
830
- // eslint-disable-next-line rulesdir/no-negated-variables
831
- function notifyCollectionSubscribersOnNextTick(key, value) {
832
- Promise.resolve().then(() => keysChanged(key, value));
892
+ function scheduleNotifyCollectionSubscribers(key, value) {
893
+ const promise = Promise.resolve().then(() => keysChanged(key, value, true, false));
894
+ batchUpdates(() => keysChanged(key, value, false, true));
895
+ return Promise.all([maybeFlushBatchUpdates(), promise]);
833
896
  }
834
897
 
835
898
  /**
@@ -841,7 +904,7 @@ function notifyCollectionSubscribersOnNextTick(key, value) {
841
904
  */
842
905
  function remove(key) {
843
906
  cache.drop(key);
844
- notifySubscribersOnNextTick(key, null);
907
+ scheduleSubscriberUpdate(key, null);
845
908
  return Storage.removeItem(key);
846
909
  }
847
910
 
@@ -902,6 +965,7 @@ function evictStorageAndRetry(error, onyxMethod, ...args) {
902
965
  * @param {*} value
903
966
  * @param {Boolean} hasChanged
904
967
  * @param {String} method
968
+ * @returns {Promise}
905
969
  */
906
970
  function broadcastUpdate(key, value, hasChanged, method) {
907
971
  // Logging properties only since values could be sensitive things we don't want to log
@@ -915,7 +979,7 @@ function broadcastUpdate(key, value, hasChanged, method) {
915
979
  cache.addToAccessedKeys(key);
916
980
  }
917
981
 
918
- notifySubscribersOnNextTick(key, value, subscriber => hasChanged || subscriber.initWithStoredValues === false);
982
+ return scheduleSubscriberUpdate(key, value, subscriber => hasChanged || subscriber.initWithStoredValues === false);
919
983
  }
920
984
 
921
985
  /**
@@ -966,15 +1030,16 @@ function set(key, value) {
966
1030
  const hasChanged = cache.hasValueChanged(key, valueWithNullRemoved);
967
1031
 
968
1032
  // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
969
- broadcastUpdate(key, valueWithNullRemoved, hasChanged, 'set');
1033
+ const updatePromise = broadcastUpdate(key, valueWithNullRemoved, hasChanged, 'set');
970
1034
 
971
1035
  // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
972
1036
  if (!hasChanged) {
973
- return Promise.resolve();
1037
+ return updatePromise;
974
1038
  }
975
1039
 
976
1040
  return Storage.setItem(key, valueWithNullRemoved)
977
- .catch(error => evictStorageAndRetry(error, set, key, valueWithNullRemoved));
1041
+ .catch(error => evictStorageAndRetry(error, set, key, valueWithNullRemoved))
1042
+ .then(() => updatePromise);
978
1043
  }
979
1044
 
980
1045
  /**
@@ -1000,14 +1065,15 @@ function prepareKeyValuePairsForStorage(data) {
1000
1065
  function multiSet(data) {
1001
1066
  const keyValuePairs = prepareKeyValuePairsForStorage(data);
1002
1067
 
1003
- _.each(data, (val, key) => {
1068
+ const updatePromises = _.map(data, (val, key) => {
1004
1069
  // Update cache and optimistically inform subscribers on the next tick
1005
1070
  cache.set(key, val);
1006
- notifySubscribersOnNextTick(key, val);
1071
+ return scheduleSubscriberUpdate(key, val);
1007
1072
  });
1008
1073
 
1009
1074
  return Storage.multiSet(keyValuePairs)
1010
- .catch(error => evictStorageAndRetry(error, multiSet, data));
1075
+ .catch(error => evictStorageAndRetry(error, multiSet, data))
1076
+ .then(() => Promise.all(updatePromises));
1011
1077
  }
1012
1078
 
1013
1079
  /**
@@ -1063,19 +1129,19 @@ function merge(key, changes) {
1063
1129
  // Using the initial value from storage in subsequent merge attempts will lead to an incorrect final merged value.
1064
1130
  if (mergeQueue[key]) {
1065
1131
  mergeQueue[key].push(changes);
1066
- return Promise.resolve();
1132
+ return mergeQueuePromise[key];
1067
1133
  }
1068
1134
  mergeQueue[key] = [changes];
1069
1135
 
1070
- return get(key)
1136
+ mergeQueuePromise[key] = get(key)
1071
1137
  .then((existingValue) => {
1072
1138
  try {
1073
1139
  // We first only merge the changes, so we can provide these to the native implementation (SQLite uses only delta changes in "JSON_PATCH" to merge)
1074
1140
  let batchedChanges = applyMerge(undefined, mergeQueue[key]);
1075
1141
 
1076
- // Clean up the write queue so we
1077
- // don't apply these changes again
1142
+ // Clean up the write queue, so we don't apply these changes again
1078
1143
  delete mergeQueue[key];
1144
+ delete mergeQueuePromise[key];
1079
1145
 
1080
1146
  // After that we merge the batched changes with the existing value
1081
1147
  const modifiedData = removeNullObjectValues(applyMerge(existingValue, [batchedChanges]));
@@ -1091,20 +1157,22 @@ function merge(key, changes) {
1091
1157
  const hasChanged = cache.hasValueChanged(key, modifiedData);
1092
1158
 
1093
1159
  // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
1094
- broadcastUpdate(key, modifiedData, hasChanged, 'merge');
1160
+ const updatePromise = broadcastUpdate(key, modifiedData, hasChanged, 'merge');
1095
1161
 
1096
1162
  // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
1097
1163
  if (!hasChanged) {
1098
- return Promise.resolve();
1164
+ return updatePromise;
1099
1165
  }
1100
1166
 
1101
- return Storage.mergeItem(key, batchedChanges, modifiedData);
1167
+ return Storage.mergeItem(key, batchedChanges, modifiedData)
1168
+ .then(() => updatePromise);
1102
1169
  } catch (error) {
1103
1170
  Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
1171
+ return Promise.resolve();
1104
1172
  }
1105
-
1106
- return Promise.resolve();
1107
1173
  });
1174
+
1175
+ return mergeQueuePromise[key];
1108
1176
  }
1109
1177
 
1110
1178
  /**
@@ -1190,19 +1258,21 @@ function clear(keysToPreserve = []) {
1190
1258
  keysToBeClearedFromStorage.push(key);
1191
1259
  });
1192
1260
 
1261
+ const updatePromises = [];
1262
+
1193
1263
  // Notify the subscribers for each key/value group so they can receive the new values
1194
1264
  _.each(keyValuesToResetIndividually, (value, key) => {
1195
- notifySubscribersOnNextTick(key, value);
1265
+ updatePromises.push(scheduleSubscriberUpdate(key, value));
1196
1266
  });
1197
1267
  _.each(keyValuesToResetAsCollection, (value, key) => {
1198
- notifyCollectionSubscribersOnNextTick(key, value);
1268
+ updatePromises.push(scheduleNotifyCollectionSubscribers(key, value));
1199
1269
  });
1200
1270
 
1201
1271
  const defaultKeyValuePairs = _.pairs(_.omit(defaultKeyStates, keysToPreserve));
1202
1272
 
1203
1273
  // Remove only the items that we want cleared from storage, and reset others to default
1204
1274
  _.each(keysToBeClearedFromStorage, key => cache.drop(key));
1205
- return Storage.removeItems(keysToBeClearedFromStorage).then(() => Storage.multiSet(defaultKeyValuePairs));
1275
+ return Storage.removeItems(keysToBeClearedFromStorage).then(() => Storage.multiSet(defaultKeyValuePairs)).then(() => Promise.all(updatePromises));
1206
1276
  });
1207
1277
  }
1208
1278
 
@@ -1273,13 +1343,14 @@ function mergeCollection(collectionKey, collection) {
1273
1343
 
1274
1344
  // Prefill cache if necessary by calling get() on any existing keys and then merge original data to cache
1275
1345
  // and update all subscribers
1276
- Promise.all(_.map(existingKeys, get)).then(() => {
1346
+ const promiseUpdate = Promise.all(_.map(existingKeys, get)).then(() => {
1277
1347
  cache.merge(collection);
1278
- keysChanged(collectionKey, collection);
1348
+ return scheduleNotifyCollectionSubscribers(collectionKey, collection);
1279
1349
  });
1280
1350
 
1281
1351
  return Promise.all(promises)
1282
- .catch(error => evictStorageAndRetry(error, mergeCollection, collection));
1352
+ .catch(error => evictStorageAndRetry(error, mergeCollection, collection))
1353
+ .then(() => promiseUpdate);
1283
1354
  });
1284
1355
  }
1285
1356
 
package/lib/batch.js ADDED
@@ -0,0 +1,3 @@
1
+ import {unstable_batchedUpdates} from 'react-dom';
2
+
3
+ export default unstable_batchedUpdates;
@@ -0,0 +1,3 @@
1
+ import {unstable_batchedUpdates} from 'react-native';
2
+
3
+ export default unstable_batchedUpdates;
@@ -88,10 +88,13 @@ const provider = {
88
88
  ON CONFLICT DO UPDATE
89
89
  SET valueJSON = JSON_PATCH(valueJSON, JSON(:value));
90
90
  `;
91
- const queryArguments = _.map(pairs, (pair) => {
91
+
92
+ const nonNullishPairs = _.filter(pairs, pair => !_.isUndefined(pair[1]));
93
+ const queryArguments = _.map(nonNullishPairs, (pair) => {
92
94
  const value = JSON.stringify(pair[1]);
93
95
  return [pair[0], value];
94
96
  });
97
+
95
98
  return db.executeBatchAsync([[query, queryArguments]]);
96
99
  },
97
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "1.0.77",
3
+ "version": "1.0.79",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",
@@ -66,6 +66,7 @@
66
66
  "metro-react-native-babel-preset": "^0.72.3",
67
67
  "prop-types": "^15.7.2",
68
68
  "react": "18.2.0",
69
+ "react-dom": "^18.1.0",
69
70
  "react-native": "0.71.2",
70
71
  "react-native-device-info": "^10.3.0",
71
72
  "react-native-performance": "^2.0.0",
@@ -79,6 +80,7 @@
79
80
  "peerDependencies": {
80
81
  "idb-keyval": "^6.2.1",
81
82
  "react": ">=18.1.0",
83
+ "react-dom": ">=18.1.0",
82
84
  "react-native-performance": "^5.1.0",
83
85
  "react-native-quick-sqlite": "^8.0.0-beta.2",
84
86
  "react-native-device-info": "^10.3.0"