react-native-onyx 2.0.21 → 2.0.23

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.
Files changed (41) hide show
  1. package/API.md +49 -64
  2. package/dist/DevTools.js +0 -1
  3. package/dist/Onyx.d.ts +49 -258
  4. package/dist/Onyx.js +192 -1165
  5. package/dist/OnyxCache.d.ts +14 -15
  6. package/dist/OnyxUtils.d.ts +320 -0
  7. package/dist/OnyxUtils.js +1061 -0
  8. package/dist/PerformanceUtils.d.ts +3 -5
  9. package/dist/index.d.ts +3 -2
  10. package/dist/storage/InstanceSync/index.d.ts +14 -0
  11. package/dist/storage/InstanceSync/index.js +20 -0
  12. package/dist/storage/InstanceSync/index.web.d.ts +27 -0
  13. package/dist/storage/InstanceSync/index.web.js +59 -0
  14. package/dist/storage/__mocks__/index.d.ts +15 -13
  15. package/dist/storage/__mocks__/index.js +43 -81
  16. package/dist/storage/index.d.ts +6 -2
  17. package/dist/storage/index.js +170 -2
  18. package/dist/storage/platforms/index.d.ts +2 -0
  19. package/dist/storage/{NativeStorage.js → platforms/index.js} +2 -2
  20. package/dist/storage/platforms/index.native.d.ts +2 -0
  21. package/dist/storage/{index.native.js → platforms/index.native.js} +2 -2
  22. package/dist/storage/providers/{IDBKeyVal.js → IDBKeyValProvider.js} +23 -19
  23. package/dist/storage/providers/MemoryOnlyProvider.d.ts +9 -0
  24. package/dist/storage/providers/MemoryOnlyProvider.js +124 -0
  25. package/dist/storage/providers/NoopProvider.js +85 -0
  26. package/dist/storage/providers/SQLiteProvider.d.ts +3 -0
  27. package/dist/storage/providers/{SQLiteStorage.js → SQLiteProvider.js} +17 -11
  28. package/dist/storage/providers/types.d.ts +17 -14
  29. package/dist/types.d.ts +128 -55
  30. package/dist/types.js +2 -0
  31. package/dist/useOnyx.js +11 -10
  32. package/dist/utils.d.ts +2 -2
  33. package/dist/utils.js +29 -16
  34. package/dist/withOnyx.js +6 -5
  35. package/package.json +1 -1
  36. package/dist/storage/NativeStorage.d.ts +0 -2
  37. package/dist/storage/WebStorage.d.ts +0 -3
  38. package/dist/storage/WebStorage.js +0 -62
  39. package/dist/storage/index.native.d.ts +0 -2
  40. /package/dist/storage/providers/{IDBKeyVal.d.ts → IDBKeyValProvider.d.ts} +0 -0
  41. /package/dist/storage/providers/{SQLiteStorage.d.ts → NoopProvider.d.ts} +0 -0
package/dist/Onyx.js CHANGED
@@ -27,761 +27,39 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  /* eslint-disable no-continue */
30
- const fast_equals_1 = require("fast-equals");
31
30
  const underscore_1 = __importDefault(require("underscore"));
32
31
  const Logger = __importStar(require("./Logger"));
33
32
  const OnyxCache_1 = __importDefault(require("./OnyxCache"));
34
- const Str = __importStar(require("./Str"));
35
33
  const createDeferredTask_1 = __importDefault(require("./createDeferredTask"));
36
34
  const PerformanceUtils = __importStar(require("./PerformanceUtils"));
37
35
  const storage_1 = __importDefault(require("./storage"));
38
36
  const utils_1 = __importDefault(require("./utils"));
39
- const batch_1 = __importDefault(require("./batch"));
40
37
  const DevTools_1 = __importDefault(require("./DevTools"));
41
- // Method constants
42
- const METHOD = {
43
- SET: 'set',
44
- MERGE: 'merge',
45
- MERGE_COLLECTION: 'mergecollection',
46
- MULTI_SET: 'multiset',
47
- CLEAR: 'clear',
48
- };
49
- // Key/value store of Onyx key and arrays of values to merge
50
- const mergeQueue = {};
51
- const mergeQueuePromise = {};
38
+ const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
52
39
  // Keeps track of the last connectionID that was used so we can keep incrementing it
53
40
  let lastConnectionID = 0;
54
- // Holds a mapping of all the react components that want their state subscribed to a store key
55
- const callbackToStateMapping = {};
56
- // Keeps a copy of the values of the onyx collection keys as a map for faster lookups
57
- let onyxCollectionKeyMap = new Map();
58
- // Holds a list of keys that have been directly subscribed to or recently modified from least to most recent
59
- let recentlyAccessedKeys = [];
60
- // Holds a list of keys that are safe to remove when we reach max storage. If a key does not match with
61
- // whatever appears in this list it will NEVER be a candidate for eviction.
62
- let evictionAllowList = [];
63
- // Holds a map of keys and connectionID arrays whose keys will never be automatically evicted as
64
- // long as we have at least one subscriber that returns false for the canEvict property.
65
- const evictionBlocklist = {};
66
- // Optional user-provided key value states set when Onyx initializes or clears
67
- let defaultKeyStates = {};
68
41
  // Connections can be made before `Onyx.init`. They would wait for this task before resolving
69
42
  const deferredInitTask = (0, createDeferredTask_1.default)();
