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/dist/web.development.js +146 -44
- package/dist/web.development.js.map +1 -1
- package/dist/web.min.js +1 -1
- package/dist/web.min.js.map +1 -1
- package/lib/Onyx.js +107 -36
- package/lib/batch.js +3 -0
- package/lib/batch.native.js +3 -0
- package/lib/storage/providers/SQLiteStorage.js +4 -1
- package/package.json +3 -1
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
_.
|
|
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
|
-
|
|
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
|
|
1132
|
+
return mergeQueuePromise[key];
|
|
1067
1133
|
}
|
|
1068
1134
|
mergeQueue[key] = [changes];
|
|
1069
1135
|
|
|
1070
|
-
|
|
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
|
|
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
|
-
|
|
1265
|
+
updatePromises.push(scheduleSubscriberUpdate(key, value));
|
|
1196
1266
|
});
|
|
1197
1267
|
_.each(keyValuesToResetAsCollection, (value, key) => {
|
|
1198
|
-
|
|
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
|
-
|
|
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
|
@@ -88,10 +88,13 @@ const provider = {
|
|
|
88
88
|
ON CONFLICT DO UPDATE
|
|
89
89
|
SET valueJSON = JSON_PATCH(valueJSON, JSON(:value));
|
|
90
90
|
`;
|
|
91
|
-
|
|
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.
|
|
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"
|