react-native-onyx 1.0.17 → 1.0.19
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/API.md +12 -0
- package/dist/web.development.js +292 -168
- package/dist/web.development.js.map +1 -1
- package/dist/web.min.js +1 -1
- package/dist/web.min.js.map +1 -1
- package/lib/Onyx.js +209 -108
- package/lib/OnyxCache.js +4 -2
- package/lib/fastMerge.js +66 -0
- package/lib/storage/providers/LocalForage.js +5 -2
- package/package.json +1 -1
- package/lib/mergeWithCustomized.js +0 -26
package/lib/Onyx.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
/* eslint-disable no-continue */
|
|
1
2
|
import _ from 'underscore';
|
|
2
3
|
import Str from 'expensify-common/lib/str';
|
|
3
|
-
import lodashMerge from 'lodash/merge';
|
|
4
4
|
import lodashGet from 'lodash/get';
|
|
5
5
|
import Storage from './storage';
|
|
6
6
|
import * as Logger from './Logger';
|
|
7
7
|
import cache from './OnyxCache';
|
|
8
8
|
import createDeferredTask from './createDeferredTask';
|
|
9
|
-
import
|
|
9
|
+
import fastMerge from './fastMerge';
|
|
10
10
|
|
|
11
11
|
// Keeps track of the last connectionID that was used so we can keep incrementing it
|
|
12
12
|
let lastConnectionID = 0;
|
|
@@ -107,8 +107,17 @@ function isCollectionKey(key) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
110
|
+
* @param {String} collectionKey
|
|
111
|
+
* @param {String} key
|
|
112
|
+
* @returns {Boolean}
|
|
113
|
+
*/
|
|
114
|
+
function isCollectionMemberKey(collectionKey, key) {
|
|
115
|
+
return Str.startsWith(key, collectionKey) && key.length > collectionKey.length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Checks to see if a provided key is the exact configured key of our connected subscriber
|
|
120
|
+
* or if the provided key is a collection member key (in case our configured key is a "collection key")
|
|
112
121
|
*
|
|
113
122
|
* @private
|
|
114
123
|
* @param {String} configKey
|
|
@@ -225,7 +234,7 @@ function addAllSafeEvictionKeysToRecentlyAccessedList() {
|
|
|
225
234
|
*/
|
|
226
235
|
function getCachedCollection(collectionKey) {
|
|
227
236
|
const collectionMemberKeys = _.filter(cache.getAllKeys(), (
|
|
228
|
-
storedKey =>
|
|
237
|
+
storedKey => isCollectionMemberKey(collectionKey, storedKey)
|
|
229
238
|
));
|
|
230
239
|
|
|
231
240
|
return _.reduce(collectionMemberKeys, (prev, curr) => {
|
|
@@ -245,73 +254,103 @@ function getCachedCollection(collectionKey) {
|
|
|
245
254
|
*
|
|
246
255
|
* @private
|
|
247
256
|
* @param {String} collectionKey
|
|
248
|
-
* @param {Object} collection
|
|
257
|
+
* @param {Object} partialCollection - a partial collection of grouped member keys
|
|
249
258
|
*/
|
|
250
|
-
function keysChanged(collectionKey,
|
|
251
|
-
//
|
|
252
|
-
|
|
259
|
+
function keysChanged(collectionKey, partialCollection) {
|
|
260
|
+
// We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or
|
|
261
|
+
// 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
|
|
262
|
+
// 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().
|
|
263
|
+
const stateMappingKeys = _.keys(callbackToStateMapping);
|
|
264
|
+
for (let i = stateMappingKeys.length; i--;) {
|
|
265
|
+
const subscriber = callbackToStateMapping[stateMappingKeys[i]];
|
|
253
266
|
if (!subscriber) {
|
|
254
|
-
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Skip iteration if we do not have a collection key or a collection member key on this subscriber
|
|
271
|
+
if (!Str.startsWith(subscriber.key, collectionKey)) {
|
|
272
|
+
continue;
|
|
255
273
|
}
|
|
256
274
|
|
|
257
275
|
/**
|
|
258
276
|
* e.g. Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT, callback: ...});
|
|
259
277
|
*/
|
|
260
|
-
const isSubscribedToCollectionKey =
|
|
261
|
-
&& isCollectionKey(subscriber.key);
|
|
278
|
+
const isSubscribedToCollectionKey = subscriber.key === collectionKey;
|
|
262
279
|
|
|
263
280
|
/**
|
|
264
281
|
* e.g. Onyx.connect({key: `${ONYXKEYS.COLLECTION.REPORT}{reportID}`, callback: ...});
|
|
265
282
|
*/
|
|
266
|
-
const isSubscribedToCollectionMemberKey = subscriber.key
|
|
283
|
+
const isSubscribedToCollectionMemberKey = isCollectionMemberKey(collectionKey, subscriber.key);
|
|
267
284
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
285
|
+
// We prepare the "cached collection" which is the entire collection + the new partial data that
|
|
286
|
+
// was merged in via mergeCollection().
|
|
287
|
+
const cachedCollection = getCachedCollection(collectionKey);
|
|
271
288
|
|
|
289
|
+
// Regular Onyx.connect() subscriber found.
|
|
290
|
+
if (_.isFunction(subscriber.callback)) {
|
|
291
|
+
// If they are subscribed to the collection key and using waitForCollectionCallback then we'll
|
|
292
|
+
// send the whole cached collection.
|
|
293
|
+
if (isSubscribedToCollectionKey) {
|
|
272
294
|
if (subscriber.waitForCollectionCallback) {
|
|
273
295
|
subscriber.callback(cachedCollection);
|
|
274
|
-
|
|
296
|
+
continue;
|
|
275
297
|
}
|
|
276
298
|
|
|
277
|
-
|
|
299
|
+
// If they are not using waitForCollectionCallback then we notify the subscriber with
|
|
300
|
+
// the new merged data but only for any keys in the partial collection.
|
|
301
|
+
const dataKeys = _.keys(partialCollection);
|
|
302
|
+
for (let j = 0; j < dataKeys.length; j++) {
|
|
303
|
+
const dataKey = dataKeys[j];
|
|
278
304
|
subscriber.callback(cachedCollection[dataKey], dataKey);
|
|
279
|
-
}
|
|
280
|
-
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// And if the subscriber is specifically only tracking a particular collection member key then we will
|
|
310
|
+
// notify them with the cached data for that key only.
|
|
311
|
+
if (isSubscribedToCollectionMemberKey) {
|
|
312
|
+
subscriber.callback(cachedCollection[subscriber.key], subscriber.key);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// React component subscriber found.
|
|
320
|
+
if (subscriber.withOnyxInstance) {
|
|
321
|
+
// We are subscribed to a collection key so we must update the data in state with the new
|
|
322
|
+
// collection member key values from the partial update.
|
|
323
|
+
if (isSubscribedToCollectionKey) {
|
|
281
324
|
subscriber.withOnyxInstance.setState((prevState) => {
|
|
282
325
|
const finalCollection = _.clone(prevState[subscriber.statePropertyName] || {});
|
|
283
|
-
_.each(collection, (data, dataKey) => {
|
|
284
|
-
if (finalCollection[dataKey]) {
|
|
285
|
-
lodashMerge(finalCollection[dataKey], data);
|
|
286
|
-
} else {
|
|
287
|
-
finalCollection[dataKey] = data;
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
326
|
|
|
327
|
+
const dataKeys = _.keys(partialCollection);
|
|
328
|
+
for (let j = 0; j < dataKeys.length; j++) {
|
|
329
|
+
const dataKey = dataKeys[j];
|
|
330
|
+
finalCollection[dataKey] = cachedCollection[dataKey];
|
|
331
|
+
}
|
|
291
332
|
return {
|
|
292
333
|
[subscriber.statePropertyName]: finalCollection,
|
|
293
334
|
};
|
|
294
335
|
});
|
|
336
|
+
continue;
|
|
295
337
|
}
|
|
296
|
-
} else if (isSubscribedToCollectionMemberKey) {
|
|
297
|
-
const dataFromCollection = collection[subscriber.key];
|
|
298
338
|
|
|
299
|
-
// If
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
339
|
+
// If a React component is only interested in a single key then we can set the cached value directly to the state name.
|
|
340
|
+
if (isSubscribedToCollectionMemberKey) {
|
|
341
|
+
// However, we only want to update this subscriber if the partial data contains a change.
|
|
342
|
+
// Otherwise, we would update them with a value they already have and trigger an unnecessary re-render.
|
|
343
|
+
const dataFromCollection = partialCollection[subscriber.key];
|
|
344
|
+
if (_.isUndefined(dataFromCollection)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
304
347
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
...dataFromCollection,
|
|
310
|
-
}
|
|
311
|
-
: dataFromCollection,
|
|
312
|
-
}));
|
|
348
|
+
subscriber.withOnyxInstance.setState({
|
|
349
|
+
[subscriber.statePropertyName]: cachedCollection[subscriber.key],
|
|
350
|
+
});
|
|
351
|
+
}
|
|
313
352
|
}
|
|
314
|
-
}
|
|
353
|
+
}
|
|
315
354
|
}
|
|
316
355
|
|
|
317
356
|
/**
|
|
@@ -329,43 +368,54 @@ function keyChanged(key, data) {
|
|
|
329
368
|
removeLastAccessedKey(key);
|
|
330
369
|
}
|
|
331
370
|
|
|
332
|
-
//
|
|
333
|
-
|
|
371
|
+
// 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
|
|
372
|
+
// 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
|
|
373
|
+
// 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.
|
|
374
|
+
const stateMappingKeys = _.keys(callbackToStateMapping);
|
|
375
|
+
for (let i = stateMappingKeys.length; i--;) {
|
|
376
|
+
const subscriber = callbackToStateMapping[stateMappingKeys[i]];
|
|
334
377
|
if (!subscriber || !isKeyMatch(subscriber.key, key)) {
|
|
335
|
-
|
|
378
|
+
continue;
|
|
336
379
|
}
|
|
337
380
|
|
|
381
|
+
// Subscriber is a regular call to connect() and provided a callback
|
|
338
382
|
if (_.isFunction(subscriber.callback)) {
|
|
339
|
-
if (subscriber.waitForCollectionCallback) {
|
|
383
|
+
if (isCollectionKey(subscriber.key) && subscriber.waitForCollectionCallback) {
|
|
340
384
|
const cachedCollection = getCachedCollection(subscriber.key);
|
|
341
385
|
cachedCollection[key] = data;
|
|
342
386
|
subscriber.callback(cachedCollection);
|
|
343
|
-
|
|
387
|
+
continue;
|
|
344
388
|
}
|
|
345
389
|
|
|
346
390
|
subscriber.callback(data, key);
|
|
347
|
-
|
|
391
|
+
continue;
|
|
348
392
|
}
|
|
349
393
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
394
|
+
// Subscriber connected via withOnyx() HOC
|
|
395
|
+
if (subscriber.withOnyxInstance) {
|
|
396
|
+
// Check if we are subscribing to a collection key and overwrite the collection member key value in state
|
|
397
|
+
if (isCollectionKey(subscriber.key)) {
|
|
398
|
+
subscriber.withOnyxInstance.setState((prevState) => {
|
|
399
|
+
const collection = prevState[subscriber.statePropertyName] || {};
|
|
400
|
+
return {
|
|
401
|
+
[subscriber.statePropertyName]: {
|
|
402
|
+
...collection,
|
|
403
|
+
[key]: data,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
353
409
|
|
|
354
|
-
|
|
355
|
-
if (isCollectionKey(subscriber.key)) {
|
|
356
|
-
subscriber.withOnyxInstance.setState((prevState) => {
|
|
357
|
-
const collection = _.clone(prevState[subscriber.statePropertyName] || {});
|
|
358
|
-
collection[key] = data;
|
|
359
|
-
return {
|
|
360
|
-
[subscriber.statePropertyName]: collection,
|
|
361
|
-
};
|
|
362
|
-
});
|
|
363
|
-
} else {
|
|
410
|
+
// If we did not match on a collection key then we just set the new data to the state property
|
|
364
411
|
subscriber.withOnyxInstance.setState({
|
|
365
412
|
[subscriber.statePropertyName]: data,
|
|
366
413
|
});
|
|
414
|
+
continue;
|
|
367
415
|
}
|
|
368
|
-
|
|
416
|
+
|
|
417
|
+
console.error('Warning: Found a matching subscriber to a key that changed, but no callback or withOnyxInstance could be found.');
|
|
418
|
+
}
|
|
369
419
|
}
|
|
370
420
|
|
|
371
421
|
/**
|
|
@@ -379,9 +429,9 @@ function keyChanged(key, data) {
|
|
|
379
429
|
* @param {string} [config.statePropertyName]
|
|
380
430
|
* @param {function} [config.callback]
|
|
381
431
|
* @param {*|null} val
|
|
382
|
-
* @param {String}
|
|
432
|
+
* @param {String} matchedKey
|
|
383
433
|
*/
|
|
384
|
-
function sendDataToConnection(config, val,
|
|
434
|
+
function sendDataToConnection(config, val, matchedKey) {
|
|
385
435
|
// If the mapping no longer exists then we should not send any data.
|
|
386
436
|
// This means our subscriber disconnected or withOnyx wrapped component unmounted.
|
|
387
437
|
if (!callbackToStateMapping[config.connectionID]) {
|
|
@@ -391,10 +441,54 @@ function sendDataToConnection(config, val, key) {
|
|
|
391
441
|
if (config.withOnyxInstance) {
|
|
392
442
|
config.withOnyxInstance.setWithOnyxState(config.statePropertyName, val);
|
|
393
443
|
} else if (_.isFunction(config.callback)) {
|
|
394
|
-
config.callback(val,
|
|
444
|
+
config.callback(val, matchedKey);
|
|
395
445
|
}
|
|
396
446
|
}
|
|
397
447
|
|
|
448
|
+
/**
|
|
449
|
+
* We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we
|
|
450
|
+
* run out of storage the least recently accessed key can be removed.
|
|
451
|
+
*
|
|
452
|
+
* @private
|
|
453
|
+
* @param {Object} mapping
|
|
454
|
+
*/
|
|
455
|
+
function addKeyToRecentlyAccessedIfNeeded(mapping) {
|
|
456
|
+
if (!isSafeEvictionKey(mapping.key)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Try to free some cache whenever we connect to a safe eviction key
|
|
461
|
+
cache.removeLeastRecentlyUsedKeys();
|
|
462
|
+
|
|
463
|
+
if (mapping.withOnyxInstance && !isCollectionKey(mapping.key)) {
|
|
464
|
+
// All React components subscribing to a key flagged as a safe eviction key must implement the canEvict property.
|
|
465
|
+
if (_.isUndefined(mapping.canEvict)) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
addLastAccessedKey(mapping.key);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.
|
|
477
|
+
*
|
|
478
|
+
* @private
|
|
479
|
+
* @param {Array} matchingKeys
|
|
480
|
+
* @param {Object} mapping
|
|
481
|
+
*/
|
|
482
|
+
function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
|
|
483
|
+
Promise.all(_.map(matchingKeys, key => get(key)))
|
|
484
|
+
.then(values => _.reduce(values, (finalObject, value, i) => {
|
|
485
|
+
// eslint-disable-next-line no-param-reassign
|
|
486
|
+
finalObject[matchingKeys[i]] = value;
|
|
487
|
+
return finalObject;
|
|
488
|
+
}, {}))
|
|
489
|
+
.then(val => sendDataToConnection(mapping, val));
|
|
490
|
+
}
|
|
491
|
+
|
|
398
492
|
/**
|
|
399
493
|
* Subscribes a react component's state directly to a store key
|
|
400
494
|
*
|
|
@@ -427,58 +521,63 @@ function connect(mapping) {
|
|
|
427
521
|
|
|
428
522
|
// Commit connection only after init passes
|
|
429
523
|
deferredInitTask.promise
|
|
430
|
-
.then(() =>
|
|
431
|
-
|
|
432
|
-
|
|
524
|
+
.then(() => addKeyToRecentlyAccessedIfNeeded(mapping))
|
|
525
|
+
.then(getAllKeys)
|
|
526
|
+
.then((keys) => {
|
|
527
|
+
// We search all the keys in storage to see if any are a "match" for the subscriber we are connecting so that we
|
|
528
|
+
// can send data back to the subscriber. Note that multiple keys can match as a subscriber could either be
|
|
529
|
+
// subscribed to a "collection key" or a single key.
|
|
530
|
+
const matchingKeys = _.filter(keys, key => isKeyMatch(mapping.key, key));
|
|
531
|
+
|
|
532
|
+
// If the key being connected to does not exist we initialize the value with null. For subscribers that connected
|
|
533
|
+
// directly via connect() they will simply get a null value sent to them without any information about which key matched
|
|
534
|
+
// since there are none matched. In withOnyx() we wait for all connected keys to return a value before rendering the child
|
|
535
|
+
// component. This null value will be filtered out so that the connected component can utilize defaultProps.
|
|
536
|
+
if (matchingKeys.length === 0) {
|
|
537
|
+
sendDataToConnection(mapping, null);
|
|
433
538
|
return;
|
|
434
539
|
}
|
|
435
540
|
|
|
436
|
-
//
|
|
437
|
-
|
|
541
|
+
// When using a callback subscriber we will either trigger the provided callback for each key we find or combine all values
|
|
542
|
+
// into an object and just make a single call. The latter behavior is enabled by providing a waitForCollectionCallback key
|
|
543
|
+
// combined with a subscription to a collection key.
|
|
544
|
+
if (_.isFunction(mapping.callback)) {
|
|
545
|
+
if (isCollectionKey(mapping.key)) {
|
|
546
|
+
if (mapping.waitForCollectionCallback) {
|
|
547
|
+
getCollectionDataAndSendAsObject(matchingKeys, mapping);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
438
550
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
`Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`,
|
|
445
|
-
);
|
|
551
|
+
// We did not opt into using waitForCollectionCallback mode so the callback is called for every matching key.
|
|
552
|
+
for (let i = 0; i < matchingKeys.length; i++) {
|
|
553
|
+
get(matchingKeys[i]).then(val => sendDataToConnection(mapping, val, matchingKeys[i]));
|
|
554
|
+
}
|
|
555
|
+
return;
|
|
446
556
|
}
|
|
447
557
|
|
|
448
|
-
|
|
558
|
+
// If we are not subscribed to a collection key then there's only a single key to send an update for.
|
|
559
|
+
get(mapping.key).then(val => sendDataToConnection(mapping, val, mapping.key));
|
|
560
|
+
return;
|
|
449
561
|
}
|
|
450
|
-
})
|
|
451
|
-
.then(getAllKeys)
|
|
452
|
-
.then((keys) => {
|
|
453
|
-
// Find all the keys matched by the config key
|
|
454
|
-
const matchingKeys = _.filter(keys, key => isKeyMatch(mapping.key, key));
|
|
455
562
|
|
|
456
|
-
// If
|
|
457
|
-
|
|
458
|
-
|
|
563
|
+
// If we have a withOnyxInstance that means a React component has subscribed via the withOnyx() HOC and we need to
|
|
564
|
+
// group collection key member data into an object.
|
|
565
|
+
if (mapping.withOnyxInstance) {
|
|
566
|
+
if (isCollectionKey(mapping.key)) {
|
|
567
|
+
getCollectionDataAndSendAsObject(matchingKeys, mapping);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// If the subscriber is not using a collection key then we just send a single value back to the subscriber
|
|
572
|
+
get(mapping.key).then(val => sendDataToConnection(mapping, val, mapping.key));
|
|
459
573
|
return;
|
|
460
574
|
}
|
|
461
575
|
|
|
462
|
-
|
|
463
|
-
// for each key we find. It's up to the subscriber to know whether
|
|
464
|
-
// to expect a single key or multiple keys in the case of a collection.
|
|
465
|
-
// React components are an exception since we'll want to send their
|
|
466
|
-
// initial data as a single object when using collection keys.
|
|
467
|
-
if ((mapping.withOnyxInstance && isCollectionKey(mapping.key)) || mapping.waitForCollectionCallback) {
|
|
468
|
-
Promise.all(_.map(matchingKeys, key => get(key)))
|
|
469
|
-
.then(values => _.reduce(values, (finalObject, value, i) => {
|
|
470
|
-
// eslint-disable-next-line no-param-reassign
|
|
471
|
-
finalObject[matchingKeys[i]] = value;
|
|
472
|
-
return finalObject;
|
|
473
|
-
}, {}))
|
|
474
|
-
.then(val => sendDataToConnection(mapping, val));
|
|
475
|
-
} else {
|
|
476
|
-
_.each(matchingKeys, (key) => {
|
|
477
|
-
get(key).then(val => sendDataToConnection(mapping, val, key));
|
|
478
|
-
});
|
|
479
|
-
}
|
|
576
|
+
console.error('Warning: Onyx.connect() was found without a callback or withOnyxInstance');
|
|
480
577
|
});
|
|
481
578
|
|
|
579
|
+
// The connectionID is returned back to the caller so that it can be used to clean up the connection when it's no longer needed
|
|
580
|
+
// by calling Onyx.disconnect(connectionID).
|
|
482
581
|
return connectionID;
|
|
483
582
|
}
|
|
484
583
|
|
|
@@ -662,7 +761,9 @@ function applyMerge(key, data) {
|
|
|
662
761
|
if (_.isObject(data) || _.every(mergeValues, _.isObject)) {
|
|
663
762
|
// Object values are merged one after the other
|
|
664
763
|
return _.reduce(mergeValues, (modifiedData, mergeValue) => {
|
|
665
|
-
|
|
764
|
+
// lodash adds a small overhead so we don't use it here
|
|
765
|
+
// eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
|
|
766
|
+
const newData = Object.assign({}, fastMerge(modifiedData, mergeValue));
|
|
666
767
|
|
|
667
768
|
// We will also delete any object keys that are undefined or null.
|
|
668
769
|
// Deleting keys is not supported by AsyncStorage so we do it this way.
|
|
@@ -733,7 +834,7 @@ function initializeWithDefaultKeyStates() {
|
|
|
733
834
|
.then((pairs) => {
|
|
734
835
|
const asObject = _.object(pairs);
|
|
735
836
|
|
|
736
|
-
const merged =
|
|
837
|
+
const merged = fastMerge(asObject, defaultKeyStates);
|
|
737
838
|
cache.merge(merged);
|
|
738
839
|
_.each(merged, (val, key) => keyChanged(key, val));
|
|
739
840
|
});
|
|
@@ -788,7 +889,7 @@ function clear() {
|
|
|
788
889
|
*/
|
|
789
890
|
function mergeCollection(collectionKey, collection) {
|
|
790
891
|
// Confirm all the collection keys belong to the same parent
|
|
791
|
-
_.each(collection, (
|
|
892
|
+
_.each(collection, (_data, dataKey) => {
|
|
792
893
|
if (isKeyMatch(collectionKey, dataKey)) {
|
|
793
894
|
return;
|
|
794
895
|
}
|
package/lib/OnyxCache.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import _ from 'underscore';
|
|
2
|
-
import
|
|
2
|
+
import fastMerge from './fastMerge';
|
|
3
3
|
|
|
4
4
|
const isDefined = _.negate(_.isUndefined);
|
|
5
5
|
|
|
@@ -110,7 +110,9 @@ class OnyxCache {
|
|
|
110
110
|
* @param {Record<string, *>} data - a map of (cache) key - values
|
|
111
111
|
*/
|
|
112
112
|
merge(data) {
|
|
113
|
-
|
|
113
|
+
// lodash adds a small overhead so we don't use it here
|
|
114
|
+
// eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
|
|
115
|
+
this.storageMap = Object.assign({}, fastMerge(this.storageMap, data));
|
|
114
116
|
|
|
115
117
|
const storageKeys = this.getAllKeys();
|
|
116
118
|
const mergedKeys = _.keys(data);
|
package/lib/fastMerge.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Mostly copied from https://medium.com/@lubaka.a/how-to-remove-lodash-performance-improvement-b306669ad0e1
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {mixed} val
|
|
5
|
+
* @returns {boolean}
|
|
6
|
+
*/
|
|
7
|
+
function isMergeableObject(val) {
|
|
8
|
+
const nonNullObject = val != null ? typeof val === 'object' : false;
|
|
9
|
+
return (nonNullObject
|
|
10
|
+
&& Object.prototype.toString.call(val) !== '[object RegExp]'
|
|
11
|
+
&& Object.prototype.toString.call(val) !== '[object Date]');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} target
|
|
16
|
+
* @param {Object} source
|
|
17
|
+
* @returns {Object}
|
|
18
|
+
*/
|
|
19
|
+
function mergeObject(target, source) {
|
|
20
|
+
const destination = {};
|
|
21
|
+
if (isMergeableObject(target)) {
|
|
22
|
+
// lodash adds a small overhead so we don't use it here
|
|
23
|
+
// eslint-disable-next-line rulesdir/prefer-underscore-method
|
|
24
|
+
const targetKeys = Object.keys(target);
|
|
25
|
+
for (let i = 0; i < targetKeys.length; ++i) {
|
|
26
|
+
const key = targetKeys[i];
|
|
27
|
+
destination[key] = target[key];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// lodash adds a small overhead so we don't use it here
|
|
32
|
+
// eslint-disable-next-line rulesdir/prefer-underscore-method
|
|
33
|
+
const sourceKeys = Object.keys(source);
|
|
34
|
+
for (let i = 0; i < sourceKeys.length; ++i) {
|
|
35
|
+
const key = sourceKeys[i];
|
|
36
|
+
if (source[key] === undefined) {
|
|
37
|
+
// eslint-disable-next-line no-continue
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!isMergeableObject(source[key]) || !target[key]) {
|
|
41
|
+
destination[key] = source[key];
|
|
42
|
+
} else {
|
|
43
|
+
// eslint-disable-next-line no-use-before-define
|
|
44
|
+
destination[key] = fastMerge(target[key], source[key]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return destination;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {Object|Array} target
|
|
53
|
+
* @param {Object|Array} source
|
|
54
|
+
* @returns {Object|Array}
|
|
55
|
+
*/
|
|
56
|
+
function fastMerge(target, source) {
|
|
57
|
+
// lodash adds a small overhead so we don't use it here
|
|
58
|
+
// eslint-disable-next-line rulesdir/prefer-underscore-method
|
|
59
|
+
const array = Array.isArray(source);
|
|
60
|
+
if (array) {
|
|
61
|
+
return source;
|
|
62
|
+
}
|
|
63
|
+
return mergeObject(target, source);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default fastMerge;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import localforage from 'localforage';
|
|
8
8
|
import _ from 'underscore';
|
|
9
9
|
import SyncQueue from '../../SyncQueue';
|
|
10
|
-
import
|
|
10
|
+
import fastMerge from '../../fastMerge';
|
|
11
11
|
|
|
12
12
|
localforage.config({
|
|
13
13
|
name: 'OnyxDB',
|
|
@@ -24,7 +24,10 @@ const provider = {
|
|
|
24
24
|
return localforage.getItem(key)
|
|
25
25
|
.then((existingValue) => {
|
|
26
26
|
const newValue = _.isObject(existingValue)
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
// lodash adds a small overhead so we don't use it here
|
|
29
|
+
// eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
|
|
30
|
+
? Object.assign({}, fastMerge(existingValue, value))
|
|
28
31
|
: value;
|
|
29
32
|
return localforage.setItem(key, newValue);
|
|
30
33
|
});
|
package/package.json
CHANGED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import lodashMergeWith from 'lodash/mergeWith';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* When merging 2 objects into onyx that contain an array, we want to completely replace the array instead of the default
|
|
5
|
-
* behavior which is to merge each item by its index.
|
|
6
|
-
* ie:
|
|
7
|
-
* merge({a: [1]}, {a: [2,3]}):
|
|
8
|
-
* - In the default implementation would produce {a:[1,3]}
|
|
9
|
-
* - With this function would produce: {a: [2,3]}
|
|
10
|
-
* @param {*} objValue
|
|
11
|
-
* @param {*} srcValue
|
|
12
|
-
* @return {*}
|
|
13
|
-
*/
|
|
14
|
-
// eslint-disable-next-line rulesdir/prefer-early-return
|
|
15
|
-
function customizerForMergeWith(objValue, srcValue) {
|
|
16
|
-
// eslint-disable-next-line rulesdir/prefer-underscore-method
|
|
17
|
-
if (Array.isArray(objValue)) {
|
|
18
|
-
return srcValue;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function mergeWithCustomized(...args) {
|
|
23
|
-
return lodashMergeWith(...args, customizerForMergeWith);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default mergeWithCustomized;
|