70
- let batchUpdatesPromise = null;
71
- let batchUpdatesQueue = [];
72
- /**
73
- * Sends an action to DevTools extension
74
- *
75
- * @param {string} method - Onyx method from METHOD
76
- * @param {string} key - Onyx key that was changed
77
- * @param {any} value - contains the change that was made by the method
78
- * @param {any} mergedValue - (optional) value that was written in the storage after a merge method was executed.
79
- */
80
- function sendActionToDevTools(method, key, value, mergedValue = undefined) {
81
- DevTools_1.default.registerAction(utils_1.default.formatActionName(method, key), value, key ? { [key]: mergedValue || value } : value);
82
- }
83
- /**
84
- * We are batching together onyx updates. This helps with use cases where we schedule onyx updates after each other.
85
- * This happens for example in the Onyx.update function, where we process API responses that might contain a lot of
86
- * update operations. Instead of calling the subscribers for each update operation, we batch them together which will
87
- * cause react to schedule the updates at once instead of after each other. This is mainly a performance optimization.
88
- * @returns {Promise}
89
- */
90
- function maybeFlushBatchUpdates() {
91
- if (batchUpdatesPromise) {
92
- return batchUpdatesPromise;
93
- }
94
- batchUpdatesPromise = new Promise((resolve) => {
95
- /* We use (setTimeout, 0) here which should be called once native module calls are flushed (usually at the end of the frame)
96
- * We may investigate if (setTimeout, 1) (which in React Native is equal to requestAnimationFrame) works even better
97
- * then the batch will be flushed on next frame.
98
- */
99
- setTimeout(() => {
100
- const updatesCopy = batchUpdatesQueue;
101
- batchUpdatesQueue = [];
102
- batchUpdatesPromise = null;
103
- (0, batch_1.default)(() => {
104
- updatesCopy.forEach((applyUpdates) => {
105
- applyUpdates();
106
- });
107
- });
108
- resolve();
109
- }, 0);
110
- });
111
- return batchUpdatesPromise;
112
- }
113
- function batchUpdates(updates) {
114
- batchUpdatesQueue.push(updates);
115
- return maybeFlushBatchUpdates();
116
- }
117
- /**
118
- * Uses a selector function to return a simplified version of sourceData
119
- * @param {Mixed} sourceData
120
- * @param {Function} selector Function that takes sourceData and returns a simplified version of it
121
- * @param {Object} [withOnyxInstanceState]
122
- * @returns {Mixed}
123
- */
124
- const getSubsetOfData = (sourceData, selector, withOnyxInstanceState) => selector(sourceData, withOnyxInstanceState);
125
- /**
126
- * Takes a collection of items (eg. {testKey_1:{a:'a'}, testKey_2:{b:'b'}})
127
- * and runs it through a reducer function to return a subset of the data according to a selector.
128
- * The resulting collection will only contain items that are returned by the selector.
129
- * @param {Object} collection
130
- * @param {String|Function} selector (see method docs for getSubsetOfData() for full details)
131
- * @param {Object} [withOnyxInstanceState]
132
- * @returns {Object}
133
- */
134
- const reduceCollectionWithSelector = (collection, selector, withOnyxInstanceState) => underscore_1.default.reduce(collection, (finalCollection, item, key) => {
135
- // eslint-disable-next-line no-param-reassign
136
- finalCollection[key] = getSubsetOfData(item, selector, withOnyxInstanceState);
137
- return finalCollection;
138
- }, {});
139
- /**
140
- * Get some data from the store
141
- *
142
- * @private
143
- * @param {string} key
144
- * @returns {Promise<*>}
145
- */
146
- function get(key) {
147
- // When we already have the value in cache - resolve right away
148
- if (OnyxCache_1.default.hasCacheForKey(key)) {
149
- return Promise.resolve(OnyxCache_1.default.getValue(key));
150
- }
151
- const taskName = `get:${key}`;
152
- // When a value retrieving task for this key is still running hook to it
153
- if (OnyxCache_1.default.hasPendingTask(taskName)) {
154
- return OnyxCache_1.default.getTaskPromise(taskName);
155
- }
156
- // Otherwise retrieve the value from storage and capture a promise to aid concurrent usages
157
- const promise = storage_1.default.getItem(key)
158
- .then((val) => {
159
- OnyxCache_1.default.set(key, val);
160
- return val;
161
- })
162
- .catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
163
- return OnyxCache_1.default.captureTask(taskName, promise);
164
- }
165
- /**
166
- * Returns current key names stored in persisted storage
167
- * @private
168
- * @returns {Promise<Set<Key>>}
169
- */
170
- function getAllKeys() {
171
- // When we've already read stored keys, resolve right away
172
- const storedKeys = OnyxCache_1.default.getAllKeys();
173
- if (storedKeys.size > 0) {
174
- return Promise.resolve(storedKeys);
175
- }
176
- const taskName = 'getAllKeys';
177
- // When a value retrieving task for all keys is still running hook to it
178
- if (OnyxCache_1.default.hasPendingTask(taskName)) {
179
- return OnyxCache_1.default.getTaskPromise(taskName);
180
- }
181
- // Otherwise retrieve the keys from storage and capture a promise to aid concurrent usages
182
- const promise = storage_1.default.getAllKeys().then((keys) => {
183
- OnyxCache_1.default.setAllKeys(keys);
184
- // return the updated set of keys
185
- return OnyxCache_1.default.getAllKeys();
186
- });
187
- return OnyxCache_1.default.captureTask(taskName, promise);
188
- }
189
- /**
190
- * Checks to see if the a subscriber's supplied key
191
- * is associated with a collection of keys.
192
- *
193
- * @param {String} key
194
- * @returns {Boolean}
195
- */
196
- function isCollectionKey(key) {
197
- return onyxCollectionKeyMap.has(key);
198
- }
199
- /**
200
- * @param {String} collectionKey
201
- * @param {String} key
202
- * @returns {Boolean}
203
- */
204
- function isCollectionMemberKey(collectionKey, key) {
205
- return Str.startsWith(key, collectionKey) && key.length > collectionKey.length;
206
- }
207
- /**
208
- * Splits a collection member key into the collection key part and the ID part.
209
- * @param {String} key - The collection member key to split.
210
- * @returns {Array<String>} A tuple where the first element is the collection part and the second element is the ID part.
211
- */
212
- function splitCollectionMemberKey(key) {
213
- const underscoreIndex = key.indexOf('_');
214
- if (underscoreIndex === -1) {
215
- throw new Error(`Invalid ${key} key provided, only collection keys are allowed.`);
216
- }
217
- return [key.substring(0, underscoreIndex + 1), key.substring(underscoreIndex + 1)];
218
- }
219
- /**
220
- * Checks to see if a provided key is the exact configured key of our connected subscriber
221
- * or if the provided key is a collection member key (in case our configured key is a "collection key")
222
- *
223
- * @private
224
- * @param {String} configKey
225
- * @param {String} key
226
- * @return {Boolean}
227
- */
228
- function isKeyMatch(configKey, key) {
229
- return isCollectionKey(configKey) ? Str.startsWith(key, configKey) : configKey === key;
230
- }
231
- /**
232
- * Checks to see if this key has been flagged as
233
- * safe for removal.
234
- *
235
- * @private
236
- * @param {String} testKey
237
- * @returns {Boolean}
238
- */
239
- function isSafeEvictionKey(testKey) {
240
- return underscore_1.default.some(evictionAllowList, (key) => isKeyMatch(key, testKey));
241
- }
242
- /**
243
- * Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined.
244
- * If the requested key is a collection, it will return an object with all the collection members.
245
- *
246
- * @param {String} key
247
- * @param {Object} mapping
248
- * @returns {Mixed}
249
- */
250
- function tryGetCachedValue(key, mapping = {}) {
251
- let val = OnyxCache_1.default.getValue(key);
252
- if (isCollectionKey(key)) {
253
- const allCacheKeys = OnyxCache_1.default.getAllKeys();
254
- // It is possible we haven't loaded all keys yet so we do not know if the
255
- // collection actually exists.
256
- if (allCacheKeys.size === 0) {
257
- return;
258
- }
259
- const matchingKeys = [];
260
- allCacheKeys.forEach((k) => {
261
- if (!k.startsWith(key)) {
262
- return;
263
- }
264
- matchingKeys.push(k);
43
+ /** Initialize the store with actions and listening for storage events */
44
+ function init({ keys = {}, initialKeyStates = {}, safeEvictionKeys = [], maxCachedKeysCount = 1000, shouldSyncMultipleInstances = Boolean(global.localStorage), debugSetState = false, }) {
45
+ var _a;
46
+ storage_1.default.init();
47
+ if (shouldSyncMultipleInstances) {
48
+ (_a = storage_1.default.keepInstancesSync) === null || _a === void 0 ? void 0 : _a.call(storage_1.default, (key, value) => {
49
+ const prevValue = OnyxCache_1.default.getValue(key, false);
50
+ OnyxCache_1.default.set(key, value);
51
+ OnyxUtils_1.default.keyChanged(key, value, prevValue);
265
52
  });
266
- const values = underscore_1.default.reduce(matchingKeys, (finalObject, matchedKey) => {
267
- const cachedValue = OnyxCache_1.default.getValue(matchedKey);
268
- if (cachedValue) {
269
- // This is permissible because we're in the process of constructing the final object in a reduce function.
270
- // eslint-disable-next-line no-param-reassign
271
- finalObject[matchedKey] = cachedValue;
272
- }
273
- return finalObject;
274
- }, {});
275
- val = values;
276
- }
277
- if (mapping.selector) {
278
- const state = mapping.withOnyxInstance ? mapping.withOnyxInstance.state : undefined;
279
- if (isCollectionKey(key)) {
280
- return reduceCollectionWithSelector(val, mapping.selector, state);
281
- }
282
- return getSubsetOfData(val, mapping.selector, state);
283
- }
284
- return val;
285
- }
286
- /**
287
- * Remove a key from the recently accessed key list.
288
- *
289
- * @private
290
- * @param {String} key
291
- */
292
- function removeLastAccessedKey(key) {
293
- recentlyAccessedKeys = underscore_1.default.without(recentlyAccessedKeys, key);
294
- }
295
- /**
296
- * Add a key to the list of recently accessed keys. The least
297
- * recently accessed key should be at the head and the most
298
- * recently accessed key at the tail.
299
- *
300
- * @private
301
- * @param {String} key
302
- */
303
- function addLastAccessedKey(key) {
304
- // Only specific keys belong in this list since we cannot remove an entire collection.
305
- if (isCollectionKey(key) || !isSafeEvictionKey(key)) {
306
- return;
307
53
  }
308
- removeLastAccessedKey(key);
309
- recentlyAccessedKeys.push(key);
310
- }
311
- /**
312
- * Removes a key previously added to this list
313
- * which will enable it to be deleted again.
314
- *
315
- * @private
316
- * @param {String} key
317
- * @param {Number} connectionID
318
- */
319
- function removeFromEvictionBlockList(key, connectionID) {
320
- evictionBlocklist[key] = underscore_1.default.without(evictionBlocklist[key] || [], connectionID);
321
- // Remove the key if there are no more subscribers
322
- if (evictionBlocklist[key].length === 0) {
323
- delete evictionBlocklist[key];
324
- }
325
- }
326
- /**
327
- * Keys added to this list can never be deleted.
328
- *
329
- * @private
330
- * @param {String} key
331
- * @param {Number} connectionID
332
- */
333
- function addToEvictionBlockList(key, connectionID) {
334
- removeFromEvictionBlockList(key, connectionID);
335
- if (!evictionBlocklist[key]) {
336
- evictionBlocklist[key] = [];
337
- }
338
- evictionBlocklist[key].push(connectionID);
339
- }
340
- /**
341
- * Take all the keys that are safe to evict and add them to
342
- * the recently accessed list when initializing the app. This
343
- * enables keys that have not recently been accessed to be
344
- * removed.
345
- *
346
- * @private
347
- * @returns {Promise}
348
- */
349
- function addAllSafeEvictionKeysToRecentlyAccessedList() {
350
- return getAllKeys().then((keys) => {
351
- underscore_1.default.each(evictionAllowList, (safeEvictionKey) => {
352
- keys.forEach((key) => {
353
- if (!isKeyMatch(safeEvictionKey, key)) {
354
- return;
355
- }
356
- addLastAccessedKey(key);
357
- });
358
- });
359
- });
360
- }
361
- /**
362
- * @private
363
- * @param {String} collectionKey
364
- * @returns {Object}
365
- */
366
- function getCachedCollection(collectionKey) {
367
- const collectionMemberKeys = [];
368
- OnyxCache_1.default.getAllKeys().forEach((storedKey) => {
369
- if (!isCollectionMemberKey(collectionKey, storedKey)) {
370
- return;
371
- }
372
- collectionMemberKeys.push(storedKey);
373
- });
374
- return underscore_1.default.reduce(collectionMemberKeys, (prev, curr) => {
375
- const cachedValue = OnyxCache_1.default.getValue(curr);
376
- if (!cachedValue) {
377
- return prev;
378
- }
379
- // eslint-disable-next-line no-param-reassign
380
- prev[curr] = cachedValue;
381
- return prev;
382
- }, {});
383
- }
384
- /**
385
- * When a collection of keys change, search for any callbacks matching the collection key and trigger those callbacks
386
- *
387
- * @private
388
- * @param {String} collectionKey
389
- * @param {Object} partialCollection - a partial collection of grouped member keys
390
- * @param {boolean} [notifyRegularSubscibers=true]
391
- * @param {boolean} [notifyWithOnyxSubscibers=true]
392
- */
393
- function keysChanged(collectionKey, partialCollection, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true) {
394
- // We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or
395
- // 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
396
- // 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().
397
- const stateMappingKeys = underscore_1.default.keys(callbackToStateMapping);
398
- for (let i = 0; i < stateMappingKeys.length; i++) {
399
- const subscriber = callbackToStateMapping[stateMappingKeys[i]];
400
- if (!subscriber) {
401
- continue;
402
- }
403
- // Skip iteration if we do not have a collection key or a collection member key on this subscriber
404
- if (!Str.startsWith(subscriber.key, collectionKey)) {
405
- continue;
406
- }
407
- /**
408
- * e.g. Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT, callback: ...});
409
- */
410
- const isSubscribedToCollectionKey = subscriber.key === collectionKey;
411
- /**
412
- * e.g. Onyx.connect({key: `${ONYXKEYS.COLLECTION.REPORT}{reportID}`, callback: ...});
413
- */
414
- const isSubscribedToCollectionMemberKey = isCollectionMemberKey(collectionKey, subscriber.key);
415
- // We prepare the "cached collection" which is the entire collection + the new partial data that
416
- // was merged in via mergeCollection().
417
- const cachedCollection = getCachedCollection(collectionKey);
418
- // Regular Onyx.connect() subscriber found.
419
- if (underscore_1.default.isFunction(subscriber.callback)) {
420
- if (!notifyRegularSubscibers) {
421
- continue;
422
- }
423
- // If they are subscribed to the collection key and using waitForCollectionCallback then we'll
424
- // send the whole cached collection.
425
- if (isSubscribedToCollectionKey) {
426
- if (subscriber.waitForCollectionCallback) {
427
- subscriber.callback(cachedCollection);
428
- continue;
429
- }
430
- // If they are not using waitForCollectionCallback then we notify the subscriber with
431
- // the new merged data but only for any keys in the partial collection.
432
- const dataKeys = underscore_1.default.keys(partialCollection);
433
- for (let j = 0; j < dataKeys.length; j++) {
434
- const dataKey = dataKeys[j];
435
- subscriber.callback(cachedCollection[dataKey], dataKey);
436
- }
437
- continue;
438
- }
439
- // And if the subscriber is specifically only tracking a particular collection member key then we will
440
- // notify them with the cached data for that key only.
441
- if (isSubscribedToCollectionMemberKey) {
442
- subscriber.callback(cachedCollection[subscriber.key], subscriber.key);
443
- continue;
444
- }
445
- continue;
446
- }
447
- // React component subscriber found.
448
- if (subscriber.withOnyxInstance) {
449
- if (!notifyWithOnyxSubscibers) {
450
- continue;
451
- }
452
- // We are subscribed to a collection key so we must update the data in state with the new
453
- // collection member key values from the partial update.
454
- if (isSubscribedToCollectionKey) {
455
- // If the subscriber has a selector, then the component's state must only be updated with the data
456
- // returned by the selector.
457
- if (subscriber.selector) {
458
- subscriber.withOnyxInstance.setStateProxy((prevState) => {
459
- const previousData = prevState[subscriber.statePropertyName];
460
- const newData = reduceCollectionWithSelector(cachedCollection, subscriber.selector, subscriber.withOnyxInstance.state);
461
- if (!(0, fast_equals_1.deepEqual)(previousData, newData)) {
462
- return {
463
- [subscriber.statePropertyName]: newData,
464
- };
465
- }
466
- return null;
467
- });
468
- continue;
469
- }
470
- subscriber.withOnyxInstance.setStateProxy((prevState) => {
471
- const finalCollection = underscore_1.default.clone(prevState[subscriber.statePropertyName] || {});
472
- const dataKeys = underscore_1.default.keys(partialCollection);
473
- for (let j = 0; j < dataKeys.length; j++) {
474
- const dataKey = dataKeys[j];
475
- finalCollection[dataKey] = cachedCollection[dataKey];
476
- }
477
- PerformanceUtils.logSetStateCall(subscriber, prevState[subscriber.statePropertyName], finalCollection, 'keysChanged', collectionKey);
478
- return {
479
- [subscriber.statePropertyName]: finalCollection,
480
- };
481
- });
482
- continue;
483
- }
484
- // If a React component is only interested in a single key then we can set the cached value directly to the state name.
485
- if (isSubscribedToCollectionMemberKey) {
486
- // However, we only want to update this subscriber if the partial data contains a change.
487
- // Otherwise, we would update them with a value they already have and trigger an unnecessary re-render.
488
- const dataFromCollection = partialCollection[subscriber.key];
489
- if (underscore_1.default.isUndefined(dataFromCollection)) {
490
- continue;
491
- }
492
- // If the subscriber has a selector, then the component's state must only be updated with the data
493
- // returned by the selector and the state should only change when the subset of data changes from what
494
- // it was previously.
495
- if (subscriber.selector) {
496
- subscriber.withOnyxInstance.setStateProxy((prevState) => {
497
- const prevData = prevState[subscriber.statePropertyName];
498
- const newData = getSubsetOfData(cachedCollection[subscriber.key], subscriber.selector, subscriber.withOnyxInstance.state);
499
- if (!(0, fast_equals_1.deepEqual)(prevData, newData)) {
500
- PerformanceUtils.logSetStateCall(subscriber, prevData, newData, 'keysChanged', collectionKey);
501
- return {
502
- [subscriber.statePropertyName]: newData,
503
- };
504
- }
505
- return null;
506
- });
507
- continue;
508
- }
509
- subscriber.withOnyxInstance.setStateProxy((prevState) => {
510
- const data = cachedCollection[subscriber.key];
511
- const previousData = prevState[subscriber.statePropertyName];
512
- // Avoids triggering unnecessary re-renders when feeding empty objects
513
- if (utils_1.default.isEmptyObject(data) && utils_1.default.isEmptyObject(previousData)) {
514
- return null;
515
- }
516
- if (data === previousData) {
517
- return null;
518
- }
519
- PerformanceUtils.logSetStateCall(subscriber, previousData, data, 'keysChanged', collectionKey);
520
- return {
521
- [subscriber.statePropertyName]: data,
522
- };
523
- });
524
- }
525
- }
526
- }
527
- }
528
- /**
529
- * When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks
530
- *
531
- * @example
532
- * keyChanged(key, value, subscriber => subscriber.initWithStoredValues === false)
533
- *
534
- * @private
535
- * @param {String} key
536
- * @param {*} data
537
- * @param {*} prevData
538
- * @param {Function} [canUpdateSubscriber] only subscribers that pass this truth test will be updated
539
- * @param {boolean} [notifyRegularSubscibers=true]
540
- * @param {boolean} [notifyWithOnyxSubscibers=true]
541
- */
542
- function keyChanged(key, data, prevData, canUpdateSubscriber = () => true, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true) {
543
- // Add or remove this key from the recentlyAccessedKeys lists
544
- if (!underscore_1.default.isNull(data)) {
545
- addLastAccessedKey(key);
546
- }
547
- else {
548
- removeLastAccessedKey(key);
549
- }
550
- // We are iterating over all subscribers to see if they are interested in the key that has just changed. If the subscriber's key is a collection key then we will
551
- // notify them if the key that changed is a collection member. Or if it is a regular key notify them when there is an exact match. Depending on whether the subscriber
552
- // was connected via withOnyx we will call setState() directly on the withOnyx instance. If it is a regular connection we will pass the data to the provided callback.
553
- const stateMappingKeys = underscore_1.default.keys(callbackToStateMapping);
554
- for (let i = 0; i < stateMappingKeys.length; i++) {
555
- const subscriber = callbackToStateMapping[stateMappingKeys[i]];
556
- if (!subscriber || !isKeyMatch(subscriber.key, key) || !canUpdateSubscriber(subscriber)) {
557
- continue;
558
- }
559
- // Subscriber is a regular call to connect() and provided a callback
560
- if (underscore_1.default.isFunction(subscriber.callback)) {
561
- if (!notifyRegularSubscibers) {
562
- continue;
563
- }
564
- if (isCollectionKey(subscriber.key) && subscriber.waitForCollectionCallback) {
565
- const cachedCollection = getCachedCollection(subscriber.key);
566
- cachedCollection[key] = data;
567
- subscriber.callback(cachedCollection);
568
- continue;
569
- }
570
- subscriber.callback(data, key);
571
- continue;
572
- }
573
- // Subscriber connected via withOnyx() HOC
574
- if (subscriber.withOnyxInstance) {
575
- if (!notifyWithOnyxSubscibers) {
576
- continue;
577
- }
578
- // Check if we are subscribing to a collection key and overwrite the collection member key value in state
579
- if (isCollectionKey(subscriber.key)) {
580
- // If the subscriber has a selector, then the consumer of this data must only be given the data
581
- // returned by the selector and only when the selected data has changed.
582
- if (subscriber.selector) {
583
- subscriber.withOnyxInstance.setStateProxy((prevState) => {
584
- const prevWithOnyxData = prevState[subscriber.statePropertyName];
585
- const newWithOnyxData = {
586
- [key]: getSubsetOfData(data, subscriber.selector, subscriber.withOnyxInstance.state),
587
- };
588
- const prevDataWithNewData = Object.assign(Object.assign({}, prevWithOnyxData), newWithOnyxData);
589
- if (!(0, fast_equals_1.deepEqual)(prevWithOnyxData, prevDataWithNewData)) {
590
- PerformanceUtils.logSetStateCall(subscriber, prevWithOnyxData, newWithOnyxData, 'keyChanged', key);
591
- return {
592
- [subscriber.statePropertyName]: prevDataWithNewData,
593
- };
594
- }
595
- return null;
596
- });
597
- continue;
598
- }
599
- subscriber.withOnyxInstance.setStateProxy((prevState) => {
600
- const collection = prevState[subscriber.statePropertyName] || {};
601
- const newCollection = Object.assign(Object.assign({}, collection), { [key]: data });
602
- PerformanceUtils.logSetStateCall(subscriber, collection, newCollection, 'keyChanged', key);
603
- return {
604
- [subscriber.statePropertyName]: newCollection,
605
- };
606
- });
607
- continue;
608
- }
609
- // If the subscriber has a selector, then the component's state must only be updated with the data
610
- // returned by the selector and only if the selected data has changed.
611
- if (subscriber.selector) {
612
- subscriber.withOnyxInstance.setStateProxy(() => {
613
- const previousValue = getSubsetOfData(prevData, subscriber.selector, subscriber.withOnyxInstance.state);
614
- const newValue = getSubsetOfData(data, subscriber.selector, subscriber.withOnyxInstance.state);
615
- if (!(0, fast_equals_1.deepEqual)(previousValue, newValue)) {
616
- return {
617
- [subscriber.statePropertyName]: newValue,
618
- };
619
- }
620
- return null;
621
- });
622
- continue;
623
- }
624
- // If we did not match on a collection key then we just set the new data to the state property
625
- subscriber.withOnyxInstance.setStateProxy((prevState) => {
626
- const prevWithOnyxData = prevState[subscriber.statePropertyName];
627
- // Avoids triggering unnecessary re-renders when feeding empty objects
628
- if (utils_1.default.isEmptyObject(data) && utils_1.default.isEmptyObject(prevWithOnyxData)) {
629
- return null;
630
- }
631
- if (prevWithOnyxData === data) {
632
- return null;
633
- }
634
- PerformanceUtils.logSetStateCall(subscriber, prevData, data, 'keyChanged', key);
635
- return {
636
- [subscriber.statePropertyName]: data,
637
- };
638
- });
639
- continue;
640
- }
641
- console.error('Warning: Found a matching subscriber to a key that changed, but no callback or withOnyxInstance could be found.');
642
- }
643
- }
644
- /**
645
- * Sends the data obtained from the keys to the connection. It either:
646
- * - sets state on the withOnyxInstances
647
- * - triggers the callback function
648
- *
649
- * @private
650
- * @param {Object} mapping
651
- * @param {Object} [mapping.withOnyxInstance]
652
- * @param {String} [mapping.statePropertyName]
653
- * @param {Function} [mapping.callback]
654
- * @param {String} [mapping.selector]
655
- * @param {*|null} val
656
- * @param {String|undefined} matchedKey
657
- * @param {Boolean} isBatched
658
- */
659
- function sendDataToConnection(mapping, val, matchedKey, isBatched) {
660
- // If the mapping no longer exists then we should not send any data.
661
- // This means our subscriber disconnected or withOnyx wrapped component unmounted.
662
- if (!callbackToStateMapping[mapping.connectionID]) {
663
- return;
664
- }
665
- if (mapping.withOnyxInstance) {
666
- let newData = val;
667
- // If the mapping has a selector, then the component's state must only be updated with the data
668
- // returned by the selector.
669
- if (mapping.selector) {
670
- if (isCollectionKey(mapping.key)) {
671
- newData = reduceCollectionWithSelector(val, mapping.selector, mapping.withOnyxInstance.state);
672
- }
673
- else {
674
- newData = getSubsetOfData(val, mapping.selector, mapping.withOnyxInstance.state);
675
- }
676
- }
677
- PerformanceUtils.logSetStateCall(mapping, null, newData, 'sendDataToConnection');
678
- if (isBatched) {
679
- batchUpdates(() => {
680
- mapping.withOnyxInstance.setWithOnyxState(mapping.statePropertyName, newData);
681
- });
682
- }
683
- else {
684
- mapping.withOnyxInstance.setWithOnyxState(mapping.statePropertyName, newData);
685
- }
686
- return;
687
- }
688
- if (underscore_1.default.isFunction(mapping.callback)) {
689
- mapping.callback(val, matchedKey);
690
- }
691
- }
692
- /**
693
- * We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we
694
- * run out of storage the least recently accessed key can be removed.
695
- *
696
- * @private
697
- * @param {Object} mapping
698
- */
699
- function addKeyToRecentlyAccessedIfNeeded(mapping) {
700
- if (!isSafeEvictionKey(mapping.key)) {
701
- return;
54
+ if (debugSetState) {
55
+ PerformanceUtils.setShouldDebugSetState(true);
702
56
  }
703
- // Try to free some cache whenever we connect to a safe eviction key
704
- OnyxCache_1.default.removeLeastRecentlyUsedKeys();
705
- if (mapping.withOnyxInstance && !isCollectionKey(mapping.key)) {
706
- // All React components subscribing to a key flagged as a safe eviction key must implement the canEvict property.
707
- if (underscore_1.default.isUndefined(mapping.canEvict)) {
708
- throw new Error(`Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`);
709
- }
710
- addLastAccessedKey(mapping.key);
57
+ if (maxCachedKeysCount > 0) {
58
+ OnyxCache_1.default.setRecentKeysLimit(maxCachedKeysCount);
711
59
  }
712
- }
713
- /**
714
- * Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.
715
- *
716
- * @private
717
- * @param {Array} matchingKeys
718
- * @param {Object} mapping
719
- */
720
- function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
721
- // Keys that are not in the cache
722
- const missingKeys = [];
723
- // Tasks that are pending
724
- const pendingTasks = [];
725
- // Keys for the tasks that are pending
726
- const pendingKeys = [];
727
- // We are going to combine all the data from the matching keys into a single object
728
- const data = {};
729
- /**
730
- * We are going to iterate over all the matching keys and check if we have the data in the cache.
731
- * If we do then we add it to the data object. If we do not then we check if there is a pending task
732
- * for the key. If there is then we add the promise to the pendingTasks array and the key to the pendingKeys
733
- * array. If there is no pending task then we add the key to the missingKeys array.
734
- *
735
- * These missingKeys will be later to use to multiGet the data from the storage.
736
- */
737
- matchingKeys.forEach((key) => {
738
- const cacheValue = OnyxCache_1.default.getValue(key);
739
- if (cacheValue) {
740
- data[key] = cacheValue;
741
- return;
742
- }
743
- const pendingKey = `get:${key}`;
744
- if (OnyxCache_1.default.hasPendingTask(pendingKey)) {
745
- pendingTasks.push(OnyxCache_1.default.getTaskPromise(pendingKey));
746
- pendingKeys.push(key);
747
- }
748
- else {
749
- missingKeys.push(key);
750
- }
751
- });
752
- Promise.all(pendingTasks)
753
- // We are going to wait for all the pending tasks to resolve and then add the data to the data object.
754
- .then((values) => {
755
- values.forEach((value, index) => {
756
- data[pendingKeys[index]] = value;
757
- });
758
- return Promise.resolve();
759
- })
760
- // We are going to get the missing keys using multiGet from the storage.
761
- .then(() => {
762
- if (missingKeys.length === 0) {
763
- return Promise.resolve();
764
- }
765
- return storage_1.default.multiGet(missingKeys);
766
- })
767
- // We are going to add the data from the missing keys to the data object and also merge it to the cache.
768
- .then((values) => {
769
- if (!values || values.length === 0) {
770
- return Promise.resolve();
771
- }
772
- // temp object is used to merge the missing data into the cache
773
- const temp = {};
774
- values.forEach((value) => {
775
- data[value[0]] = value[1];
776
- temp[value[0]] = value[1];
777
- });
778
- OnyxCache_1.default.merge(temp);
779
- return Promise.resolve();
780
- })
781
- // We are going to send the data to the subscriber.
782
- .finally(() => {
783
- sendDataToConnection(mapping, data, undefined, true);
784
- });
60
+ OnyxUtils_1.default.initStoreValues(keys, initialKeyStates, safeEvictionKeys);
61
+ // Initialize all of our keys with data provided then give green light to any pending connections
62
+ Promise.all([OnyxUtils_1.default.addAllSafeEvictionKeysToRecentlyAccessedList(), OnyxUtils_1.default.initializeWithDefaultKeyStates()]).then(deferredInitTask.resolve);
785
63
  }
786
64
  /**
787
65
  * Subscribes a react component's state directly to a store key
@@ -792,27 +70,28 @@ function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
792
70
  * callback: onSessionChange,
793
71
  * });
794
72
  *
795
- * @param {Object} mapping the mapping information to connect Onyx to the components state
796
- * @param {String} mapping.key ONYXKEY to subscribe to
797
- * @param {String} [mapping.statePropertyName] the name of the property in the state to connect the data to
798
- * @param {Object} [mapping.withOnyxInstance] whose setState() method will be called with any changed data
73
+ * @param mapping the mapping information to connect Onyx to the components state
74
+ * @param mapping.key ONYXKEY to subscribe to
75
+ * @param [mapping.statePropertyName] the name of the property in the state to connect the data to
76
+ * @param [mapping.withOnyxInstance] whose setState() method will be called with any changed data
799
77
  * This is used by React components to connect to Onyx
800
- * @param {Function} [mapping.callback] a method that will be called with changed data
78
+ * @param [mapping.callback] a method that will be called with changed data
801
79
  * This is used by any non-React code to connect to Onyx
802
- * @param {Boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the
80
+ * @param [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the
803
81
  * component
804
- * @param {Boolean} [mapping.waitForCollectionCallback] If set to true, it will return the entire collection to the callback as a single object
805
- * @param {Function} [mapping.selector] THIS PARAM IS ONLY USED WITH withOnyx(). If included, this will be used to subscribe to a subset of an Onyx key's data.
82
+ * @param [mapping.waitForCollectionCallback] If set to true, it will return the entire collection to the callback as a single object
83
+ * @param [mapping.selector] THIS PARAM IS ONLY USED WITH withOnyx(). If included, this will be used to subscribe to a subset of an Onyx key's data.
806
84
  * The sourceData and withOnyx state are passed to the selector and should return the simplified data. Using this setting on `withOnyx` can have very positive
807
85
  * performance benefits because the component will only re-render when the subset of data changes. Otherwise, any change of data on any property would normally
808
86
  * cause the component to re-render (and that can be expensive from a performance standpoint).
809
- * @param {String | Number | Boolean | Object} [mapping.initialValue] THIS PARAM IS ONLY USED WITH withOnyx().
87
+ * @param [mapping.initialValue] THIS PARAM IS ONLY USED WITH withOnyx().
810
88
  * If included, this will be passed to the component so that something can be rendered while data is being fetched from the DB.
811
- * Note that it will not cause the component to have the loading prop set to true. |
812
- * @returns {Number} an ID to use when calling disconnect
89
+ * Note that it will not cause the component to have the loading prop set to true.
90
+ * @returns an ID to use when calling disconnect
813
91
  */
814
92
  function connect(mapping) {
815
93
  const connectionID = lastConnectionID++;
94
+ const callbackToStateMapping = OnyxUtils_1.default.getCallbackToStateMapping();
816
95
  callbackToStateMapping[connectionID] = mapping;
817
96
  callbackToStateMapping[connectionID].connectionID = connectionID;
818
97
  if (mapping.initWithStoredValues === false) {
@@ -820,68 +99,63 @@ function connect(mapping) {
820
99
  }
821
100
  // Commit connection only after init passes
822
101
  deferredInitTask.promise
823
- .then(() => addKeyToRecentlyAccessedIfNeeded(mapping))
102
+ .then(() => OnyxUtils_1.default.addKeyToRecentlyAccessedIfNeeded(mapping))
824
103
  .then(() => {
825
104
  // Performance improvement
826
105
  // If the mapping is connected to an onyx key that is not a collection
827
106
  // we can skip the call to getAllKeys() and return an array with a single item
828
107
  if (Boolean(mapping.key) && typeof mapping.key === 'string' && !mapping.key.endsWith('_') && OnyxCache_1.default.storageKeys.has(mapping.key)) {
829
- return [mapping.key];
108
+ return new Set([mapping.key]);
830
109
  }
831
- return getAllKeys();
110
+ return OnyxUtils_1.default.getAllKeys();
832
111
  })
833
112
  .then((keys) => {
834
113
  // We search all the keys in storage to see if any are a "match" for the subscriber we are connecting so that we
835
114
  // can send data back to the subscriber. Note that multiple keys can match as a subscriber could either be
836
115
  // subscribed to a "collection key" or a single key.
837
- const matchingKeys = [];
838
- keys.forEach((key) => {
839
- if (!isKeyMatch(mapping.key, key)) {
840
- return;
841
- }
842
- matchingKeys.push(key);
843
- });
116
+ const matchingKeys = Array.from(keys).filter((key) => OnyxUtils_1.default.isKeyMatch(mapping.key, key));
844
117
  // If the key being connected to does not exist we initialize the value with null. For subscribers that connected
845
118
  // directly via connect() they will simply get a null value sent to them without any information about which key matched
846
119
  // since there are none matched. In withOnyx() we wait for all connected keys to return a value before rendering the child
847
120
  // component. This null value will be filtered out so that the connected component can utilize defaultProps.
848
121
  if (matchingKeys.length === 0) {
849
- if (mapping.key && !isCollectionKey(mapping.key)) {
122
+ if (mapping.key && !OnyxUtils_1.default.isCollectionKey(mapping.key)) {
850
123
  OnyxCache_1.default.set(mapping.key, null);
851
124
  }
852
125
  // Here we cannot use batching because the null value is expected to be set immediately for default props
853
126
  // or they will be undefined.
854
- sendDataToConnection(mapping, null, undefined, false);
127
+ OnyxUtils_1.default.sendDataToConnection(mapping, null, undefined, false);
855
128
  return;
856
129
  }
857
130
  // When using a callback subscriber we will either trigger the provided callback for each key we find or combine all values
858
131
  // into an object and just make a single call. The latter behavior is enabled by providing a waitForCollectionCallback key
859
132
  // combined with a subscription to a collection key.
860
- if (underscore_1.default.isFunction(mapping.callback)) {
861
- if (isCollectionKey(mapping.key)) {
133
+ if (typeof mapping.callback === 'function') {
134
+ if (OnyxUtils_1.default.isCollectionKey(mapping.key)) {
862
135
  if (mapping.waitForCollectionCallback) {
863
- getCollectionDataAndSendAsObject(matchingKeys, mapping);
136
+ OnyxUtils_1.default.getCollectionDataAndSendAsObject(matchingKeys, mapping);
864
137
  return;
865
138
  }
866
139
  // We did not opt into using waitForCollectionCallback mode so the callback is called for every matching key.
140
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
867
141
  for (let i = 0; i < matchingKeys.length; i++) {
868
- get(matchingKeys[i]).then((val) => sendDataToConnection(mapping, val, matchingKeys[i], true));
142
+ OnyxUtils_1.default.get(matchingKeys[i]).then((val) => OnyxUtils_1.default.sendDataToConnection(mapping, val, matchingKeys[i], true));
869
143
  }
870
144
  return;
871
145
  }
872
146
  // If we are not subscribed to a collection key then there's only a single key to send an update for.
873
- get(mapping.key).then((val) => sendDataToConnection(mapping, val, mapping.key, true));
147
+ OnyxUtils_1.default.get(mapping.key).then((val) => OnyxUtils_1.default.sendDataToConnection(mapping, val, mapping.key, true));
874
148
  return;
875
149
  }
876
150
  // If we have a withOnyxInstance that means a React component has subscribed via the withOnyx() HOC and we need to
877
151
  // group collection key member data into an object.
878
152
  if (mapping.withOnyxInstance) {
879
- if (isCollectionKey(mapping.key)) {
880
- getCollectionDataAndSendAsObject(matchingKeys, mapping);
153
+ if (OnyxUtils_1.default.isCollectionKey(mapping.key)) {
154
+ OnyxUtils_1.default.getCollectionDataAndSendAsObject(matchingKeys, mapping);
881
155
  return;
882
156
  }
883
157
  // If the subscriber is not using a collection key then we just send a single value back to the subscriber
884
- get(mapping.key).then((val) => sendDataToConnection(mapping, val, mapping.key, true));
158
+ OnyxUtils_1.default.get(mapping.key).then((val) => OnyxUtils_1.default.sendDataToConnection(mapping, val, mapping.key, true));
885
159
  return;
886
160
  }
887
161
  console.error('Warning: Onyx.connect() was found without a callback or withOnyxInstance');
@@ -895,247 +169,70 @@ function connect(mapping) {
895
169
  * @example
896
170
  * Onyx.disconnect(connectionID);
897
171
  *
898
- * @param {Number} connectionID unique id returned by call to Onyx.connect()
899
- * @param {String} [keyToRemoveFromEvictionBlocklist]
172
+ * @param connectionID unique id returned by call to Onyx.connect()
900
173
  */
901
174
  function disconnect(connectionID, keyToRemoveFromEvictionBlocklist) {
175
+ const callbackToStateMapping = OnyxUtils_1.default.getCallbackToStateMapping();
902
176
  if (!callbackToStateMapping[connectionID]) {
903
177
  return;
904
178
  }
905
179
  // Remove this key from the eviction block list as we are no longer
906
180
  // subscribing to it and it should be safe to delete again
907
181
  if (keyToRemoveFromEvictionBlocklist) {
908
- removeFromEvictionBlockList(keyToRemoveFromEvictionBlocklist, connectionID);
182
+ OnyxUtils_1.default.removeFromEvictionBlockList(keyToRemoveFromEvictionBlocklist, connectionID);
909
183
  }
910
184
  delete callbackToStateMapping[connectionID];
911
185
  }
912
- /**
913
- * Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately).
914
- *
915
- * @example
916
- * scheduleSubscriberUpdate(key, value, subscriber => subscriber.initWithStoredValues === false)
917
- *
918
- * @param {String} key
919
- * @param {*} value
920
- * @param {*} prevValue
921
- * @param {Function} [canUpdateSubscriber] only subscribers that pass this truth test will be updated
922
- * @returns {Promise}
923
- */
924
- function scheduleSubscriberUpdate(key, value, prevValue, canUpdateSubscriber = () => true) {
925
- const promise = Promise.resolve().then(() => keyChanged(key, value, prevValue, canUpdateSubscriber, true, false));
926
- batchUpdates(() => keyChanged(key, value, prevValue, canUpdateSubscriber, false, true));
927
- return Promise.all([maybeFlushBatchUpdates(), promise]);
928
- }
929
- /**
930
- * This method is similar to notifySubscribersOnNextTick but it is built for working specifically with collections
931
- * so that keysChanged() is triggered for the collection and not keyChanged(). If this was not done, then the
932
- * subscriber callbacks receive the data in a different format than they normally expect and it breaks code.
933
- *
934
- * @param {String} key
935
- * @param {*} value
936
- * @returns {Promise}
937
- */
938
- function scheduleNotifyCollectionSubscribers(key, value) {
939
- const promise = Promise.resolve().then(() => keysChanged(key, value, true, false));
940
- batchUpdates(() => keysChanged(key, value, false, true));
941
- return Promise.all([maybeFlushBatchUpdates(), promise]);
942
- }
943
- /**
944
- * Remove a key from Onyx and update the subscribers
945
- *
946
- * @private
947
- * @param {String} key
948
- * @return {Promise}
949
- */
950
- function remove(key) {
951
- const prevValue = OnyxCache_1.default.getValue(key, false);
952
- OnyxCache_1.default.drop(key);
953
- scheduleSubscriberUpdate(key, null, prevValue);
954
- return storage_1.default.removeItem(key);
955
- }
956
- /**
957
- * @private
958
- * @returns {Promise<void>}
959
- */
960
- function reportStorageQuota() {
961
- return storage_1.default.getDatabaseSize()
962
- .then(({ bytesUsed, bytesRemaining }) => {
963
- Logger.logInfo(`Storage Quota Check -- bytesUsed: ${bytesUsed} bytesRemaining: ${bytesRemaining}`);
964
- })
965
- .catch((dbSizeError) => {
966
- Logger.logAlert(`Unable to get database size. Error: ${dbSizeError}`);
967
- });
968
- }
969
- /**
970
- * If we fail to set or merge we must handle this by
971
- * evicting some data from Onyx and then retrying to do
972
- * whatever it is we attempted to do.
973
- *
974
- * @private
975
- * @param {Error} error
976
- * @param {Function} onyxMethod
977
- * @param {...any} args
978
- * @return {Promise}
979
- */
980
- function evictStorageAndRetry(error, onyxMethod, ...args) {
981
- Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}`);
982
- if (error && Str.startsWith(error.message, "Failed to execute 'put' on 'IDBObjectStore'")) {
983
- Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.');
984
- throw error;
985
- }
986
- // Find the first key that we can remove that has no subscribers in our blocklist
987
- const keyForRemoval = underscore_1.default.find(recentlyAccessedKeys, (key) => !evictionBlocklist[key]);
988
- if (!keyForRemoval) {
989
- // If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case,
990
- // 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
991
- // will allow this write to be skipped.
992
- Logger.logAlert('Out of storage. But found no acceptable keys to remove.');
993
- return reportStorageQuota();
994
- }
995
- // Remove the least recently viewed key that is not currently being accessed and retry.
996
- Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`);
997
- reportStorageQuota();
998
- return remove(keyForRemoval).then(() => onyxMethod(...args));
999
- }
1000
- /**
1001
- * Notifys subscribers and writes current value to cache
1002
- *
1003
- * @param {String} key
1004
- * @param {*} value
1005
- * @param {String} method
1006
- * @param {Boolean} hasChanged
1007
- * @param {Boolean} wasRemoved
1008
- * @returns {Promise}
1009
- */
1010
- function broadcastUpdate(key, value, method, hasChanged, wasRemoved = false) {
1011
- // Logging properties only since values could be sensitive things we don't want to log
1012
- Logger.logInfo(`${method}() called for key: ${key}${underscore_1.default.isObject(value) ? ` properties: ${underscore_1.default.keys(value).join(',')}` : ''}`);
1013
- const prevValue = OnyxCache_1.default.getValue(key, false);
1014
- // Update subscribers if the cached value has changed, or when the subscriber specifically requires
1015
- // all updates regardless of value changes (indicated by initWithStoredValues set to false).
1016
- if (hasChanged && !wasRemoved) {
1017
- OnyxCache_1.default.set(key, value);
1018
- }
1019
- else {
1020
- OnyxCache_1.default.addToAccessedKeys(key);
1021
- }
1022
- return scheduleSubscriberUpdate(key, value, prevValue, (subscriber) => hasChanged || subscriber.initWithStoredValues === false);
1023
- }
1024
- /**
1025
- * @param {String} key
1026
- * @returns {Boolean}
1027
- */
1028
- function hasPendingMergeForKey(key) {
1029
- return Boolean(mergeQueue[key]);
1030
- }
1031
- /**
1032
- * Removes a key from storage if the value is null.
1033
- * Otherwise removes all nested null values in objects and returns the object
1034
- * @param {String} key
1035
- * @param {Mixed} value
1036
- * @returns {Mixed} The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely
1037
- */
1038
- function removeNullValues(key, value) {
1039
- if (underscore_1.default.isNull(value)) {
1040
- remove(key);
1041
- return { value, wasRemoved: true };
1042
- }
1043
- // We can remove all null values in an object by merging it with itself
1044
- // utils.fastMerge recursively goes through the object and removes all null values
1045
- // Passing two identical objects as source and target to fastMerge will not change it, but only remove the null values
1046
- return { value: utils_1.default.removeNestedNullValues(value), wasRemoved: false };
1047
- }
1048
186
  /**
1049
187
  * Write a value to our store with the given key
1050
188
  *
1051
- * @param {String} key ONYXKEY to set
1052
- * @param {*} value value to store
1053
- *
1054
- * @returns {Promise}
189
+ * @param key ONYXKEY to set
190
+ * @param value value to store
1055
191
  */
1056
192
  function set(key, value) {
1057
193
  // If the value is null, we remove the key from storage
1058
- const { value: valueAfterRemoving, wasRemoved } = removeNullValues(key, value);
1059
- if (hasPendingMergeForKey(key)) {
1060
- delete mergeQueue[key];
194
+ const { value: valueAfterRemoving, wasRemoved } = OnyxUtils_1.default.removeNullValues(key, value);
195
+ if (OnyxUtils_1.default.hasPendingMergeForKey(key)) {
196
+ delete OnyxUtils_1.default.getMergeQueue()[key];
1061
197
  }
1062
198
  const hasChanged = OnyxCache_1.default.hasValueChanged(key, valueAfterRemoving);
199
+ // Logging properties only since values could be sensitive things we don't want to log
200
+ Logger.logInfo(`set called for key: ${key}${underscore_1.default.isObject(value) ? ` properties: ${underscore_1.default.keys(value).join(',')}` : ''} hasChanged: ${hasChanged}`);
1063
201
  // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
1064
- const updatePromise = broadcastUpdate(key, valueAfterRemoving, 'set', hasChanged, wasRemoved);
202
+ const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, valueAfterRemoving, hasChanged, wasRemoved);
1065
203
  // If the value has not changed or the key got removed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
1066
204
  if (!hasChanged || wasRemoved) {
1067
205
  return updatePromise;
1068
206
  }
1069
207
  return storage_1.default.setItem(key, valueAfterRemoving)
1070
- .catch((error) => evictStorageAndRetry(error, set, key, valueAfterRemoving))
208
+ .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, set, key, valueAfterRemoving))
1071
209
  .then(() => {
1072
- sendActionToDevTools(METHOD.SET, key, valueAfterRemoving);
210
+ OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.SET, key, valueAfterRemoving);
1073
211
  return updatePromise;
1074
212
  });
1075
213
  }
1076
- /**
1077
- * Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
1078
- * This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
1079
- * to an array of key-value pairs in the above format and removes key-value pairs that are being set to null
1080
- * @private
1081
- * @param {Record} data
1082
- * @return {Array} an array of key - value pairs <[key, value]>
1083
- */
1084
- function prepareKeyValuePairsForStorage(data) {
1085
- const keyValuePairs = [];
1086
- underscore_1.default.forEach(data, (value, key) => {
1087
- const { value: valueAfterRemoving, wasRemoved } = removeNullValues(key, value);
1088
- if (wasRemoved)
1089
- return;
1090
- keyValuePairs.push([key, valueAfterRemoving]);
1091
- });
1092
- return keyValuePairs;
1093
- }
1094
214
  /**
1095
215
  * Sets multiple keys and values
1096
216
  *
1097
217
  * @example Onyx.multiSet({'key1': 'a', 'key2': 'b'});
1098
218
  *
1099
- * @param {Object} data object keyed by ONYXKEYS and the values to set
1100
- * @returns {Promise}
219
+ * @param data object keyed by ONYXKEYS and the values to set
1101
220
  */
1102
221
  function multiSet(data) {
1103
- const keyValuePairs = prepareKeyValuePairsForStorage(data);
1104
- const updatePromises = underscore_1.default.map(keyValuePairs, ([key, value]) => {
222
+ const keyValuePairs = OnyxUtils_1.default.prepareKeyValuePairsForStorage(data);
223
+ const updatePromises = keyValuePairs.map(([key, value]) => {
1105
224
  const prevValue = OnyxCache_1.default.getValue(key, false);
1106
225
  // Update cache and optimistically inform subscribers on the next tick
1107
226
  OnyxCache_1.default.set(key, value);
1108
- return scheduleSubscriberUpdate(key, value, prevValue);
227
+ return OnyxUtils_1.default.scheduleSubscriberUpdate(key, value, prevValue);
1109
228
  });
1110
229
  return storage_1.default.multiSet(keyValuePairs)
1111
- .catch((error) => evictStorageAndRetry(error, multiSet, data))
230
+ .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, multiSet, data))
1112
231
  .then(() => {
1113
- sendActionToDevTools(METHOD.MULTI_SET, undefined, data);
232
+ OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MULTI_SET, undefined, data);
1114
233
  return Promise.all(updatePromises);
1115
234
  });
1116
235
  }
1117
- /**
1118
- * Merges an array of changes with an existing value
1119
- *
1120
- * @private
1121
- * @param {*} existingValue
1122
- * @param {Array<*>} changes Array of changes that should be applied to the existing value
1123
- * @param {Boolean} shouldRemoveNullObjectValues
1124
- * @returns {*}
1125
- */
1126
- function applyMerge(existingValue, changes, shouldRemoveNullObjectValues) {
1127
- const lastChange = underscore_1.default.last(changes);
1128
- if (underscore_1.default.isArray(lastChange)) {
1129
- return lastChange;
1130
- }
1131
- if (underscore_1.default.some(changes, underscore_1.default.isObject)) {
1132
- // Object values are then merged one after the other
1133
- return underscore_1.default.reduce(changes, (modifiedData, change) => utils_1.default.fastMerge(modifiedData, change, shouldRemoveNullObjectValues), existingValue || {});
1134
- }
1135
- // If we have anything else we can't merge it so we'll
1136
- // simply return the last value that was queued
1137
- return lastChange;
1138
- }
1139
236
  /**
1140
237
  * Merge a new value into an existing value at a key.
1141
238
  *
@@ -1151,15 +248,13 @@ function applyMerge(existingValue, changes, shouldRemoveNullObjectValues) {
1151
248
  * Onyx.merge(ONYXKEYS.EMPLOYEE_LIST, ['Jack']); // -> ['Joe', 'Jack']
1152
249
  * Onyx.merge(ONYXKEYS.POLICY, {id: 1}); // -> {id: 1}
1153
250
  * Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Workspace'}
1154
- *
1155
- * @param {String} key ONYXKEYS key
1156
- * @param {(Object|Array)} changes Object or Array value to merge
1157
- * @returns {Promise}
1158
251
  */
1159
252
  function merge(key, changes) {
253
+ const mergeQueue = OnyxUtils_1.default.getMergeQueue();
254
+ const mergeQueuePromise = OnyxUtils_1.default.getMergeQueuePromise();
1160
255
  // Top-level undefined values are ignored
1161
256
  // Therefore we need to prevent adding them to the merge queue
1162
- if (underscore_1.default.isUndefined(changes)) {
257
+ if (changes === undefined) {
1163
258
  return mergeQueue[key] ? mergeQueuePromise[key] : Promise.resolve();
1164
259
  }
1165
260
  // Merge attempts are batched together. The delta should be applied after a single call to get() to prevent a race condition.
@@ -1169,42 +264,45 @@ function merge(key, changes) {
1169
264
  return mergeQueuePromise[key];
1170
265
  }
1171
266
  mergeQueue[key] = [changes];
1172
- mergeQueuePromise[key] = get(key).then((existingValue) => {
267
+ mergeQueuePromise[key] = OnyxUtils_1.default.get(key).then((existingValue) => {
1173
268
  // Calls to Onyx.set after a merge will terminate the current merge process and clear the merge queue
1174
- if (mergeQueue[key] == null)
1175
- return;
269
+ if (mergeQueue[key] == null) {
270
+ return undefined;
271
+ }
1176
272
  try {
1177
273
  // 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)
1178
274
  // We don't want to remove null values from the "batchedChanges", because SQLite uses them to remove keys from storage natively.
1179
- let batchedChanges = applyMerge(undefined, mergeQueue[key], false);
275
+ let batchedChanges = OnyxUtils_1.default.applyMerge(undefined, mergeQueue[key], false);
1180
276
  // The presence of a `null` in the merge queue instructs us to drop the existing value.
1181
277
  // 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
1182
- const shouldOverwriteExistingValue = underscore_1.default.includes(mergeQueue[key], null);
278
+ const shouldOverwriteExistingValue = mergeQueue[key].includes(null);
1183
279
  // Clean up the write queue, so we don't apply these changes again
1184
280
  delete mergeQueue[key];
1185
281
  delete mergeQueuePromise[key];
1186
282
  // If the batched changes equal null, we want to remove the key from storage, to reduce storage size
1187
- const { wasRemoved } = removeNullValues(key, batchedChanges);
283
+ const { wasRemoved } = OnyxUtils_1.default.removeNullValues(key, batchedChanges);
1188
284
  // After that we merge the batched changes with the existing value
1189
285
  // We can remove null values from the "modifiedData", because "null" implicates that the user wants to remove a value from storage.
1190
286
  // The "modifiedData" will be directly "set" in storage instead of being merged
1191
- const modifiedData = shouldOverwriteExistingValue ? batchedChanges : applyMerge(existingValue, [batchedChanges], true);
287
+ const modifiedData = shouldOverwriteExistingValue ? batchedChanges : OnyxUtils_1.default.applyMerge(existingValue, [batchedChanges], true);
1192
288
  // On native platforms we use SQLite which utilises JSON_PATCH to merge changes.
1193
289
  // JSON_PATCH generally removes null values from the stored object.
1194
290
  // 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.
1195
291
  // Therefore we need to remove null values from the `batchedChanges` which are sent to the SQLite, if no existing value is present.
1196
292
  if (!existingValue) {
1197
- batchedChanges = applyMerge(undefined, [batchedChanges], true);
293
+ batchedChanges = OnyxUtils_1.default.applyMerge(undefined, [batchedChanges], true);
1198
294
  }
1199
295
  const hasChanged = OnyxCache_1.default.hasValueChanged(key, modifiedData);
296
+ // Logging properties only since values could be sensitive things we don't want to log
297
+ Logger.logInfo(`merge called for key: ${key}${underscore_1.default.isObject(batchedChanges) ? ` properties: ${underscore_1.default.keys(batchedChanges).join(',')}` : ''} hasChanged: ${hasChanged}`);
1200
298
  // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
1201
- const updatePromise = broadcastUpdate(key, modifiedData, 'merge', hasChanged, wasRemoved);
299
+ const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, modifiedData, hasChanged, wasRemoved);
1202
300
  // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
1203
301
  if (!hasChanged || wasRemoved) {
1204
302
  return updatePromise;
1205
303
  }
1206
304
  return storage_1.default.mergeItem(key, batchedChanges, modifiedData).then(() => {
1207
- sendActionToDevTools(METHOD.MERGE, key, changes, modifiedData);
305
+ OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MERGE, key, changes, modifiedData);
1208
306
  return updatePromise;
1209
307
  });
1210
308
  }
@@ -1216,16 +314,84 @@ function merge(key, changes) {
1216
314
  return mergeQueuePromise[key];
1217
315
  }
1218
316
  /**
1219
- * Merge user provided default key value pairs.
1220
- * @private
1221
- * @returns {Promise}
317
+ * Merges a collection based on their keys
318
+ *
319
+ * @example
320
+ *
321
+ * Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, {
322
+ * [`${ONYXKEYS.COLLECTION.REPORT}1`]: report1,
323
+ * [`${ONYXKEYS.COLLECTION.REPORT}2`]: report2,
324
+ * });
325
+ *
326
+ * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
327
+ * @param collection Object collection keyed by individual collection member keys and values
1222
328
  */
1223
- function initializeWithDefaultKeyStates() {
1224
- return storage_1.default.multiGet(underscore_1.default.keys(defaultKeyStates)).then((pairs) => {
1225
- const existingDataAsObject = underscore_1.default.object(pairs);
1226
- const merged = utils_1.default.fastMerge(existingDataAsObject, defaultKeyStates);
1227
- OnyxCache_1.default.merge(merged);
1228
- underscore_1.default.each(merged, (val, key) => keyChanged(key, val, existingDataAsObject));
329
+ function mergeCollection(collectionKey, collection) {
330
+ if (typeof collection !== 'object' || Array.isArray(collection) || utils_1.default.isEmptyObject(collection)) {
331
+ Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
332
+ return Promise.resolve();
333
+ }
334
+ const mergedCollection = collection;
335
+ // Confirm all the collection keys belong to the same parent
336
+ let hasCollectionKeyCheckFailed = false;
337
+ Object.keys(mergedCollection).forEach((dataKey) => {
338
+ if (OnyxUtils_1.default.isKeyMatch(collectionKey, dataKey)) {
339
+ return;
340
+ }
341
+ if (process.env.NODE_ENV === 'development') {
342
+ throw new Error(`Provided collection doesn't have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
343
+ }
344
+ hasCollectionKeyCheckFailed = true;
345
+ Logger.logAlert(`Provided collection doesn't have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
346
+ });
347
+ // Gracefully handle bad mergeCollection updates so it doesn't block the merge queue
348
+ if (hasCollectionKeyCheckFailed) {
349
+ return Promise.resolve();
350
+ }
351
+ return OnyxUtils_1.default.getAllKeys().then((persistedKeys) => {
352
+ // Split to keys that exist in storage and keys that don't
353
+ const keys = Object.keys(mergedCollection).filter((key) => {
354
+ if (mergedCollection[key] === null) {
355
+ OnyxUtils_1.default.remove(key);
356
+ return false;
357
+ }
358
+ return true;
359
+ });
360
+ const existingKeys = keys.filter((key) => persistedKeys.has(key));
361
+ const newKeys = keys.filter((key) => !persistedKeys.has(key));
362
+ const existingKeyCollection = existingKeys.reduce((obj, key) => {
363
+ // eslint-disable-next-line no-param-reassign
364
+ obj[key] = mergedCollection[key];
365
+ return obj;
366
+ }, {});
367
+ const newCollection = newKeys.reduce((obj, key) => {
368
+ // eslint-disable-next-line no-param-reassign
369
+ obj[key] = mergedCollection[key];
370
+ return obj;
371
+ }, {});
372
+ const keyValuePairsForExistingCollection = OnyxUtils_1.default.prepareKeyValuePairsForStorage(existingKeyCollection);
373
+ const keyValuePairsForNewCollection = OnyxUtils_1.default.prepareKeyValuePairsForStorage(newCollection);
374
+ const promises = [];
375
+ // New keys will be added via multiSet while existing keys will be updated using multiMerge
376
+ // This is because setting a key that doesn't exist yet with multiMerge will throw errors
377
+ if (keyValuePairsForExistingCollection.length > 0) {
378
+ promises.push(storage_1.default.multiMerge(keyValuePairsForExistingCollection));
379
+ }
380
+ if (keyValuePairsForNewCollection.length > 0) {
381
+ promises.push(storage_1.default.multiSet(keyValuePairsForNewCollection));
382
+ }
383
+ // Prefill cache if necessary by calling get() on any existing keys and then merge original data to cache
384
+ // and update all subscribers
385
+ const promiseUpdate = Promise.all(existingKeys.map(OnyxUtils_1.default.get)).then(() => {
386
+ OnyxCache_1.default.merge(mergedCollection);
387
+ return OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(collectionKey, mergedCollection);
388
+ });
389
+ return Promise.all(promises)
390
+ .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, mergeCollection, collectionKey, mergedCollection))
391
+ .then(() => {
392
+ OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MERGE_COLLECTION, undefined, mergedCollection);
393
+ return promiseUpdate;
394
+ });
1229
395
  });
1230
396
  }
1231
397
  /**
@@ -1247,11 +413,10 @@ function initializeWithDefaultKeyStates() {
1247
413
  * Storage.setItem() from Onyx.clear() will have already finished and the merged
1248
414
  * value will be saved to storage after the default value.
1249
415
  *
1250
- * @param {Array} keysToPreserve is a list of ONYXKEYS that should not be cleared with the rest of the data
1251
- * @returns {Promise<void>}
416
+ * @param keysToPreserve is a list of ONYXKEYS that should not be cleared with the rest of the data
1252
417
  */
1253
418
  function clear(keysToPreserve = []) {
1254
- return getAllKeys().then((keys) => {
419
+ return OnyxUtils_1.default.getAllKeys().then((keys) => {
1255
420
  const keysToBeClearedFromStorage = [];
1256
421
  const keyValuesToResetAsCollection = {};
1257
422
  const keyValuesToResetIndividually = {};
@@ -1261,15 +426,17 @@ function clear(keysToPreserve = []) {
1261
426
  // 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them
1262
427
  // to null would cause unknown behavior)
1263
428
  keys.forEach((key) => {
1264
- const isKeyToPreserve = underscore_1.default.contains(keysToPreserve, key);
1265
- const isDefaultKey = underscore_1.default.has(defaultKeyStates, key);
429
+ var _a;
430
+ const isKeyToPreserve = keysToPreserve.includes(key);
431
+ const defaultKeyStates = OnyxUtils_1.default.getDefaultKeyStates();
432
+ const isDefaultKey = key in defaultKeyStates;
1266
433
  // If the key is being removed or reset to default:
1267
434
  // 1. Update it in the cache
1268
435
  // 2. Figure out whether it is a collection key or not,
1269
436
  // since collection key subscribers need to be updated differently
1270
437
  if (!isKeyToPreserve) {
1271
438
  const oldValue = OnyxCache_1.default.getValue(key);
1272
- const newValue = underscore_1.default.get(defaultKeyStates, key, null);
439
+ const newValue = (_a = defaultKeyStates[key]) !== null && _a !== void 0 ? _a : null;
1273
440
  if (newValue !== oldValue) {
1274
441
  OnyxCache_1.default.set(key, newValue);
1275
442
  const collectionKey = key.substring(0, key.indexOf('_') + 1);
@@ -1292,15 +459,22 @@ function clear(keysToPreserve = []) {
1292
459
  });
1293
460
  const updatePromises = [];
1294
461
  // Notify the subscribers for each key/value group so they can receive the new values
1295
- underscore_1.default.each(keyValuesToResetIndividually, (value, key) => {
1296
- updatePromises.push(scheduleSubscriberUpdate(key, value, OnyxCache_1.default.getValue(key, false)));
462
+ Object.entries(keyValuesToResetIndividually).forEach(([key, value]) => {
463
+ updatePromises.push(OnyxUtils_1.default.scheduleSubscriberUpdate(key, value, OnyxCache_1.default.getValue(key, false)));
1297
464
  });
1298
- underscore_1.default.each(keyValuesToResetAsCollection, (value, key) => {
1299
- updatePromises.push(scheduleNotifyCollectionSubscribers(key, value));
465
+ Object.entries(keyValuesToResetAsCollection).forEach(([key, value]) => {
466
+ updatePromises.push(OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(key, value));
1300
467
  });
1301
- const defaultKeyValuePairs = underscore_1.default.pairs(underscore_1.default.omit(defaultKeyStates, keysToPreserve));
468
+ const defaultKeyStates = OnyxUtils_1.default.getDefaultKeyStates();
469
+ const defaultKeyValuePairs = Object.entries(Object.keys(defaultKeyStates)
470
+ .filter((key) => !keysToPreserve.includes(key))
471
+ .reduce((obj, key) => {
472
+ // eslint-disable-next-line no-param-reassign
473
+ obj[key] = defaultKeyStates[key];
474
+ return obj;
475
+ }, {}));
1302
476
  // Remove only the items that we want cleared from storage, and reset others to default
1303
- underscore_1.default.each(keysToBeClearedFromStorage, (key) => OnyxCache_1.default.drop(key));
477
+ keysToBeClearedFromStorage.forEach((key) => OnyxCache_1.default.drop(key));
1304
478
  return storage_1.default.removeItems(keysToBeClearedFromStorage)
1305
479
  .then(() => storage_1.default.multiSet(defaultKeyValuePairs))
1306
480
  .then(() => {
@@ -1309,192 +483,56 @@ function clear(keysToPreserve = []) {
1309
483
  });
1310
484
  });
1311
485
  }
1312
- /**
1313
- * Merges a collection based on their keys
1314
- *
1315
- * @example
1316
- *
1317
- * Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, {
1318
- * [`${ONYXKEYS.COLLECTION.REPORT}1`]: report1,
1319
- * [`${ONYXKEYS.COLLECTION.REPORT}2`]: report2,
1320
- * });
1321
- *
1322
- * @param {String} collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
1323
- * @param {Object} collection Object collection keyed by individual collection member keys and values
1324
- * @returns {Promise}
1325
- */
1326
- function mergeCollection(collectionKey, collection) {
1327
- if (!underscore_1.default.isObject(collection) || underscore_1.default.isArray(collection) || underscore_1.default.isEmpty(collection)) {
1328
- Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
1329
- return Promise.resolve();
1330
- }
1331
- // Confirm all the collection keys belong to the same parent
1332
- let hasCollectionKeyCheckFailed = false;
1333
- underscore_1.default.each(collection, (_data, dataKey) => {
1334
- if (isKeyMatch(collectionKey, dataKey)) {
1335
- return;
1336
- }
1337
- if (process.env.NODE_ENV === 'development') {
1338
- throw new Error(`Provided collection doesn't have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
1339
- }
1340
- hasCollectionKeyCheckFailed = true;
1341
- Logger.logAlert(`Provided collection doesn't have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
1342
- });
1343
- // Gracefully handle bad mergeCollection updates so it doesn't block the merge queue
1344
- if (hasCollectionKeyCheckFailed) {
1345
- return Promise.resolve();
1346
- }
1347
- return getAllKeys().then((persistedKeys) => {
1348
- // Split to keys that exist in storage and keys that don't
1349
- const [existingKeys, newKeys] = underscore_1.default.chain(collection)
1350
- .pick((value, key) => {
1351
- if (underscore_1.default.isNull(value)) {
1352
- remove(key);
1353
- return false;
1354
- }
1355
- return true;
1356
- })
1357
- .keys()
1358
- .partition((key) => persistedKeys.has(key))
1359
- .value();
1360
- const existingKeyCollection = underscore_1.default.pick(collection, existingKeys);
1361
- const newCollection = underscore_1.default.pick(collection, newKeys);
1362
- const keyValuePairsForExistingCollection = prepareKeyValuePairsForStorage(existingKeyCollection);
1363
- const keyValuePairsForNewCollection = prepareKeyValuePairsForStorage(newCollection);
1364
- const promises = [];
1365
- // New keys will be added via multiSet while existing keys will be updated using multiMerge
1366
- // This is because setting a key that doesn't exist yet with multiMerge will throw errors
1367
- if (keyValuePairsForExistingCollection.length > 0) {
1368
- promises.push(storage_1.default.multiMerge(keyValuePairsForExistingCollection));
1369
- }
1370
- if (keyValuePairsForNewCollection.length > 0) {
1371
- promises.push(storage_1.default.multiSet(keyValuePairsForNewCollection));
1372
- }
1373
- // Prefill cache if necessary by calling get() on any existing keys and then merge original data to cache
1374
- // and update all subscribers
1375
- const promiseUpdate = Promise.all(underscore_1.default.map(existingKeys, get)).then(() => {
1376
- OnyxCache_1.default.merge(collection);
1377
- return scheduleNotifyCollectionSubscribers(collectionKey, collection);
1378
- });
1379
- return Promise.all(promises)
1380
- .catch((error) => evictStorageAndRetry(error, mergeCollection, collection))
1381
- .then(() => {
1382
- sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, collection);
1383
- return promiseUpdate;
1384
- });
1385
- });
1386
- }
1387
486
  /**
1388
487
  * Insert API responses and lifecycle data into Onyx
1389
488
  *
1390
- * @param {Array} data An array of objects with shape {onyxMethod: oneOf('set', 'merge', 'mergeCollection', 'multiSet', 'clear'), key: string, value: *}
1391
- * @returns {Promise} resolves when all operations are complete
489
+ * @param data An array of objects with shape {onyxMethod: oneOf('set', 'merge', 'mergeCollection', 'multiSet', 'clear'), key: string, value: *}
490
+ * @returns resolves when all operations are complete
1392
491
  */
1393
492
  function update(data) {
1394
493
  // First, validate the Onyx object is in the format we expect
1395
- underscore_1.default.each(data, ({ onyxMethod, key, value }) => {
1396
- if (!underscore_1.default.contains([METHOD.CLEAR, METHOD.SET, METHOD.MERGE, METHOD.MERGE_COLLECTION, METHOD.MULTI_SET], onyxMethod)) {
494
+ data.forEach(({ onyxMethod, key, value }) => {
495
+ if (![OnyxUtils_1.default.METHOD.CLEAR, OnyxUtils_1.default.METHOD.SET, OnyxUtils_1.default.METHOD.MERGE, OnyxUtils_1.default.METHOD.MERGE_COLLECTION, OnyxUtils_1.default.METHOD.MULTI_SET].includes(onyxMethod)) {
1397
496
  throw new Error(`Invalid onyxMethod ${onyxMethod} in Onyx update.`);
1398
497
  }
1399
- if (onyxMethod === METHOD.MULTI_SET) {
498
+ if (onyxMethod === OnyxUtils_1.default.METHOD.MULTI_SET) {
1400
499
  // For multiset, we just expect the value to be an object
1401
- if (!underscore_1.default.isObject(value) || underscore_1.default.isArray(value) || underscore_1.default.isFunction(value)) {
500
+ if (typeof value !== 'object' || Array.isArray(value) || typeof value === 'function') {
1402
501
  throw new Error('Invalid value provided in Onyx multiSet. Onyx multiSet value must be of type object.');
1403
502
  }
1404
503
  }
1405
- else if (onyxMethod !== METHOD.CLEAR && !underscore_1.default.isString(key)) {
504
+ else if (onyxMethod !== OnyxUtils_1.default.METHOD.CLEAR && typeof key !== 'string') {
1406
505
  throw new Error(`Invalid ${typeof key} key provided in Onyx update. Onyx key must be of type string.`);
1407
506
  }
1408
507
  });
1409
508
  const promises = [];
1410
509
  let clearPromise = Promise.resolve();
1411
- underscore_1.default.each(data, ({ onyxMethod, key, value }) => {
510
+ data.forEach(({ onyxMethod, key, value }) => {
1412
511
  switch (onyxMethod) {
1413
- case METHOD.SET:
512
+ case OnyxUtils_1.default.METHOD.SET:
1414
513
  promises.push(() => set(key, value));
1415
514
  break;
1416
- case METHOD.MERGE:
515
+ case OnyxUtils_1.default.METHOD.MERGE:
1417
516
  promises.push(() => merge(key, value));
1418
517
  break;
1419
- case METHOD.MERGE_COLLECTION:
518
+ case OnyxUtils_1.default.METHOD.MERGE_COLLECTION:
519
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- We validated that the value is a collection
1420
520
  promises.push(() => mergeCollection(key, value));
1421
521
  break;
1422
- case METHOD.MULTI_SET:
522
+ case OnyxUtils_1.default.METHOD.MULTI_SET:
1423
523
  promises.push(() => multiSet(value));
1424
524
  break;
1425
- case METHOD.CLEAR:
525
+ case OnyxUtils_1.default.METHOD.CLEAR:
1426
526
  clearPromise = clear();
1427
527
  break;
1428
528
  default:
1429
529
  break;
1430
530
  }
1431
531
  });
1432
- return clearPromise.then(() => Promise.all(underscore_1.default.map(promises, (p) => p())));
1433
- }
1434
- /**
1435
- * When set these keys will not be persisted to storage
1436
- * @param {string[]} keyList
1437
- */
1438
- function setMemoryOnlyKeys(keyList) {
1439
- storage_1.default.setMemoryOnlyKeys(keyList);
1440
- // When in memory only mode for certain keys we do not want to ever drop items from the cache as the user will have no way to recover them again via storage.
1441
- OnyxCache_1.default.setRecentKeysLimit(Infinity);
1442
- }
1443
- /**
1444
- * Initialize the store with actions and listening for storage events
1445
- *
1446
- * @param {Object} [options={}] config object
1447
- * @param {Object} [options.keys={}] `ONYXKEYS` constants object
1448
- * @param {Object} [options.initialKeyStates={}] initial data to set when `init()` and `clear()` is called
1449
- * @param {String[]} [options.safeEvictionKeys=[]] This is an array of keys
1450
- * (individual or collection patterns) that when provided to Onyx are flagged
1451
- * as "safe" for removal. Any components subscribing to these keys must also
1452
- * implement a canEvict option. See the README for more info.
1453
- * @param {Number} [options.maxCachedKeysCount=55] Sets how many recent keys should we try to keep in cache
1454
- * Setting this to 0 would practically mean no cache
1455
- * We try to free cache when we connect to a safe eviction key
1456
- * @param {Boolean} [options.captureMetrics] Enables Onyx benchmarking and exposes the get/print/reset functions
1457
- * @param {Boolean} [options.shouldSyncMultipleInstances] Auto synchronize storage events between multiple instances
1458
- * of Onyx running in different tabs/windows. Defaults to true for platforms that support local storage (web/desktop)
1459
- * @param {Boolean} [options.debugSetState] Enables debugging setState() calls to connected components.
1460
- * @example
1461
- * Onyx.init({
1462
- * keys: ONYXKEYS,
1463
- * initialKeyStates: {
1464
- * [ONYXKEYS.SESSION]: {loading: false},
1465
- * },
1466
- * });
1467
- */
1468
- function init({ keys = {}, initialKeyStates = {}, safeEvictionKeys = [], maxCachedKeysCount = 1000, shouldSyncMultipleInstances = Boolean(global.localStorage), debugSetState = false } = {}) {
1469
- if (debugSetState) {
1470
- PerformanceUtils.setShouldDebugSetState(true);
1471
- }
1472
- if (maxCachedKeysCount > 0) {
1473
- OnyxCache_1.default.setRecentKeysLimit(maxCachedKeysCount);
1474
- }
1475
- // We need the value of the collection keys later for checking if a
1476
- // key is a collection. We store it in a map for faster lookup.
1477
- const collectionValues = underscore_1.default.values(keys.COLLECTION);
1478
- onyxCollectionKeyMap = underscore_1.default.reduce(collectionValues, (acc, val) => {
1479
- acc.set(val, true);
1480
- return acc;
1481
- }, new Map());
1482
- // Set our default key states to use when initializing and clearing Onyx data
1483
- defaultKeyStates = initialKeyStates;
1484
- DevTools_1.default.initState(initialKeyStates);
1485
- // Let Onyx know about which keys are safe to evict
1486
- evictionAllowList = safeEvictionKeys;
1487
- // Initialize all of our keys with data provided then give green light to any pending connections
1488
- Promise.all([addAllSafeEvictionKeysToRecentlyAccessedList(), initializeWithDefaultKeyStates()]).then(deferredInitTask.resolve);
1489
- if (shouldSyncMultipleInstances && underscore_1.default.isFunction(storage_1.default.keepInstancesSync)) {
1490
- storage_1.default.keepInstancesSync((key, value) => {
1491
- const prevValue = OnyxCache_1.default.getValue(key, false);
1492
- OnyxCache_1.default.set(key, value);
1493
- keyChanged(key, value, prevValue);
1494
- });
1495
- }
532
+ return clearPromise.then(() => Promise.all(promises.map((p) => p())));
1496
533
  }
1497
534
  const Onyx = {
535
+ METHOD: OnyxUtils_1.default.METHOD,
1498
536
  connect,
1499
537
  disconnect,
1500
538
  set,
@@ -1503,18 +541,7 @@ const Onyx = {
1503
541
  mergeCollection,
1504
542
  update,
1505
543
  clear,
1506
- getAllKeys,
1507
544
  init,
1508
545
  registerLogger: Logger.registerLogger,
1509
- addToEvictionBlockList,
1510
- removeFromEvictionBlockList,
1511
- isSafeEvictionKey,
1512
- METHOD,
1513
- setMemoryOnlyKeys,
1514
- tryGetCachedValue,
1515
- hasPendingMergeForKey,
1516
- isCollectionKey,
1517
- isCollectionMemberKey,
1518
- splitCollectionMemberKey,
1519
546
  };
1520
547
  exports.default = Onyx;