@@ -3,6 +3,7 @@ import AsyncStorage from '@react-native-community/async-storage';
3
3
import Str from 'expensify-common/lib/str' ;
4
4
import lodashMerge from 'lodash/merge' ;
5
5
import { registerLogger , logInfo , logAlert } from './Logger' ;
6
+ import cache from './OnyxCache' ;
6
7
7
8
// Keeps track of the last connectionID that was used so we can keep incrementing it
8
9
let lastConnectionID = 0 ;
@@ -34,17 +35,56 @@ let defaultKeyStates = {};
34
35
* @returns {Promise<*> }
35
36
*/
36
37
function get ( key ) {
37
- return AsyncStorage . getItem ( key )
38
- . then ( val => JSON . parse ( val ) )
38
+ // When we already have the value in cache - resolve right away
39
+ if ( cache . hasCacheForKey ( key ) ) {
40
+ return Promise . resolve ( cache . getValue ( key ) ) ;
41
+ }
42
+
43
+ const taskName = `get:${ key } ` ;
44
+
45
+ // When a value retrieving task for this key is still running hook to it
46
+ if ( cache . hasPendingTask ( taskName ) ) {
47
+ return cache . getTaskPromise ( taskName ) ;
48
+ }
49
+
50
+ // Otherwise retrieve the value from storage and capture a promise to aid concurrent usages
51
+ const promise = AsyncStorage . getItem ( key )
52
+ . then ( ( val ) => {
53
+ const parsed = val && JSON . parse ( val ) ;
54
+ cache . set ( key , parsed ) ;
55
+ return parsed ;
56
+ } )
39
57
. catch ( err => logInfo ( `Unable to get item from persistent storage. Key: ${ key } Error: ${ err } ` ) ) ;
58
+
59
+ return cache . captureTask ( taskName , promise ) ;
40
60
}
41
61
42
62
/**
43
63
* Returns current key names stored in persisted storage
44
64
* @returns {Promise<string[]> }
45
65
*/
46
66
function getAllKeys ( ) {
47
- return AsyncStorage . getAllKeys ( ) ;
67
+ // When we've already read stored keys, resolve right away
68
+ const storedKeys = cache . getAllKeys ( ) ;
69
+ if ( storedKeys . length > 0 ) {
70
+ return Promise . resolve ( storedKeys ) ;
71
+ }
72
+
73
+ const taskName = 'getAllKeys' ;
74
+
75
+ // When a value retrieving task for all keys is still running hook to it
76
+ if ( cache . hasPendingTask ( taskName ) ) {
77
+ return cache . getTaskPromise ( taskName ) ;
78
+ }
79
+
80
+ // Otherwise retrieve the keys from storage and capture a promise to aid concurrent usages
81
+ const promise = AsyncStorage . getAllKeys ( )
82
+ . then ( ( keys ) => {
83
+ _ . each ( keys , key => cache . addKey ( key ) ) ;
84
+ return keys ;
85
+ } ) ;
86
+
87
+ return cache . captureTask ( taskName , promise ) ;
48
88
}
49
89
50
90
/**
@@ -58,6 +98,19 @@ function isCollectionKey(key) {
58
98
return _ . contains ( _ . values ( onyxKeys . COLLECTION ) , key ) ;
59
99
}
60
100
101
+ /**
102
+ * Find the collection a collection item belongs to
103
+ * or return null if them item is not a part of a collection
104
+ * @param {string } key
105
+ * @returns {string|null }
106
+ */
107
+ function getCollectionKeyForItem ( key ) {
108
+ return _ . chain ( onyxKeys . COLLECTION )
109
+ . values ( )
110
+ . find ( name => key . startsWith ( name ) )
111
+ . value ( ) ;
112
+ }
113
+
61
114
/**
62
115
* Checks to see if a given key matches with the
63
116
* configured key of our connected subscriber
@@ -350,6 +403,43 @@ function connect(mapping) {
350
403
return connectionID ;
351
404
}
352
405
406
+ /**
407
+ * Remove cache items that are no longer connected through Onyx
408
+ * @param {string } key
409
+ */
410
+ function cleanCache ( key ) {
411
+ const hasRemainingConnections = _ . some ( callbackToStateMapping , { key} ) ;
412
+
413
+ // When the key is still used in other places don't remove it from cache
414
+ if ( hasRemainingConnections ) {
415
+ return ;
416
+ }
417
+
418
+ // When this is a collection - also recursively remove any unused individual items
419
+ if ( isCollectionKey ( key ) ) {
420
+ cache . remove ( key ) ;
421
+
422
+ getAllKeys ( ) . then ( cachedKeys => _ . chain ( cachedKeys )
423
+ . filter ( name => name . startsWith ( key ) )
424
+ . forEach ( cleanCache ) ) ;
425
+
426
+ return ;
427
+ }
428
+
429
+ // When this is a collection item - check if the collection is still used
430
+ const collectionKey = getCollectionKeyForItem ( key ) ;
431
+ if ( collectionKey ) {
432
+ // When there's an active subscription for a collection don't remove the item
433
+ const hasRemainingConnectionsForCollection = _ . some ( callbackToStateMapping , { key : collectionKey } ) ;
434
+ if ( hasRemainingConnectionsForCollection ) {
435
+ return ;
436
+ }
437
+ }
438
+
439
+ // Otherwise remove the item from cache
440
+ cache . remove ( key ) ;
441
+ }
442
+
353
443
/**
354
444
* Remove the listener for a react component
355
445
*
@@ -367,7 +457,11 @@ function disconnect(connectionID, keyToRemoveFromEvictionBlocklist) {
367
457
removeFromEvictionBlockList ( keyToRemoveFromEvictionBlocklist , connectionID ) ;
368
458
}
369
459
460
+ const key = callbackToStateMapping [ connectionID ] . key ;
370
461
delete callbackToStateMapping [ connectionID ] ;
462
+
463
+ // When the last subscriber disconnects, drop cache as well
464
+ cleanCache ( key ) ;
371
465
}
372
466
373
467
/**
@@ -377,8 +471,13 @@ function disconnect(connectionID, keyToRemoveFromEvictionBlocklist) {
377
471
* @return {Promise }
378
472
*/
379
473
function remove ( key ) {
380
- return AsyncStorage . removeItem ( key )
381
- . then ( ( ) => keyChanged ( key , null ) ) ;
474
+ // Remove from cache
475
+ cache . remove ( key ) ;
476
+
477
+ // Optimistically inform subscribers
478
+ keyChanged ( key , null ) ;
479
+
480
+ return AsyncStorage . removeItem ( key ) ;
382
481
}
383
482
384
483
/**
@@ -414,12 +513,28 @@ function evictStorageAndRetry(error, ionMethod, ...args) {
414
513
* @returns {Promise }
415
514
*/
416
515
function set ( key , val ) {
516
+ // Adds the key to cache when it's not available
517
+ cache . set ( key , val ) ;
518
+
519
+ // Optimistically inform subscribers
520
+ keyChanged ( key , val ) ;
521
+
417
522
// Write the thing to persistent storage, which will trigger a storage event for any other tabs open on this domain
418
523
return AsyncStorage . setItem ( key , JSON . stringify ( val ) )
419
- . then ( ( ) => keyChanged ( key , val ) )
420
524
. catch ( error => evictStorageAndRetry ( error , set , key , val ) ) ;
421
525
}
422
526
527
+ /**
528
+ * AsyncStorage expects array like: [["@MyApp_user", "value_1"], ["@MyApp_key", "value_2"]]
529
+ * This method transforms an object like {'@MyApp_user ': 'myUserValue', '@MyApp_key': 'myKeyValue'}
530
+ * to an array of key-value pairs in the above format
531
+ * @param {Record<string, *> } data
532
+ * @return {Array<[string, string]> } an array of stringified key - value pairs
533
+ */
534
+ function prepareKeyValuePairsForStorage ( data ) {
535
+ return _ . keys ( data ) . map ( key => [ key , JSON . stringify ( data [ key ] ) ] ) ;
536
+ }
537
+
423
538
/**
424
539
* Sets multiple keys and values. Example
425
540
* Onyx.multiSet({'key1': 'a', 'key2': 'b'});
@@ -428,17 +543,15 @@ function set(key, val) {
428
543
* @returns {Promise }
429
544
*/
430
545
function multiSet ( data ) {
431
- // AsyncStorage expenses the data in an array like:
432
- // [["@MyApp_user", "value_1"], ["@MyApp_key", "value_2"]]
433
- // This method will transform the params from a better JSON format like:
434
- // {'@MyApp_user': 'myUserValue', '@MyApp_key': 'myKeyValue' }
435
- const keyValuePairs = _ . reduce ( data , ( finalArray , val , key ) => ( [
436
- ...finalArray ,
437
- [ key , JSON . stringify ( val ) ] ,
438
- ] ) , [ ] ) ;
546
+ const keyValuePairs = prepareKeyValuePairsForStorage ( data ) ;
547
+
548
+ _ . each ( data , ( val , key ) => {
549
+ // Update cache and optimistically inform subscribers
550
+ cache . set ( key , val ) ;
551
+ keyChanged ( key , val ) ;
552
+ } ) ;
439
553
440
554
return AsyncStorage . multiSet ( keyValuePairs )
441
- . then ( ( ) => _ . each ( data , ( val , key ) => keyChanged ( key , val ) ) )
442
555
. catch ( error => evictStorageAndRetry ( error , multiSet , data ) ) ;
443
556
}
444
557
@@ -522,17 +635,15 @@ function initializeWithDefaultKeyStates() {
522
635
* @returns {Promise<void> }
523
636
*/
524
637
function clear ( ) {
525
- let allKeys ;
526
638
return getAllKeys ( )
527
- . then ( keys => allKeys = keys )
528
- . then ( ( ) => AsyncStorage . clear ( ) )
529
- . then ( ( ) => {
530
- _ . each ( allKeys , ( key ) => {
639
+ . then ( ( keys ) => {
640
+ _ . each ( keys , ( key ) => {
531
641
keyChanged ( key , null ) ;
642
+ cache . remove ( key ) ;
532
643
} ) ;
533
-
534
- initializeWithDefaultKeyStates ( ) ;
535
- } ) ;
644
+ } )
645
+ . then ( AsyncStorage . clear )
646
+ . then ( initializeWithDefaultKeyStates ) ;
536
647
}
537
648
538
649
/**
@@ -551,34 +662,31 @@ function mergeCollection(collectionKey, collection) {
551
662
}
552
663
} ) ;
553
664
554
- const existingKeyCollection = { } ;
555
- const newCollection = { } ;
556
665
return getAllKeys ( )
557
- . then ( ( keys ) => {
558
- _ . each ( collection , ( data , dataKey ) => {
559
- if ( keys . includes ( dataKey ) ) {
560
- existingKeyCollection [ dataKey ] = data ;
561
- } else {
562
- newCollection [ dataKey ] = data ;
563
- }
564
- } ) ;
565
-
566
- const keyValuePairsForExistingCollection = _ . reduce ( existingKeyCollection , ( finalArray , val , key ) => ( [
567
- ...finalArray ,
568
- [ key , JSON . stringify ( val ) ] ,
569
- ] ) , [ ] ) ;
570
- const keyValuePairsForNewCollection = _ . reduce ( newCollection , ( finalArray , val , key ) => ( [
571
- ...finalArray ,
572
- [ key , JSON . stringify ( val ) ] ,
573
- ] ) , [ ] ) ;
666
+ . then ( ( persistedKeys ) => {
667
+ // Split to keys that exist in storage and keys that don't
668
+ const [ existingKeys , newKeys ] = _ . chain ( collection )
669
+ . keys ( )
670
+ . partition ( key => persistedKeys . includes ( key ) )
671
+ . value ( ) ;
672
+
673
+ const existingKeyCollection = _ . pick ( collection , existingKeys ) ;
674
+ const newCollection = _ . pick ( collection , newKeys ) ;
675
+ const keyValuePairsForExistingCollection = prepareKeyValuePairsForStorage ( existingKeyCollection ) ;
676
+ const keyValuePairsForNewCollection = prepareKeyValuePairsForStorage ( newCollection ) ;
574
677
575
678
// New keys will be added via multiSet while existing keys will be updated using multiMerge
576
679
// This is because setting a key that doesn't exist yet with multiMerge will throw errors
577
680
const existingCollectionPromise = AsyncStorage . multiMerge ( keyValuePairsForExistingCollection ) ;
578
681
const newCollectionPromise = AsyncStorage . multiSet ( keyValuePairsForNewCollection ) ;
579
682
683
+ // Merge original data to cache
684
+ cache . merge ( collection ) ;
685
+
686
+ // Optimistically inform collection subscribers
687
+ keysChanged ( collectionKey , collection ) ;
688
+
580
689
return Promise . all ( [ existingCollectionPromise , newCollectionPromise ] )
581
- . then ( ( ) => keysChanged ( collectionKey , collection ) )
582
690
. catch ( error => evictStorageAndRetry ( error , mergeCollection , collection ) ) ;
583
691
} ) ;
584
692
}
0 commit comments