react-native-onyx 1.0.119 → 1.0.121
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/README.md +7 -0
- package/dist/web.development.js +408 -113
- 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/ActiveClientManager/index.d.ts +22 -0
- package/lib/ActiveClientManager/index.native.js +23 -0
- package/lib/ActiveClientManager/index.web.js +99 -0
- package/lib/Logger.js +1 -5
- package/lib/MDTable.js +11 -14
- package/lib/Onyx.d.ts +11 -4
- package/lib/Onyx.js +344 -232
- package/lib/OnyxCache.js +12 -3
- package/lib/Str.js +17 -4
- package/lib/broadcast/index.d.ts +17 -0
- package/lib/broadcast/index.native.js +14 -0
- package/lib/broadcast/index.web.js +35 -0
- package/lib/compose.js +6 -2
- package/lib/metrics/PerformanceUtils.js +2 -7
- package/lib/metrics/index.native.js +28 -41
- package/lib/metrics/index.web.js +4 -7
- package/lib/storage/WebStorage.js +6 -11
- package/lib/storage/__mocks__/index.js +2 -2
- package/lib/storage/providers/IDBKeyVal.js +27 -37
- package/lib/storage/providers/SQLiteStorage.js +58 -62
- package/lib/types.d.ts +1 -13
- package/lib/utils.d.ts +2 -6
- package/lib/utils.js +19 -22
- package/lib/withOnyx.d.ts +8 -32
- package/lib/withOnyx.js +37 -34
- package/package.json +15 -6
package/lib/Onyx.js
CHANGED
|
@@ -7,6 +7,8 @@ import * as Str from './Str';
|
|
|
7
7
|
import createDeferredTask from './createDeferredTask';
|
|
8
8
|
import * as PerformanceUtils from './metrics/PerformanceUtils';
|
|
9
9
|
import Storage from './storage';
|
|
10
|
+
import * as Broadcast from './broadcast';
|
|
11
|
+
import * as ActiveClientManager from './ActiveClientManager';
|
|
10
12
|
import utils from './utils';
|
|
11
13
|
import unstable_batchedUpdates from './batch';
|
|
12
14
|
|
|
@@ -19,6 +21,8 @@ const METHOD = {
|
|
|
19
21
|
CLEAR: 'clear',
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
const ON_CLEAR = 'on_clear';
|
|
25
|
+
|
|
22
26
|
// Key/value store of Onyx key and arrays of values to merge
|
|
23
27
|
const mergeQueue = {};
|
|
24
28
|
const mergeQueuePromise = {};
|
|
@@ -49,6 +53,12 @@ let defaultKeyStates = {};
|
|
|
49
53
|
// Connections can be made before `Onyx.init`. They would wait for this task before resolving
|
|
50
54
|
const deferredInitTask = createDeferredTask();
|
|
51
55
|
|
|
56
|
+
// The promise of the clear function, saved so that no writes happen while it's executing
|
|
57
|
+
let isClearing = false;
|
|
58
|
+
|
|
59
|
+
// Callback to be executed after the clear execution ends
|
|
60
|
+
let onClearCallback = null;
|
|
61
|
+
|
|
52
62
|
let batchUpdatesPromise = null;
|
|
53
63
|
let batchUpdatesQueue = [];
|
|
54
64
|
|
|
@@ -108,12 +118,17 @@ const getSubsetOfData = (sourceData, selector, withOnyxInstanceState) => selecto
|
|
|
108
118
|
* @param {Object} [withOnyxInstanceState]
|
|
109
119
|
* @returns {Object}
|
|
110
120
|
*/
|
|
111
|
-
const reduceCollectionWithSelector = (collection, selector, withOnyxInstanceState) =>
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
const reduceCollectionWithSelector = (collection, selector, withOnyxInstanceState) =>
|
|
122
|
+
_.reduce(
|
|
123
|
+
collection,
|
|
124
|
+
(finalCollection, item, key) => {
|
|
125
|
+
// eslint-disable-next-line no-param-reassign
|
|
126
|
+
finalCollection[key] = getSubsetOfData(item, selector, withOnyxInstanceState);
|
|
114
127
|
|
|
115
|
-
|
|
116
|
-
},
|
|
128
|
+
return finalCollection;
|
|
129
|
+
},
|
|
130
|
+
{},
|
|
131
|
+
);
|
|
117
132
|
|
|
118
133
|
/**
|
|
119
134
|
* Get some data from the store
|
|
@@ -141,7 +156,7 @@ function get(key) {
|
|
|
141
156
|
cache.set(key, val);
|
|
142
157
|
return val;
|
|
143
158
|
})
|
|
144
|
-
.catch(err => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
|
|
159
|
+
.catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
|
|
145
160
|
|
|
146
161
|
return cache.captureTask(taskName, promise);
|
|
147
162
|
}
|
|
@@ -166,11 +181,10 @@ function getAllKeys() {
|
|
|
166
181
|
}
|
|
167
182
|
|
|
168
183
|
// Otherwise retrieve the keys from storage and capture a promise to aid concurrent usages
|
|
169
|
-
const promise = Storage.getAllKeys()
|
|
170
|
-
.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
184
|
+
const promise = Storage.getAllKeys().then((keys) => {
|
|
185
|
+
_.each(keys, (key) => cache.addKey(key));
|
|
186
|
+
return keys;
|
|
187
|
+
});
|
|
174
188
|
|
|
175
189
|
return cache.captureTask(taskName, promise);
|
|
176
190
|
}
|
|
@@ -206,9 +220,7 @@ function isCollectionMemberKey(collectionKey, key) {
|
|
|
206
220
|
* @return {Boolean}
|
|
207
221
|
*/
|
|
208
222
|
function isKeyMatch(configKey, key) {
|
|
209
|
-
return isCollectionKey(configKey)
|
|
210
|
-
? Str.startsWith(key, configKey)
|
|
211
|
-
: configKey === key;
|
|
223
|
+
return isCollectionKey(configKey) ? Str.startsWith(key, configKey) : configKey === key;
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
/**
|
|
@@ -220,7 +232,7 @@ function isKeyMatch(configKey, key) {
|
|
|
220
232
|
* @returns {Boolean}
|
|
221
233
|
*/
|
|
222
234
|
function isSafeEvictionKey(testKey) {
|
|
223
|
-
return _.some(evictionAllowList, key => isKeyMatch(key, testKey));
|
|
235
|
+
return _.some(evictionAllowList, (key) => isKeyMatch(key, testKey));
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
/**
|
|
@@ -242,16 +254,20 @@ function tryGetCachedValue(key, mapping = {}) {
|
|
|
242
254
|
if (allCacheKeys.length === 0) {
|
|
243
255
|
return;
|
|
244
256
|
}
|
|
245
|
-
const matchingKeys = _.filter(allCacheKeys, k => k.startsWith(key));
|
|
246
|
-
const values = _.reduce(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
const matchingKeys = _.filter(allCacheKeys, (k) => k.startsWith(key));
|
|
258
|
+
const values = _.reduce(
|
|
259
|
+
matchingKeys,
|
|
260
|
+
(finalObject, matchedKey) => {
|
|
261
|
+
const cachedValue = cache.getValue(matchedKey);
|
|
262
|
+
if (cachedValue) {
|
|
263
|
+
// This is permissible because we're in the process of constructing the final object in a reduce function.
|
|
264
|
+
// eslint-disable-next-line no-param-reassign
|
|
265
|
+
finalObject[matchedKey] = cachedValue;
|
|
266
|
+
}
|
|
267
|
+
return finalObject;
|
|
268
|
+
},
|
|
269
|
+
{},
|
|
270
|
+
);
|
|
255
271
|
|
|
256
272
|
val = values;
|
|
257
273
|
}
|
|
@@ -339,17 +355,16 @@ function addToEvictionBlockList(key, connectionID) {
|
|
|
339
355
|
* @returns {Promise}
|
|
340
356
|
*/
|
|
341
357
|
function addAllSafeEvictionKeysToRecentlyAccessedList() {
|
|
342
|
-
return getAllKeys()
|
|
343
|
-
.
|
|
344
|
-
_.each(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
addLastAccessedKey(key);
|
|
350
|
-
});
|
|
358
|
+
return getAllKeys().then((keys) => {
|
|
359
|
+
_.each(evictionAllowList, (safeEvictionKey) => {
|
|
360
|
+
_.each(keys, (key) => {
|
|
361
|
+
if (!isKeyMatch(safeEvictionKey, key)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
addLastAccessedKey(key);
|
|
351
365
|
});
|
|
352
366
|
});
|
|
367
|
+
});
|
|
353
368
|
}
|
|
354
369
|
|
|
355
370
|
/**
|
|
@@ -358,20 +373,22 @@ function addAllSafeEvictionKeysToRecentlyAccessedList() {
|
|
|
358
373
|
* @returns {Object}
|
|
359
374
|
*/
|
|
360
375
|
function getCachedCollection(collectionKey) {
|
|
361
|
-
const collectionMemberKeys = _.filter(cache.getAllKeys(), (
|
|
362
|
-
|
|
363
|
-
|
|
376
|
+
const collectionMemberKeys = _.filter(cache.getAllKeys(), (storedKey) => isCollectionMemberKey(collectionKey, storedKey));
|
|
377
|
+
|
|
378
|
+
return _.reduce(
|
|
379
|
+
collectionMemberKeys,
|
|
380
|
+
(prev, curr) => {
|
|
381
|
+
const cachedValue = cache.getValue(curr);
|
|
382
|
+
if (!cachedValue) {
|
|
383
|
+
return prev;
|
|
384
|
+
}
|
|
364
385
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (!cachedValue) {
|
|
386
|
+
// eslint-disable-next-line no-param-reassign
|
|
387
|
+
prev[curr] = cachedValue;
|
|
368
388
|
return prev;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
prev[curr] = cachedValue;
|
|
373
|
-
return prev;
|
|
374
|
-
}, {});
|
|
389
|
+
},
|
|
390
|
+
{},
|
|
391
|
+
);
|
|
375
392
|
}
|
|
376
393
|
|
|
377
394
|
/**
|
|
@@ -740,9 +757,7 @@ function addKeyToRecentlyAccessedIfNeeded(mapping) {
|
|
|
740
757
|
if (mapping.withOnyxInstance && !isCollectionKey(mapping.key)) {
|
|
741
758
|
// All React components subscribing to a key flagged as a safe eviction key must implement the canEvict property.
|
|
742
759
|
if (_.isUndefined(mapping.canEvict)) {
|
|
743
|
-
throw new Error(
|
|
744
|
-
`Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`,
|
|
745
|
-
);
|
|
760
|
+
throw new Error(`Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`);
|
|
746
761
|
}
|
|
747
762
|
|
|
748
763
|
addLastAccessedKey(mapping.key);
|
|
@@ -757,13 +772,19 @@ function addKeyToRecentlyAccessedIfNeeded(mapping) {
|
|
|
757
772
|
* @param {Object} mapping
|
|
758
773
|
*/
|
|
759
774
|
function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
|
|
760
|
-
Promise.all(_.map(matchingKeys, key => get(key)))
|
|
761
|
-
.then(
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
775
|
+
Promise.all(_.map(matchingKeys, (key) => get(key)))
|
|
776
|
+
.then((values) =>
|
|
777
|
+
_.reduce(
|
|
778
|
+
values,
|
|
779
|
+
(finalObject, value, i) => {
|
|
780
|
+
// eslint-disable-next-line no-param-reassign
|
|
781
|
+
finalObject[matchingKeys[i]] = value;
|
|
782
|
+
return finalObject;
|
|
783
|
+
},
|
|
784
|
+
{},
|
|
785
|
+
),
|
|
786
|
+
)
|
|
787
|
+
.then((val) => sendDataToConnection(mapping, val, undefined, true));
|
|
767
788
|
}
|
|
768
789
|
|
|
769
790
|
/**
|
|
@@ -810,11 +831,7 @@ function connect(mapping) {
|
|
|
810
831
|
// Performance improvement
|
|
811
832
|
// If the mapping is connected to an onyx key that is not a collection
|
|
812
833
|
// we can skip the call to getAllKeys() and return an array with a single item
|
|
813
|
-
if (Boolean(mapping.key)
|
|
814
|
-
&& typeof mapping.key === 'string'
|
|
815
|
-
&& !(mapping.key.endsWith('_'))
|
|
816
|
-
&& cache.storageKeys.has(mapping.key)
|
|
817
|
-
) {
|
|
834
|
+
if (Boolean(mapping.key) && typeof mapping.key === 'string' && !mapping.key.endsWith('_') && cache.storageKeys.has(mapping.key)) {
|
|
818
835
|
return [mapping.key];
|
|
819
836
|
}
|
|
820
837
|
return getAllKeys();
|
|
@@ -823,7 +840,7 @@ function connect(mapping) {
|
|
|
823
840
|
// We search all the keys in storage to see if any are a "match" for the subscriber we are connecting so that we
|
|
824
841
|
// can send data back to the subscriber. Note that multiple keys can match as a subscriber could either be
|
|
825
842
|
// subscribed to a "collection key" or a single key.
|
|
826
|
-
const matchingKeys = _.filter(keys, key => isKeyMatch(mapping.key, key));
|
|
843
|
+
const matchingKeys = _.filter(keys, (key) => isKeyMatch(mapping.key, key));
|
|
827
844
|
|
|
828
845
|
// If the key being connected to does not exist we initialize the value with null. For subscribers that connected
|
|
829
846
|
// directly via connect() they will simply get a null value sent to them without any information about which key matched
|
|
@@ -852,13 +869,13 @@ function connect(mapping) {
|
|
|
852
869
|
|
|
853
870
|
// We did not opt into using waitForCollectionCallback mode so the callback is called for every matching key.
|
|
854
871
|
for (let i = 0; i < matchingKeys.length; i++) {
|
|
855
|
-
get(matchingKeys[i]).then(val => sendDataToConnection(mapping, val, matchingKeys[i], true));
|
|
872
|
+
get(matchingKeys[i]).then((val) => sendDataToConnection(mapping, val, matchingKeys[i], true));
|
|
856
873
|
}
|
|
857
874
|
return;
|
|
858
875
|
}
|
|
859
876
|
|
|
860
877
|
// If we are not subscribed to a collection key then there's only a single key to send an update for.
|
|
861
|
-
get(mapping.key).then(val => sendDataToConnection(mapping, val, mapping.key, true));
|
|
878
|
+
get(mapping.key).then((val) => sendDataToConnection(mapping, val, mapping.key, true));
|
|
862
879
|
return;
|
|
863
880
|
}
|
|
864
881
|
|
|
@@ -871,7 +888,7 @@ function connect(mapping) {
|
|
|
871
888
|
}
|
|
872
889
|
|
|
873
890
|
// If the subscriber is not using a collection key then we just send a single value back to the subscriber
|
|
874
|
-
get(mapping.key).then(val => sendDataToConnection(mapping, val, mapping.key, true));
|
|
891
|
+
get(mapping.key).then((val) => sendDataToConnection(mapping, val, mapping.key, true));
|
|
875
892
|
return;
|
|
876
893
|
}
|
|
877
894
|
|
|
@@ -978,13 +995,13 @@ function reportStorageQuota() {
|
|
|
978
995
|
function evictStorageAndRetry(error, onyxMethod, ...args) {
|
|
979
996
|
Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}`);
|
|
980
997
|
|
|
981
|
-
if (error && Str.startsWith(error.message,
|
|
998
|
+
if (error && Str.startsWith(error.message, "Failed to execute 'put' on 'IDBObjectStore'")) {
|
|
982
999
|
Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.');
|
|
983
1000
|
throw error;
|
|
984
1001
|
}
|
|
985
1002
|
|
|
986
1003
|
// Find the first key that we can remove that has no subscribers in our blocklist
|
|
987
|
-
const keyForRemoval = _.find(recentlyAccessedKeys, key => !evictionBlocklist[key]);
|
|
1004
|
+
const keyForRemoval = _.find(recentlyAccessedKeys, (key) => !evictionBlocklist[key]);
|
|
988
1005
|
if (!keyForRemoval) {
|
|
989
1006
|
// If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case,
|
|
990
1007
|
// then we should stop retrying as there is not much the user can do to fix this. Instead of getting them stuck in an infinite loop we
|
|
@@ -996,8 +1013,7 @@ function evictStorageAndRetry(error, onyxMethod, ...args) {
|
|
|
996
1013
|
// Remove the least recently viewed key that is not currently being accessed and retry.
|
|
997
1014
|
Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`);
|
|
998
1015
|
reportStorageQuota();
|
|
999
|
-
return remove(keyForRemoval)
|
|
1000
|
-
.then(() => onyxMethod(...args));
|
|
1016
|
+
return remove(keyForRemoval).then(() => onyxMethod(...args));
|
|
1001
1017
|
}
|
|
1002
1018
|
|
|
1003
1019
|
/**
|
|
@@ -1021,7 +1037,7 @@ function broadcastUpdate(key, value, hasChanged, method) {
|
|
|
1021
1037
|
cache.addToAccessedKeys(key);
|
|
1022
1038
|
}
|
|
1023
1039
|
|
|
1024
|
-
return scheduleSubscriberUpdate(key, value, subscriber => hasChanged || subscriber.initWithStoredValues === false);
|
|
1040
|
+
return scheduleSubscriberUpdate(key, value, (subscriber) => hasChanged || subscriber.initWithStoredValues === false);
|
|
1025
1041
|
}
|
|
1026
1042
|
|
|
1027
1043
|
/**
|
|
@@ -1060,6 +1076,15 @@ function removeNullValues(key, value) {
|
|
|
1060
1076
|
* @returns {Promise}
|
|
1061
1077
|
*/
|
|
1062
1078
|
function set(key, value) {
|
|
1079
|
+
if (!ActiveClientManager.isClientTheLeader()) {
|
|
1080
|
+
Broadcast.sendMessage({type: METHOD.SET, key, value});
|
|
1081
|
+
return Promise.resolve();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (isClearing) {
|
|
1085
|
+
return Promise.resolve();
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1063
1088
|
const valueWithoutNull = removeNullValues(key, value);
|
|
1064
1089
|
|
|
1065
1090
|
if (valueWithoutNull === null) {
|
|
@@ -1081,7 +1106,7 @@ function set(key, value) {
|
|
|
1081
1106
|
}
|
|
1082
1107
|
|
|
1083
1108
|
return Storage.setItem(key, valueWithoutNull)
|
|
1084
|
-
.catch(error => evictStorageAndRetry(error, set, key, valueWithoutNull))
|
|
1109
|
+
.catch((error) => evictStorageAndRetry(error, set, key, valueWithoutNull))
|
|
1085
1110
|
.then(() => updatePromise);
|
|
1086
1111
|
}
|
|
1087
1112
|
|
|
@@ -1106,6 +1131,15 @@ function prepareKeyValuePairsForStorage(data) {
|
|
|
1106
1131
|
* @returns {Promise}
|
|
1107
1132
|
*/
|
|
1108
1133
|
function multiSet(data) {
|
|
1134
|
+
if (!ActiveClientManager.isClientTheLeader()) {
|
|
1135
|
+
Broadcast.sendMessage({type: METHOD.MULTI_SET, data});
|
|
1136
|
+
return Promise.resolve();
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (isClearing) {
|
|
1140
|
+
return Promise.resolve();
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1109
1143
|
const keyValuePairs = prepareKeyValuePairsForStorage(data);
|
|
1110
1144
|
|
|
1111
1145
|
const updatePromises = _.map(data, (val, key) => {
|
|
@@ -1114,17 +1148,20 @@ function multiSet(data) {
|
|
|
1114
1148
|
return scheduleSubscriberUpdate(key, val);
|
|
1115
1149
|
});
|
|
1116
1150
|
|
|
1117
|
-
const keyValuePairsWithoutNull = _.filter(
|
|
1118
|
-
|
|
1151
|
+
const keyValuePairsWithoutNull = _.filter(
|
|
1152
|
+
_.map(keyValuePairs, ([key, value]) => {
|
|
1153
|
+
const valueWithoutNull = removeNullValues(key, value);
|
|
1119
1154
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1155
|
+
if (valueWithoutNull === null) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
return [key, valueWithoutNull];
|
|
1159
|
+
}),
|
|
1160
|
+
Boolean,
|
|
1161
|
+
);
|
|
1125
1162
|
|
|
1126
1163
|
return Storage.multiSet(keyValuePairsWithoutNull)
|
|
1127
|
-
.catch(error => evictStorageAndRetry(error, multiSet, data))
|
|
1164
|
+
.catch((error) => evictStorageAndRetry(error, multiSet, data))
|
|
1128
1165
|
.then(() => Promise.all(updatePromises));
|
|
1129
1166
|
}
|
|
1130
1167
|
|
|
@@ -1146,8 +1183,7 @@ function applyMerge(existingValue, changes, shouldRemoveNullObjectValues) {
|
|
|
1146
1183
|
|
|
1147
1184
|
if (_.some(changes, _.isObject)) {
|
|
1148
1185
|
// Object values are then merged one after the other
|
|
1149
|
-
return _.reduce(changes, (modifiedData, change) => utils.fastMerge(modifiedData, change, shouldRemoveNullObjectValues),
|
|
1150
|
-
existingValue || {});
|
|
1186
|
+
return _.reduce(changes, (modifiedData, change) => utils.fastMerge(modifiedData, change, shouldRemoveNullObjectValues), existingValue || {});
|
|
1151
1187
|
}
|
|
1152
1188
|
|
|
1153
1189
|
// If we have anything else we can't merge it so we'll
|
|
@@ -1176,6 +1212,15 @@ function applyMerge(existingValue, changes, shouldRemoveNullObjectValues) {
|
|
|
1176
1212
|
* @returns {Promise}
|
|
1177
1213
|
*/
|
|
1178
1214
|
function merge(key, changes) {
|
|
1215
|
+
if (!ActiveClientManager.isClientTheLeader()) {
|
|
1216
|
+
Broadcast.sendMessage({type: METHOD.MERGE, key, changes});
|
|
1217
|
+
return Promise.resolve();
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (isClearing) {
|
|
1221
|
+
return Promise.resolve();
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1179
1224
|
// Top-level undefined values are ignored
|
|
1180
1225
|
// Therefore we need to prevent adding them to the merge queue
|
|
1181
1226
|
if (_.isUndefined(changes)) {
|
|
@@ -1190,57 +1235,55 @@ function merge(key, changes) {
|
|
|
1190
1235
|
}
|
|
1191
1236
|
mergeQueue[key] = [changes];
|
|
1192
1237
|
|
|
1193
|
-
mergeQueuePromise[key] = get(key)
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
let batchedChanges = applyMerge(undefined, mergeQueue[key], false);
|
|
1199
|
-
|
|
1200
|
-
// The presence of a `null` in the merge queue instructs us to drop the existing value.
|
|
1201
|
-
// In this case, we can't simply merge the batched changes with the existing value, because then the null in the merge queue would have no effect
|
|
1202
|
-
const shouldOverwriteExistingValue = _.includes(mergeQueue[key], null);
|
|
1238
|
+
mergeQueuePromise[key] = get(key).then((existingValue) => {
|
|
1239
|
+
try {
|
|
1240
|
+
// 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)
|
|
1241
|
+
// We don't want to remove null values from the "batchedChanges", because SQLite uses them to remove keys from storage natively.
|
|
1242
|
+
let batchedChanges = applyMerge(undefined, mergeQueue[key], false);
|
|
1203
1243
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1244
|
+
// The presence of a `null` in the merge queue instructs us to drop the existing value.
|
|
1245
|
+
// In this case, we can't simply merge the batched changes with the existing value, because then the null in the merge queue would have no effect
|
|
1246
|
+
const shouldOverwriteExistingValue = _.includes(mergeQueue[key], null);
|
|
1207
1247
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1248
|
+
// Clean up the write queue, so we don't apply these changes again
|
|
1249
|
+
delete mergeQueue[key];
|
|
1250
|
+
delete mergeQueuePromise[key];
|
|
1213
1251
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
// On native platforms we use SQLite which utilises JSON_PATCH to merge changes.
|
|
1220
|
-
// JSON_PATCH generally removes null values from the stored object.
|
|
1221
|
-
// When there is no existing value though, SQLite will just insert the changes as a new value and thus the null values won't be removed.
|
|
1222
|
-
// Therefore we need to remove null values from the `batchedChanges` which are sent to the SQLite, if no existing value is present.
|
|
1223
|
-
if (!existingValue) {
|
|
1224
|
-
batchedChanges = applyMerge(undefined, [batchedChanges], true);
|
|
1225
|
-
}
|
|
1252
|
+
// If the batched changes equal null, we want to remove the key from storage, to reduce storage size
|
|
1253
|
+
if (_.isNull(batchedChanges)) {
|
|
1254
|
+
remove(key);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1226
1257
|
|
|
1227
|
-
|
|
1258
|
+
// After that we merge the batched changes with the existing value
|
|
1259
|
+
// We can remove null values from the "modifiedData", because "null" implicates that the user wants to remove a value from storage.
|
|
1260
|
+
// The "modifiedData" will be directly "set" in storage instead of being merged
|
|
1261
|
+
const modifiedData = shouldOverwriteExistingValue ? batchedChanges : applyMerge(existingValue, [batchedChanges], true);
|
|
1262
|
+
|
|
1263
|
+
// On native platforms we use SQLite which utilises JSON_PATCH to merge changes.
|
|
1264
|
+
// JSON_PATCH generally removes null values from the stored object.
|
|
1265
|
+
// When there is no existing value though, SQLite will just insert the changes as a new value and thus the null values won't be removed.
|
|
1266
|
+
// Therefore we need to remove null values from the `batchedChanges` which are sent to the SQLite, if no existing value is present.
|
|
1267
|
+
if (!existingValue) {
|
|
1268
|
+
batchedChanges = applyMerge(undefined, [batchedChanges], true);
|
|
1269
|
+
}
|
|
1228
1270
|
|
|
1229
|
-
|
|
1230
|
-
const updatePromise = broadcastUpdate(key, modifiedData, hasChanged, 'merge');
|
|
1271
|
+
const hasChanged = cache.hasValueChanged(key, modifiedData);
|
|
1231
1272
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
return updatePromise;
|
|
1235
|
-
}
|
|
1273
|
+
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
1274
|
+
const updatePromise = broadcastUpdate(key, modifiedData, hasChanged, 'merge');
|
|
1236
1275
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
|
|
1241
|
-
return Promise.resolve();
|
|
1276
|
+
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
|
|
1277
|
+
if (!hasChanged || isClearing) {
|
|
1278
|
+
return updatePromise;
|
|
1242
1279
|
}
|
|
1243
|
-
|
|
1280
|
+
|
|
1281
|
+
return Storage.mergeItem(key, batchedChanges, modifiedData).then(() => updatePromise);
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
|
|
1284
|
+
return Promise.resolve();
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1244
1287
|
|
|
1245
1288
|
return mergeQueuePromise[key];
|
|
1246
1289
|
}
|
|
@@ -1251,14 +1294,13 @@ function merge(key, changes) {
|
|
|
1251
1294
|
* @returns {Promise}
|
|
1252
1295
|
*/
|
|
1253
1296
|
function initializeWithDefaultKeyStates() {
|
|
1254
|
-
return Storage.multiGet(_.keys(defaultKeyStates))
|
|
1255
|
-
.
|
|
1256
|
-
const asObject = _.object(pairs);
|
|
1297
|
+
return Storage.multiGet(_.keys(defaultKeyStates)).then((pairs) => {
|
|
1298
|
+
const asObject = _.object(pairs);
|
|
1257
1299
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1300
|
+
const merged = utils.fastMerge(asObject, defaultKeyStates);
|
|
1301
|
+
cache.merge(merged);
|
|
1302
|
+
_.each(merged, (val, key) => keyChanged(key, val));
|
|
1303
|
+
});
|
|
1262
1304
|
}
|
|
1263
1305
|
|
|
1264
1306
|
/**
|
|
@@ -1284,66 +1326,82 @@ function initializeWithDefaultKeyStates() {
|
|
|
1284
1326
|
* @returns {Promise<void>}
|
|
1285
1327
|
*/
|
|
1286
1328
|
function clear(keysToPreserve = []) {
|
|
1287
|
-
|
|
1288
|
-
.
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1329
|
+
if (!ActiveClientManager.isClientTheLeader()) {
|
|
1330
|
+
Broadcast.sendMessage({type: METHOD.CLEAR, keysToPreserve});
|
|
1331
|
+
return Promise.resolve();
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (isClearing) {
|
|
1335
|
+
return Promise.resolve();
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
isClearing = true;
|
|
1339
|
+
|
|
1340
|
+
return getAllKeys().then((keys) => {
|
|
1341
|
+
const keysToBeClearedFromStorage = [];
|
|
1342
|
+
const keyValuesToResetAsCollection = {};
|
|
1343
|
+
const keyValuesToResetIndividually = {};
|
|
1344
|
+
|
|
1345
|
+
// The only keys that should not be cleared are:
|
|
1346
|
+
// 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline
|
|
1347
|
+
// status, or activeClients need to remain in Onyx even when signed out)
|
|
1348
|
+
// 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them
|
|
1349
|
+
// to null would cause unknown behavior)
|
|
1350
|
+
_.each(keys, (key) => {
|
|
1351
|
+
const isKeyToPreserve = _.contains(keysToPreserve, key);
|
|
1352
|
+
const isDefaultKey = _.has(defaultKeyStates, key);
|
|
1353
|
+
|
|
1354
|
+
// If the key is being removed or reset to default:
|
|
1355
|
+
// 1. Update it in the cache
|
|
1356
|
+
// 2. Figure out whether it is a collection key or not,
|
|
1357
|
+
// since collection key subscribers need to be updated differently
|
|
1358
|
+
if (!isKeyToPreserve) {
|
|
1359
|
+
const oldValue = cache.getValue(key);
|
|
1360
|
+
const newValue = _.get(defaultKeyStates, key, null);
|
|
1361
|
+
if (newValue !== oldValue) {
|
|
1362
|
+
cache.set(key, newValue);
|
|
1363
|
+
const collectionKey = key.substring(0, key.indexOf('_') + 1);
|
|
1364
|
+
if (collectionKey) {
|
|
1365
|
+
if (!keyValuesToResetAsCollection[collectionKey]) {
|
|
1366
|
+
keyValuesToResetAsCollection[collectionKey] = {};
|
|
1319
1367
|
}
|
|
1368
|
+
keyValuesToResetAsCollection[collectionKey][key] = newValue;
|
|
1369
|
+
} else {
|
|
1370
|
+
keyValuesToResetIndividually[key] = newValue;
|
|
1320
1371
|
}
|
|
1321
1372
|
}
|
|
1373
|
+
}
|
|
1322
1374
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1375
|
+
if (isKeyToPreserve || isDefaultKey) {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1326
1378
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1379
|
+
// If it isn't preserved and doesn't have a default, we'll remove it
|
|
1380
|
+
keysToBeClearedFromStorage.push(key);
|
|
1381
|
+
});
|
|
1330
1382
|
|
|
1331
|
-
|
|
1383
|
+
const updatePromises = [];
|
|
1332
1384
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1385
|
+
// Notify the subscribers for each key/value group so they can receive the new values
|
|
1386
|
+
_.each(keyValuesToResetIndividually, (value, key) => {
|
|
1387
|
+
updatePromises.push(scheduleSubscriberUpdate(key, value));
|
|
1388
|
+
});
|
|
1389
|
+
_.each(keyValuesToResetAsCollection, (value, key) => {
|
|
1390
|
+
updatePromises.push(scheduleNotifyCollectionSubscribers(key, value));
|
|
1391
|
+
});
|
|
1340
1392
|
|
|
1341
|
-
|
|
1393
|
+
const defaultKeyValuePairs = _.pairs(_.omit(defaultKeyStates, keysToPreserve));
|
|
1342
1394
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1395
|
+
// Remove only the items that we want cleared from storage, and reset others to default
|
|
1396
|
+
_.each(keysToBeClearedFromStorage, (key) => cache.drop(key));
|
|
1397
|
+
return Storage.removeItems(keysToBeClearedFromStorage)
|
|
1398
|
+
.then(() => Storage.multiSet(defaultKeyValuePairs))
|
|
1399
|
+
.then(() => {
|
|
1400
|
+
isClearing = false;
|
|
1401
|
+
Broadcast.sendMessage({type: METHOD.CLEAR, keysToPreserve});
|
|
1402
|
+
return Promise.all(updatePromises);
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1347
1405
|
}
|
|
1348
1406
|
|
|
1349
1407
|
/**
|
|
@@ -1386,49 +1444,48 @@ function mergeCollection(collectionKey, collection) {
|
|
|
1386
1444
|
return Promise.resolve();
|
|
1387
1445
|
}
|
|
1388
1446
|
|
|
1389
|
-
return getAllKeys()
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
.
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
if (keyValuePairsForNewCollection.length > 0) {
|
|
1418
|
-
promises.push(Storage.multiSet(keyValuePairsForNewCollection));
|
|
1419
|
-
}
|
|
1447
|
+
return getAllKeys().then((persistedKeys) => {
|
|
1448
|
+
// Split to keys that exist in storage and keys that don't
|
|
1449
|
+
const [existingKeys, newKeys] = _.chain(collection)
|
|
1450
|
+
.pick((value, key) => {
|
|
1451
|
+
if (_.isNull(value)) {
|
|
1452
|
+
remove(key);
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
return true;
|
|
1456
|
+
})
|
|
1457
|
+
.keys()
|
|
1458
|
+
.partition((key) => persistedKeys.includes(key))
|
|
1459
|
+
.value();
|
|
1460
|
+
|
|
1461
|
+
const existingKeyCollection = _.pick(collection, existingKeys);
|
|
1462
|
+
const newCollection = _.pick(collection, newKeys);
|
|
1463
|
+
const keyValuePairsForExistingCollection = prepareKeyValuePairsForStorage(existingKeyCollection);
|
|
1464
|
+
const keyValuePairsForNewCollection = prepareKeyValuePairsForStorage(newCollection);
|
|
1465
|
+
|
|
1466
|
+
const promises = [];
|
|
1467
|
+
|
|
1468
|
+
// New keys will be added via multiSet while existing keys will be updated using multiMerge
|
|
1469
|
+
// This is because setting a key that doesn't exist yet with multiMerge will throw errors
|
|
1470
|
+
if (keyValuePairsForExistingCollection.length > 0) {
|
|
1471
|
+
promises.push(Storage.multiMerge(keyValuePairsForExistingCollection));
|
|
1472
|
+
}
|
|
1420
1473
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
cache.merge(collection);
|
|
1425
|
-
return scheduleNotifyCollectionSubscribers(collectionKey, collection);
|
|
1426
|
-
});
|
|
1474
|
+
if (keyValuePairsForNewCollection.length > 0) {
|
|
1475
|
+
promises.push(Storage.multiSet(keyValuePairsForNewCollection));
|
|
1476
|
+
}
|
|
1427
1477
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1478
|
+
// Prefill cache if necessary by calling get() on any existing keys and then merge original data to cache
|
|
1479
|
+
// and update all subscribers
|
|
1480
|
+
const promiseUpdate = Promise.all(_.map(existingKeys, get)).then(() => {
|
|
1481
|
+
cache.merge(collection);
|
|
1482
|
+
return scheduleNotifyCollectionSubscribers(collectionKey, collection);
|
|
1431
1483
|
});
|
|
1484
|
+
|
|
1485
|
+
return Promise.all(promises)
|
|
1486
|
+
.catch((error) => evictStorageAndRetry(error, mergeCollection, collection))
|
|
1487
|
+
.then(() => promiseUpdate);
|
|
1488
|
+
});
|
|
1432
1489
|
}
|
|
1433
1490
|
|
|
1434
1491
|
/**
|
|
@@ -1478,7 +1535,7 @@ function update(data) {
|
|
|
1478
1535
|
}
|
|
1479
1536
|
});
|
|
1480
1537
|
|
|
1481
|
-
return clearPromise.then(() => Promise.all(_.map(promises, p => p())));
|
|
1538
|
+
return clearPromise.then(() => Promise.all(_.map(promises, (p) => p())));
|
|
1482
1539
|
}
|
|
1483
1540
|
|
|
1484
1541
|
/**
|
|
@@ -1492,6 +1549,48 @@ function setMemoryOnlyKeys(keyList) {
|
|
|
1492
1549
|
cache.setRecentKeysLimit(Infinity);
|
|
1493
1550
|
}
|
|
1494
1551
|
|
|
1552
|
+
/**
|
|
1553
|
+
* Sets the callback to be called when the clear finishes executing.
|
|
1554
|
+
* @param {Function} callback
|
|
1555
|
+
*/
|
|
1556
|
+
function onClear(callback) {
|
|
1557
|
+
onClearCallback = callback;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Subscribes to the Broadcast channel and executes actions based on the
|
|
1562
|
+
* types of events.
|
|
1563
|
+
*/
|
|
1564
|
+
function subscribeToEvents() {
|
|
1565
|
+
Broadcast.subscribe(({data}) => {
|
|
1566
|
+
if (!ActiveClientManager.isClientTheLeader()) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
switch (data.type) {
|
|
1570
|
+
case METHOD.CLEAR:
|
|
1571
|
+
clear(data.keysToPreserve);
|
|
1572
|
+
break;
|
|
1573
|
+
case METHOD.SET:
|
|
1574
|
+
set(data.key, data.value);
|
|
1575
|
+
break;
|
|
1576
|
+
case METHOD.MULTI_SET:
|
|
1577
|
+
multiSet(data.key, data.value);
|
|
1578
|
+
break;
|
|
1579
|
+
case METHOD.MERGE:
|
|
1580
|
+
merge(data.key, data.changes);
|
|
1581
|
+
break;
|
|
1582
|
+
case ON_CLEAR:
|
|
1583
|
+
if (!onClearCallback) {
|
|
1584
|
+
break;
|
|
1585
|
+
}
|
|
1586
|
+
onClearCallback();
|
|
1587
|
+
break;
|
|
1588
|
+
default:
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1495
1594
|
/**
|
|
1496
1595
|
* Initialize the store with actions and listening for storage events
|
|
1497
1596
|
*
|
|
@@ -1526,6 +1625,15 @@ function init({
|
|
|
1526
1625
|
shouldSyncMultipleInstances = Boolean(global.localStorage),
|
|
1527
1626
|
debugSetState = false,
|
|
1528
1627
|
} = {}) {
|
|
1628
|
+
ActiveClientManager.init();
|
|
1629
|
+
|
|
1630
|
+
ActiveClientManager.isReady().then(() => {
|
|
1631
|
+
if (!ActiveClientManager.isClientTheLeader()) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
subscribeToEvents();
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1529
1637
|
if (captureMetrics) {
|
|
1530
1638
|
// The code here is only bundled and applied when the captureMetrics is set
|
|
1531
1639
|
// eslint-disable-next-line no-use-before-define
|
|
@@ -1543,10 +1651,14 @@ function init({
|
|
|
1543
1651
|
// We need the value of the collection keys later for checking if a
|
|
1544
1652
|
// key is a collection. We store it in a map for faster lookup.
|
|
1545
1653
|
const collectionValues = _.values(keys.COLLECTION);
|
|
1546
|
-
onyxCollectionKeyMap = _.reduce(
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1654
|
+
onyxCollectionKeyMap = _.reduce(
|
|
1655
|
+
collectionValues,
|
|
1656
|
+
(acc, val) => {
|
|
1657
|
+
acc.set(val, true);
|
|
1658
|
+
return acc;
|
|
1659
|
+
},
|
|
1660
|
+
new Map(),
|
|
1661
|
+
);
|
|
1550
1662
|
|
|
1551
1663
|
// Set our default key states to use when initializing and clearing Onyx data
|
|
1552
1664
|
defaultKeyStates = initialKeyStates;
|
|
@@ -1555,11 +1667,7 @@ function init({
|
|
|
1555
1667
|
evictionAllowList = safeEvictionKeys;
|
|
1556
1668
|
|
|
1557
1669
|
// Initialize all of our keys with data provided then give green light to any pending connections
|
|
1558
|
-
Promise.all([
|
|
1559
|
-
addAllSafeEvictionKeysToRecentlyAccessedList(),
|
|
1560
|
-
initializeWithDefaultKeyStates(),
|
|
1561
|
-
])
|
|
1562
|
-
.then(deferredInitTask.resolve);
|
|
1670
|
+
Promise.all([addAllSafeEvictionKeysToRecentlyAccessedList(), initializeWithDefaultKeyStates()]).then(deferredInitTask.resolve);
|
|
1563
1671
|
|
|
1564
1672
|
if (shouldSyncMultipleInstances && _.isFunction(Storage.keepInstancesSync)) {
|
|
1565
1673
|
Storage.keepInstancesSync((key, value) => {
|
|
@@ -1588,6 +1696,10 @@ const Onyx = {
|
|
|
1588
1696
|
setMemoryOnlyKeys,
|
|
1589
1697
|
tryGetCachedValue,
|
|
1590
1698
|
hasPendingMergeForKey,
|
|
1699
|
+
onClear,
|
|
1700
|
+
isClientManagerReady: ActiveClientManager.isReady,
|
|
1701
|
+
isClientTheLeader: ActiveClientManager.isClientTheLeader,
|
|
1702
|
+
subscribeToClientChange: ActiveClientManager.subscribeToClientChange,
|
|
1591
1703
|
};
|
|
1592
1704
|
|
|
1593
1705
|
/**
|