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/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) => _.reduce(collection, (finalCollection, item, key) => {
112
- // eslint-disable-next-line no-param-reassign
113
- finalCollection[key] = getSubsetOfData(item, selector, withOnyxInstanceState);
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
- return finalCollection;
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
- .then((keys) => {
171
- _.each(keys, key => cache.addKey(key));
172
- return keys;
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(matchingKeys, (finalObject, matchedKey) => {
247
- const cachedValue = cache.getValue(matchedKey);
248
- if (cachedValue) {
249
- // This is permissible because we're in the process of constructing the final object in a reduce function.
250
- // eslint-disable-next-line no-param-reassign
251
- finalObject[matchedKey] = cachedValue;
252
- }
253
- return finalObject;
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
- .then((keys) => {
344
- _.each(evictionAllowList, (safeEvictionKey) => {
345
- _.each(keys, (key) => {
346
- if (!isKeyMatch(safeEvictionKey, key)) {
347
- return;
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
- storedKey => isCollectionMemberKey(collectionKey, storedKey)
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
- return _.reduce(collectionMemberKeys, (prev, curr) => {
366
- const cachedValue = cache.getValue(curr);
367
- if (!cachedValue) {
386
+ // eslint-disable-next-line no-param-reassign
387
+ prev[curr] = cachedValue;
368
388
  return prev;
369
- }
370
-
371
- // eslint-disable-next-line no-param-reassign
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(values => _.reduce(values, (finalObject, value, i) => {
762
- // eslint-disable-next-line no-param-reassign
763
- finalObject[matchingKeys[i]] = value;
764
- return finalObject;
765
- }, {}))
766
- .then(val => sendDataToConnection(mapping, val, undefined, true));
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, 'Failed to execute \'put\' on \'IDBObjectStore\'')) {
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(_.map(keyValuePairs, ([key, value]) => {
1118
- const valueWithoutNull = removeNullValues(key, value);
1151
+ const keyValuePairsWithoutNull = _.filter(
1152
+ _.map(keyValuePairs, ([key, value]) => {
1153
+ const valueWithoutNull = removeNullValues(key, value);
1119
1154
 
1120
- if (valueWithoutNull === null) {
1121
- return;
1122
- }
1123
- return [key, valueWithoutNull];
1124
- }), Boolean);
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
- .then((existingValue) => {
1195
- try {
1196
- // 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)
1197
- // We don't want to remove null values from the "batchedChanges", because SQLite uses them to remove keys from storage natively.
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
- // Clean up the write queue, so we don't apply these changes again
1205
- delete mergeQueue[key];
1206
- delete mergeQueuePromise[key];
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
- // If the batched changes equal null, we want to remove the key from storage, to reduce storage size
1209
- if (_.isNull(batchedChanges)) {
1210
- remove(key);
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
- // After that we merge the batched changes with the existing value
1215
- // We can remove null values from the "modifiedData", because "null" implicates that the user wants to remove a value from storage.
1216
- // The "modifiedData" will be directly "set" in storage instead of being merged
1217
- const modifiedData = shouldOverwriteExistingValue ? batchedChanges : applyMerge(existingValue, [batchedChanges], true);
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
- const hasChanged = cache.hasValueChanged(key, modifiedData);
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
- // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
1230
- const updatePromise = broadcastUpdate(key, modifiedData, hasChanged, 'merge');
1271
+ const hasChanged = cache.hasValueChanged(key, modifiedData);
1231
1272
 
1232
- // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
1233
- if (!hasChanged) {
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
- return Storage.mergeItem(key, batchedChanges, modifiedData)
1238
- .then(() => updatePromise);
1239
- } catch (error) {
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
- .then((pairs) => {
1256
- const asObject = _.object(pairs);
1297
+ return Storage.multiGet(_.keys(defaultKeyStates)).then((pairs) => {
1298
+ const asObject = _.object(pairs);
1257
1299
 
1258
- const merged = utils.fastMerge(asObject, defaultKeyStates);
1259
- cache.merge(merged);
1260
- _.each(merged, (val, key) => keyChanged(key, val));
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
- return getAllKeys()
1288
- .then((keys) => {
1289
- const keysToBeClearedFromStorage = [];
1290
- const keyValuesToResetAsCollection = {};
1291
- const keyValuesToResetIndividually = {};
1292
-
1293
- // The only keys that should not be cleared are:
1294
- // 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline
1295
- // status, or activeClients need to remain in Onyx even when signed out)
1296
- // 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them
1297
- // to null would cause unknown behavior)
1298
- _.each(keys, (key) => {
1299
- const isKeyToPreserve = _.contains(keysToPreserve, key);
1300
- const isDefaultKey = _.has(defaultKeyStates, key);
1301
-
1302
- // If the key is being removed or reset to default:
1303
- // 1. Update it in the cache
1304
- // 2. Figure out whether it is a collection key or not,
1305
- // since collection key subscribers need to be updated differently
1306
- if (!isKeyToPreserve) {
1307
- const oldValue = cache.getValue(key);
1308
- const newValue = _.get(defaultKeyStates, key, null);
1309
- if (newValue !== oldValue) {
1310
- cache.set(key, newValue);
1311
- const collectionKey = key.substring(0, key.indexOf('_') + 1);
1312
- if (collectionKey) {
1313
- if (!keyValuesToResetAsCollection[collectionKey]) {
1314
- keyValuesToResetAsCollection[collectionKey] = {};
1315
- }
1316
- keyValuesToResetAsCollection[collectionKey][key] = newValue;
1317
- } else {
1318
- keyValuesToResetIndividually[key] = newValue;
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
- if (isKeyToPreserve || isDefaultKey) {
1324
- return;
1325
- }
1375
+ if (isKeyToPreserve || isDefaultKey) {
1376
+ return;
1377
+ }
1326
1378
 
1327
- // If it isn't preserved and doesn't have a default, we'll remove it
1328
- keysToBeClearedFromStorage.push(key);
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
- const updatePromises = [];
1383
+ const updatePromises = [];
1332
1384
 
1333
- // Notify the subscribers for each key/value group so they can receive the new values
1334
- _.each(keyValuesToResetIndividually, (value, key) => {
1335
- updatePromises.push(scheduleSubscriberUpdate(key, value));
1336
- });
1337
- _.each(keyValuesToResetAsCollection, (value, key) => {
1338
- updatePromises.push(scheduleNotifyCollectionSubscribers(key, value));
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
- const defaultKeyValuePairs = _.pairs(_.omit(defaultKeyStates, keysToPreserve));
1393
+ const defaultKeyValuePairs = _.pairs(_.omit(defaultKeyStates, keysToPreserve));
1342
1394
 
1343
- // Remove only the items that we want cleared from storage, and reset others to default
1344
- _.each(keysToBeClearedFromStorage, key => cache.drop(key));
1345
- return Storage.removeItems(keysToBeClearedFromStorage).then(() => Storage.multiSet(defaultKeyValuePairs)).then(() => Promise.all(updatePromises));
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
- .then((persistedKeys) => {
1391
- // Split to keys that exist in storage and keys that don't
1392
- const [existingKeys, newKeys] = _.chain(collection)
1393
- .pick((value, key) => {
1394
- if (_.isNull(value)) {
1395
- remove(key);
1396
- return false;
1397
- }
1398
- return true;
1399
- })
1400
- .keys()
1401
- .partition(key => persistedKeys.includes(key))
1402
- .value();
1403
-
1404
- const existingKeyCollection = _.pick(collection, existingKeys);
1405
- const newCollection = _.pick(collection, newKeys);
1406
- const keyValuePairsForExistingCollection = prepareKeyValuePairsForStorage(existingKeyCollection);
1407
- const keyValuePairsForNewCollection = prepareKeyValuePairsForStorage(newCollection);
1408
-
1409
- const promises = [];
1410
-
1411
- // New keys will be added via multiSet while existing keys will be updated using multiMerge
1412
- // This is because setting a key that doesn't exist yet with multiMerge will throw errors
1413
- if (keyValuePairsForExistingCollection.length > 0) {
1414
- promises.push(Storage.multiMerge(keyValuePairsForExistingCollection));
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
- // Prefill cache if necessary by calling get() on any existing keys and then merge original data to cache
1422
- // and update all subscribers
1423
- const promiseUpdate = Promise.all(_.map(existingKeys, get)).then(() => {
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
- return Promise.all(promises)
1429
- .catch(error => evictStorageAndRetry(error, mergeCollection, collection))
1430
- .then(() => promiseUpdate);
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(collectionValues, (acc, val) => {
1547
- acc.set(val, true);
1548
- return acc;
1549
- }, new Map());
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
  /**