react-native-onyx 1.0.54 → 1.0.56
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/dist/web.development.js +128 -34
- 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 +92 -26
- package/lib/storage/__mocks__/index.js +1 -2
- package/lib/withOnyx.js +35 -7
- package/package.json +1 -1
package/lib/Onyx.js
CHANGED
|
@@ -184,6 +184,46 @@ function isSafeEvictionKey(testKey) {
|
|
|
184
184
|
return _.some(evictionAllowList, key => isKeyMatch(key, testKey));
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined.
|
|
189
|
+
* If the requested key is a collection, it will return an object with all the collection members.
|
|
190
|
+
*
|
|
191
|
+
* @param {String} key
|
|
192
|
+
* @param {Object} mapping
|
|
193
|
+
* @returns {Mixed}
|
|
194
|
+
*/
|
|
195
|
+
function tryGetCachedValue(key, mapping = {}) {
|
|
196
|
+
let val = cache.getValue(key);
|
|
197
|
+
|
|
198
|
+
if (isCollectionKey(key)) {
|
|
199
|
+
const allKeys = cache.getAllKeys();
|
|
200
|
+
const matchingKeys = _.filter(allKeys, k => k.startsWith(key));
|
|
201
|
+
const values = _.reduce(matchingKeys, (finalObject, matchedKey) => {
|
|
202
|
+
const cachedValue = cache.getValue(matchedKey);
|
|
203
|
+
if (cachedValue) {
|
|
204
|
+
// This is permissible because we're in the process of constructing the final object in a reduce function.
|
|
205
|
+
// eslint-disable-next-line no-param-reassign
|
|
206
|
+
finalObject[matchedKey] = cachedValue;
|
|
207
|
+
}
|
|
208
|
+
return finalObject;
|
|
209
|
+
}, {});
|
|
210
|
+
if (_.isEmpty(values)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
val = values;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (mapping.selector) {
|
|
217
|
+
const state = mapping.withOnyxInstance ? mapping.withOnyxInstance.state : undefined;
|
|
218
|
+
if (isCollectionKey(key)) {
|
|
219
|
+
return reduceCollectionWithSelector(val, mapping.selector, state);
|
|
220
|
+
}
|
|
221
|
+
return getSubsetOfData(val, mapping.selector, state);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return val;
|
|
225
|
+
}
|
|
226
|
+
|
|
187
227
|
/**
|
|
188
228
|
* Remove a key from the recently accessed key list.
|
|
189
229
|
*
|
|
@@ -848,19 +888,33 @@ function evictStorageAndRetry(error, onyxMethod, ...args) {
|
|
|
848
888
|
*
|
|
849
889
|
* @param {String} key
|
|
850
890
|
* @param {*} value
|
|
891
|
+
* @param {Boolean} hasChanged
|
|
851
892
|
* @param {String} method
|
|
852
893
|
*/
|
|
853
|
-
function broadcastUpdate(key, value, method) {
|
|
894
|
+
function broadcastUpdate(key, value, hasChanged, method) {
|
|
854
895
|
// Logging properties only since values could be sensitive things we don't want to log
|
|
855
896
|
Logger.logInfo(`${method}() called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''}`);
|
|
856
897
|
|
|
857
898
|
// Update subscribers if the cached value has changed, or when the subscriber specifically requires
|
|
858
899
|
// all updates regardless of value changes (indicated by initWithStoredValues set to false).
|
|
859
|
-
|
|
860
|
-
|
|
900
|
+
if (hasChanged) {
|
|
901
|
+
cache.set(key, value);
|
|
902
|
+
} else {
|
|
903
|
+
cache.addToAccessedKeys(key);
|
|
904
|
+
}
|
|
905
|
+
|
|
861
906
|
notifySubscribersOnNextTick(key, value, subscriber => hasChanged || subscriber.initWithStoredValues === false);
|
|
862
907
|
}
|
|
863
908
|
|
|
909
|
+
/**
|
|
910
|
+
* @private
|
|
911
|
+
* @param {String} key
|
|
912
|
+
* @returns {Boolean}
|
|
913
|
+
*/
|
|
914
|
+
function hasPendingMergeForKey(key) {
|
|
915
|
+
return Boolean(mergeQueue[key]);
|
|
916
|
+
}
|
|
917
|
+
|
|
864
918
|
/**
|
|
865
919
|
* Write a value to our store with the given key
|
|
866
920
|
*
|
|
@@ -874,8 +928,19 @@ function set(key, value) {
|
|
|
874
928
|
return remove(key);
|
|
875
929
|
}
|
|
876
930
|
|
|
931
|
+
if (hasPendingMergeForKey(key)) {
|
|
932
|
+
Logger.logAlert(`Onyx.set() called after Onyx.merge() for key: ${key}. It is recommended to use set() or merge() not both.`);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const hasChanged = cache.hasValueChanged(key, value);
|
|
936
|
+
|
|
877
937
|
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
878
|
-
broadcastUpdate(key, value, 'set');
|
|
938
|
+
broadcastUpdate(key, value, hasChanged, 'set');
|
|
939
|
+
|
|
940
|
+
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
|
|
941
|
+
if (!hasChanged) {
|
|
942
|
+
return Promise.resolve();
|
|
943
|
+
}
|
|
879
944
|
|
|
880
945
|
return Storage.setItem(key, value)
|
|
881
946
|
.catch(error => evictStorageAndRetry(error, set, key, value));
|
|
@@ -918,11 +983,11 @@ function multiSet(data) {
|
|
|
918
983
|
* Merges an array of changes with an existing value
|
|
919
984
|
*
|
|
920
985
|
* @private
|
|
921
|
-
* @param {Array<*>} changes Array of changes that should be applied to the existing value
|
|
922
986
|
* @param {*} existingValue
|
|
987
|
+
* @param {Array<*>} changes Array of changes that should be applied to the existing value
|
|
923
988
|
* @returns {*}
|
|
924
989
|
*/
|
|
925
|
-
function applyMerge(
|
|
990
|
+
function applyMerge(existingValue, changes) {
|
|
926
991
|
const lastChange = _.last(changes);
|
|
927
992
|
|
|
928
993
|
if (_.isArray(existingValue) || _.isArray(lastChange)) {
|
|
@@ -931,14 +996,10 @@ function applyMerge(changes, existingValue) {
|
|
|
931
996
|
|
|
932
997
|
if (_.isObject(existingValue) || _.every(changes, _.isObject)) {
|
|
933
998
|
// Object values are merged one after the other
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
// Remove all first level keys that are explicitly set to null.
|
|
940
|
-
return _.omit(newData, value => _.isNull(value));
|
|
941
|
-
}, existingValue || {});
|
|
999
|
+
// lodash adds a small overhead so we don't use it here
|
|
1000
|
+
// eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
|
|
1001
|
+
return _.reduce(changes, (modifiedData, change) => fastMerge(modifiedData, change),
|
|
1002
|
+
existingValue || {});
|
|
942
1003
|
}
|
|
943
1004
|
|
|
944
1005
|
// If we have anything else we can't merge it so we'll
|
|
@@ -979,17 +1040,30 @@ function merge(key, changes) {
|
|
|
979
1040
|
.then((existingValue) => {
|
|
980
1041
|
try {
|
|
981
1042
|
// 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)
|
|
982
|
-
const batchedChanges = applyMerge(mergeQueue[key]);
|
|
1043
|
+
const batchedChanges = applyMerge(undefined, mergeQueue[key]);
|
|
983
1044
|
|
|
984
1045
|
// Clean up the write queue so we
|
|
985
1046
|
// don't apply these changes again
|
|
986
1047
|
delete mergeQueue[key];
|
|
987
1048
|
|
|
988
1049
|
// After that we merge the batched changes with the existing value
|
|
989
|
-
|
|
1050
|
+
let modifiedData = applyMerge(existingValue, [batchedChanges]);
|
|
1051
|
+
|
|
1052
|
+
// For objects, the key for null values needs to be removed from the object to ensure the value will get removed from storage completely.
|
|
1053
|
+
// On native, SQLite will remove top-level keys that are null. To be consistent, we remove them on web too.
|
|
1054
|
+
if (!_.isArray(modifiedData) && _.isObject(modifiedData)) {
|
|
1055
|
+
modifiedData = _.omit(modifiedData, value => _.isNull(value));
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const hasChanged = cache.hasValueChanged(key, modifiedData);
|
|
990
1059
|
|
|
991
1060
|
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
992
|
-
broadcastUpdate(key, modifiedData, 'merge');
|
|
1061
|
+
broadcastUpdate(key, modifiedData, hasChanged, 'merge');
|
|
1062
|
+
|
|
1063
|
+
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
|
|
1064
|
+
if (!hasChanged) {
|
|
1065
|
+
return Promise.resolve();
|
|
1066
|
+
}
|
|
993
1067
|
|
|
994
1068
|
return Storage.mergeItem(key, batchedChanges, modifiedData);
|
|
995
1069
|
} catch (error) {
|
|
@@ -1000,15 +1074,6 @@ function merge(key, changes) {
|
|
|
1000
1074
|
});
|
|
1001
1075
|
}
|
|
1002
1076
|
|
|
1003
|
-
/**
|
|
1004
|
-
* @private
|
|
1005
|
-
* @param {String} key
|
|
1006
|
-
* @returns {Boolean}
|
|
1007
|
-
*/
|
|
1008
|
-
function hasPendingMergeForKey(key) {
|
|
1009
|
-
return Boolean(mergeQueue[key]);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
1077
|
/**
|
|
1013
1078
|
* Merge user provided default key value pairs.
|
|
1014
1079
|
* @private
|
|
@@ -1328,6 +1393,7 @@ const Onyx = {
|
|
|
1328
1393
|
isSafeEvictionKey,
|
|
1329
1394
|
METHOD,
|
|
1330
1395
|
setMemoryOnlyKeys,
|
|
1396
|
+
tryGetCachedValue,
|
|
1331
1397
|
};
|
|
1332
1398
|
|
|
1333
1399
|
/**
|
|
@@ -4,9 +4,8 @@ import fastMerge from '../../fastMerge';
|
|
|
4
4
|
let storageMapInternal = {};
|
|
5
5
|
|
|
6
6
|
const set = jest.fn((key, value) => {
|
|
7
|
-
// eslint-disable-next-line no-param-reassign
|
|
8
7
|
storageMapInternal[key] = value;
|
|
9
|
-
return Promise.resolve(
|
|
8
|
+
return Promise.resolve(value);
|
|
10
9
|
});
|
|
11
10
|
|
|
12
11
|
const localForageMock = {
|
package/lib/withOnyx.js
CHANGED
|
@@ -37,13 +37,25 @@ export default function (mapOnyxToState) {
|
|
|
37
37
|
// disconnected. It is a key value store with the format {[mapping.key]: connectionID}.
|
|
38
38
|
this.activeConnectionIDs = {};
|
|
39
39
|
|
|
40
|
+
const cachedState = _.reduce(mapOnyxToState, (resultObj, mapping, propertyName) => {
|
|
41
|
+
const key = Str.result(mapping.key, props);
|
|
42
|
+
const value = Onyx.tryGetCachedValue(key, mapping);
|
|
43
|
+
|
|
44
|
+
if (value !== undefined) {
|
|
45
|
+
// eslint-disable-next-line no-param-reassign
|
|
46
|
+
resultObj[propertyName] = value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return resultObj;
|
|
50
|
+
}, {});
|
|
51
|
+
|
|
52
|
+
// If we have all the data we need, then we can render the component immediately
|
|
53
|
+
cachedState.loading = _.size(cachedState) < requiredKeysForInit.length;
|
|
54
|
+
|
|
40
55
|
// Object holding the temporary initial state for the component while we load the various Onyx keys
|
|
41
|
-
this.tempState =
|
|
56
|
+
this.tempState = cachedState;
|
|
42
57
|
|
|
43
|
-
this.state =
|
|
44
|
-
// If there are no required keys for init then we can render the wrapped component immediately
|
|
45
|
-
loading: requiredKeysForInit.length > 0,
|
|
46
|
-
};
|
|
58
|
+
this.state = cachedState;
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
componentDidMount() {
|
|
@@ -60,7 +72,6 @@ export default function (mapOnyxToState) {
|
|
|
60
72
|
_.each(mapOnyxToState, (mapping, propertyName) => {
|
|
61
73
|
const previousKey = Str.result(mapping.key, prevProps);
|
|
62
74
|
const newKey = Str.result(mapping.key, this.props);
|
|
63
|
-
|
|
64
75
|
if (previousKey !== newKey) {
|
|
65
76
|
Onyx.disconnect(this.activeConnectionIDs[previousKey], previousKey);
|
|
66
77
|
delete this.activeConnectionIDs[previousKey];
|
|
@@ -88,6 +99,16 @@ export default function (mapOnyxToState) {
|
|
|
88
99
|
* @param {*} val
|
|
89
100
|
*/
|
|
90
101
|
setWithOnyxState(statePropertyName, val) {
|
|
102
|
+
// We might have loaded the values for the onyx keys/mappings already from the cache.
|
|
103
|
+
// In case we were able to load all the values upfront, the loading state will be false.
|
|
104
|
+
// However, Onyx.js will always call setWithOnyxState, as it doesn't know that this implementation
|
|
105
|
+
// already loaded the values from cache. Thus we have to check whether the value has changed
|
|
106
|
+
// before we set the state to prevent unnecessary renders.
|
|
107
|
+
const prevValue = this.state[statePropertyName];
|
|
108
|
+
if (!this.state.loading && prevValue === val) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
91
112
|
if (!this.state.loading) {
|
|
92
113
|
this.setState({[statePropertyName]: val});
|
|
93
114
|
return;
|
|
@@ -100,7 +121,14 @@ export default function (mapOnyxToState) {
|
|
|
100
121
|
return;
|
|
101
122
|
}
|
|
102
123
|
|
|
103
|
-
|
|
124
|
+
const stateUpdate = {...this.tempState, loading: false};
|
|
125
|
+
|
|
126
|
+
// The state is set here manually, instead of using setState because setState is an async operation, meaning it might execute on the next tick,
|
|
127
|
+
// or at a later point in the microtask queue. That can lead to a race condition where
|
|
128
|
+
// setWithOnyxState is called before the state is actually set. This results in unreliable behavior when checking the loading state and has been mainly observed on fabric.
|
|
129
|
+
this.state = stateUpdate;
|
|
130
|
+
|
|
131
|
+
this.setState(stateUpdate); // Trigger a render
|
|
104
132
|
delete this.tempState;
|
|
105
133
|
}
|
|
106
134
|
|