react-native-onyx 1.0.53 → 1.0.55
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 +111 -74
- 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 +88 -62
- package/lib/metrics/index.native.js +1 -0
- package/lib/storage/__mocks__/index.js +6 -5
- package/lib/storage/providers/LocalForage.js +21 -10
- package/lib/storage/providers/SQLiteStorage.js +14 -4
- package/package.json +2 -2
package/lib/Onyx.js
CHANGED
|
@@ -18,6 +18,9 @@ const METHOD = {
|
|
|
18
18
|
CLEAR: 'clear',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
// Key/value store of Onyx key and arrays of values to merge
|
|
22
|
+
const mergeQueue = {};
|
|
23
|
+
|
|
21
24
|
// Keeps track of the last connectionID that was used so we can keep incrementing it
|
|
22
25
|
let lastConnectionID = 0;
|
|
23
26
|
|
|
@@ -840,6 +843,38 @@ function evictStorageAndRetry(error, onyxMethod, ...args) {
|
|
|
840
843
|
.then(() => onyxMethod(...args));
|
|
841
844
|
}
|
|
842
845
|
|
|
846
|
+
/**
|
|
847
|
+
* Notifys subscribers and writes current value to cache
|
|
848
|
+
*
|
|
849
|
+
* @param {String} key
|
|
850
|
+
* @param {*} value
|
|
851
|
+
* @param {Boolean} hasChanged
|
|
852
|
+
* @param {String} method
|
|
853
|
+
*/
|
|
854
|
+
function broadcastUpdate(key, value, hasChanged, method) {
|
|
855
|
+
// Logging properties only since values could be sensitive things we don't want to log
|
|
856
|
+
Logger.logInfo(`${method}() called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''}`);
|
|
857
|
+
|
|
858
|
+
// Update subscribers if the cached value has changed, or when the subscriber specifically requires
|
|
859
|
+
// all updates regardless of value changes (indicated by initWithStoredValues set to false).
|
|
860
|
+
if (hasChanged) {
|
|
861
|
+
cache.set(key, value);
|
|
862
|
+
} else {
|
|
863
|
+
cache.addToAccessedKeys(key);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
notifySubscribersOnNextTick(key, value, subscriber => hasChanged || subscriber.initWithStoredValues === false);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* @private
|
|
871
|
+
* @param {String} key
|
|
872
|
+
* @returns {Boolean}
|
|
873
|
+
*/
|
|
874
|
+
function hasPendingMergeForKey(key) {
|
|
875
|
+
return Boolean(mergeQueue[key]);
|
|
876
|
+
}
|
|
877
|
+
|
|
843
878
|
/**
|
|
844
879
|
* Write a value to our store with the given key
|
|
845
880
|
*
|
|
@@ -853,24 +888,20 @@ function set(key, value) {
|
|
|
853
888
|
return remove(key);
|
|
854
889
|
}
|
|
855
890
|
|
|
856
|
-
// eslint-disable-next-line no-use-before-define
|
|
857
891
|
if (hasPendingMergeForKey(key)) {
|
|
858
892
|
Logger.logAlert(`Onyx.set() called after Onyx.merge() for key: ${key}. It is recommended to use set() or merge() not both.`);
|
|
859
893
|
}
|
|
860
894
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
895
|
+
const hasChanged = cache.hasValueChanged(key, value);
|
|
896
|
+
|
|
897
|
+
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
898
|
+
broadcastUpdate(key, value, hasChanged, 'set');
|
|
899
|
+
|
|
900
|
+
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
|
|
901
|
+
if (!hasChanged) {
|
|
866
902
|
return Promise.resolve();
|
|
867
903
|
}
|
|
868
904
|
|
|
869
|
-
// Adds the key to cache when it's not available
|
|
870
|
-
cache.set(key, value);
|
|
871
|
-
notifySubscribersOnNextTick(key, value);
|
|
872
|
-
|
|
873
|
-
// Write the thing to persistent storage, which will trigger a storage event for any other tabs open on this domain
|
|
874
905
|
return Storage.setItem(key, value)
|
|
875
906
|
.catch(error => evictStorageAndRetry(error, set, key, value));
|
|
876
907
|
}
|
|
@@ -908,67 +939,39 @@ function multiSet(data) {
|
|
|
908
939
|
.catch(error => evictStorageAndRetry(error, multiSet, data));
|
|
909
940
|
}
|
|
910
941
|
|
|
911
|
-
// Key/value store of Onyx key and arrays of values to merge
|
|
912
|
-
const mergeQueue = {};
|
|
913
|
-
|
|
914
942
|
/**
|
|
915
|
-
*
|
|
916
|
-
* @param {String} key
|
|
917
|
-
* @returns {Boolean}
|
|
918
|
-
*/
|
|
919
|
-
function hasPendingMergeForKey(key) {
|
|
920
|
-
return Boolean(mergeQueue[key]);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Given an Onyx key and value this method will combine all queued
|
|
925
|
-
* value updates and return a single value. Merge attempts are
|
|
926
|
-
* batched. They must occur after a single call to get() so we
|
|
927
|
-
* can avoid race conditions.
|
|
943
|
+
* Merges an array of changes with an existing value
|
|
928
944
|
*
|
|
929
945
|
* @private
|
|
930
|
-
* @param {
|
|
931
|
-
* @param {
|
|
932
|
-
*
|
|
946
|
+
* @param {*} existingValue
|
|
947
|
+
* @param {Array<*>} changes Array of changes that should be applied to the existing value
|
|
933
948
|
* @returns {*}
|
|
934
949
|
*/
|
|
935
|
-
function applyMerge(
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return _.reduce(mergeValues, (modifiedData, mergeValue) => [
|
|
941
|
-
...modifiedData,
|
|
942
|
-
...mergeValue,
|
|
943
|
-
], data || []);
|
|
950
|
+
function applyMerge(existingValue, changes) {
|
|
951
|
+
const lastChange = _.last(changes);
|
|
952
|
+
|
|
953
|
+
if (_.isArray(existingValue) || _.isArray(lastChange)) {
|
|
954
|
+
return lastChange;
|
|
944
955
|
}
|
|
945
956
|
|
|
946
|
-
if (_.isObject(
|
|
957
|
+
if (_.isObject(existingValue) || _.every(changes, _.isObject)) {
|
|
947
958
|
// Object values are merged one after the other
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
// We will also delete any object keys that are undefined or null.
|
|
954
|
-
// Deleting keys is not supported by AsyncStorage so we do it this way.
|
|
955
|
-
// Remove all first level keys that are explicitly set to null.
|
|
956
|
-
return _.omit(newData, (value, finalObjectKey) => _.isNull(mergeValue[finalObjectKey]));
|
|
957
|
-
}, data || {});
|
|
959
|
+
// lodash adds a small overhead so we don't use it here
|
|
960
|
+
// eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
|
|
961
|
+
return _.reduce(changes, (modifiedData, change) => fastMerge(modifiedData, change),
|
|
962
|
+
existingValue || {});
|
|
958
963
|
}
|
|
959
964
|
|
|
960
965
|
// If we have anything else we can't merge it so we'll
|
|
961
966
|
// simply return the last value that was queued
|
|
962
|
-
return
|
|
967
|
+
return lastChange;
|
|
963
968
|
}
|
|
964
969
|
|
|
965
970
|
/**
|
|
966
971
|
* Merge a new value into an existing value at a key.
|
|
967
972
|
*
|
|
968
|
-
* The types of values that can be merged are `Object` and `Array`. To set another type of value use `Onyx.set()`.
|
|
969
|
-
*
|
|
970
|
-
* to note that if you have an array value property on an `Object` that the default behavior of lodash/merge is not to
|
|
971
|
-
* concatenate. See here: https://github.com/lodash/lodash/issues/2872
|
|
973
|
+
* The types of values that can be merged are `Object` and `Array`. To set another type of value use `Onyx.set()`.
|
|
974
|
+
* Values of type `Object` get merged with the old value, whilst for `Array`'s we simply replace the current value with the new one.
|
|
972
975
|
*
|
|
973
976
|
* Calls to `Onyx.merge()` are batched so that any calls performed in a single tick will stack in a queue and get
|
|
974
977
|
* applied in the order they were called. Note: `Onyx.set()` calls do not work this way so use caution when mixing
|
|
@@ -981,26 +984,48 @@ function applyMerge(key, data) {
|
|
|
981
984
|
* Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Workspace'}
|
|
982
985
|
*
|
|
983
986
|
* @param {String} key ONYXKEYS key
|
|
984
|
-
* @param {(Object|Array)}
|
|
987
|
+
* @param {(Object|Array)} changes Object or Array value to merge
|
|
985
988
|
* @returns {Promise}
|
|
986
989
|
*/
|
|
987
|
-
function merge(key,
|
|
990
|
+
function merge(key, changes) {
|
|
991
|
+
// Merge attempts are batched together. The delta should be applied after a single call to get() to prevent a race condition.
|
|
992
|
+
// Using the initial value from storage in subsequent merge attempts will lead to an incorrect final merged value.
|
|
988
993
|
if (mergeQueue[key]) {
|
|
989
|
-
mergeQueue[key].push(
|
|
994
|
+
mergeQueue[key].push(changes);
|
|
990
995
|
return Promise.resolve();
|
|
991
996
|
}
|
|
997
|
+
mergeQueue[key] = [changes];
|
|
992
998
|
|
|
993
|
-
mergeQueue[key] = [value];
|
|
994
999
|
return get(key)
|
|
995
|
-
.then((
|
|
1000
|
+
.then((existingValue) => {
|
|
996
1001
|
try {
|
|
997
|
-
|
|
1002
|
+
// 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)
|
|
1003
|
+
const batchedChanges = applyMerge(undefined, mergeQueue[key]);
|
|
998
1004
|
|
|
999
1005
|
// Clean up the write queue so we
|
|
1000
1006
|
// don't apply these changes again
|
|
1001
1007
|
delete mergeQueue[key];
|
|
1002
1008
|
|
|
1003
|
-
|
|
1009
|
+
// After that we merge the batched changes with the existing value
|
|
1010
|
+
let modifiedData = applyMerge(existingValue, [batchedChanges]);
|
|
1011
|
+
|
|
1012
|
+
// For objects, the key for null values needs to be removed from the object to ensure the value will get removed from storage completely.
|
|
1013
|
+
// On native, SQLite will remove top-level keys that are null. To be consistent, we remove them on web too.
|
|
1014
|
+
if (!_.isArray(modifiedData) && _.isObject(modifiedData)) {
|
|
1015
|
+
modifiedData = _.omit(modifiedData, value => _.isNull(value));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const hasChanged = cache.hasValueChanged(key, modifiedData);
|
|
1019
|
+
|
|
1020
|
+
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
1021
|
+
broadcastUpdate(key, modifiedData, hasChanged, 'merge');
|
|
1022
|
+
|
|
1023
|
+
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
|
|
1024
|
+
if (!hasChanged) {
|
|
1025
|
+
return Promise.resolve();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return Storage.mergeItem(key, batchedChanges, modifiedData);
|
|
1004
1029
|
} catch (error) {
|
|
1005
1030
|
Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
|
|
1006
1031
|
}
|
|
@@ -1317,6 +1342,7 @@ const Onyx = {
|
|
|
1317
1342
|
multiSet,
|
|
1318
1343
|
merge,
|
|
1319
1344
|
mergeCollection,
|
|
1345
|
+
hasPendingMergeForKey,
|
|
1320
1346
|
update,
|
|
1321
1347
|
clear,
|
|
1322
1348
|
getAllKeys,
|
|
@@ -4,15 +4,13 @@ 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 = {
|
|
13
12
|
setItem(key, value) {
|
|
14
|
-
set(key, value);
|
|
15
|
-
return Promise.resolve();
|
|
13
|
+
return set(key, value);
|
|
16
14
|
},
|
|
17
15
|
multiSet(pairs) {
|
|
18
16
|
const setPromises = _.map(pairs, ([key, value]) => this.setItem(key, value));
|
|
@@ -36,6 +34,9 @@ const localForageMock = {
|
|
|
36
34
|
|
|
37
35
|
return Promise.resolve(storageMapInternal);
|
|
38
36
|
},
|
|
37
|
+
mergeItem(key, _changes, modifiedData) {
|
|
38
|
+
return this.setItem(key, modifiedData);
|
|
39
|
+
},
|
|
39
40
|
removeItem(key) {
|
|
40
41
|
delete storageMapInternal[key];
|
|
41
42
|
return Promise.resolve();
|
|
@@ -68,7 +69,7 @@ const localForageMockSpy = {
|
|
|
68
69
|
multiGet: jest.fn(localForageMock.multiGet),
|
|
69
70
|
multiSet: jest.fn(localForageMock.multiSet),
|
|
70
71
|
multiMerge: jest.fn(localForageMock.multiMerge),
|
|
71
|
-
|
|
72
|
+
mergeItem: jest.fn(localForageMock.mergeItem),
|
|
72
73
|
getStorageMap: jest.fn(() => storageMapInternal),
|
|
73
74
|
setInitialMockData: jest.fn((data) => {
|
|
74
75
|
storageMapInternal = data;
|
|
@@ -49,6 +49,16 @@ const provider = {
|
|
|
49
49
|
return localforage.setItem(key, value);
|
|
50
50
|
}),
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Sets the value for a given key. The only requirement is that the value should be serializable to JSON string
|
|
54
|
+
* @param {String} key
|
|
55
|
+
* @param {*} value
|
|
56
|
+
* @return {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
setItem(key, value) {
|
|
59
|
+
return this.setItemQueue.push({key, value});
|
|
60
|
+
},
|
|
61
|
+
|
|
52
62
|
/**
|
|
53
63
|
* Get multiple key-value pairs for the give array of keys in a batch
|
|
54
64
|
* @param {String[]} keys
|
|
@@ -76,6 +86,17 @@ const provider = {
|
|
|
76
86
|
return Promise.all(tasks).then(() => Promise.resolve());
|
|
77
87
|
},
|
|
78
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Merging an existing value with a new one
|
|
91
|
+
* @param {String} key
|
|
92
|
+
* @param {any} _changes - not used, as we rely on the pre-merged data from the `modifiedData`
|
|
93
|
+
* @param {any} modifiedData - the pre-merged data from `Onyx.applyMerge`
|
|
94
|
+
* @return {Promise<void>}
|
|
95
|
+
*/
|
|
96
|
+
mergeItem(key, _changes, modifiedData) {
|
|
97
|
+
return this.setItem(key, modifiedData);
|
|
98
|
+
},
|
|
99
|
+
|
|
79
100
|
/**
|
|
80
101
|
* Stores multiple key-value pairs in a batch
|
|
81
102
|
* @param {Array<[key, value]>} pairs
|
|
@@ -126,16 +147,6 @@ const provider = {
|
|
|
126
147
|
return localforage.removeItems(keys);
|
|
127
148
|
},
|
|
128
149
|
|
|
129
|
-
/**
|
|
130
|
-
* Sets the value for a given key. The only requirement is that the value should be serializable to JSON string
|
|
131
|
-
* @param {String} key
|
|
132
|
-
* @param {*} value
|
|
133
|
-
* @return {Promise<void>}
|
|
134
|
-
*/
|
|
135
|
-
setItem(key, value) {
|
|
136
|
-
return this.setItemQueue.push({key, value});
|
|
137
|
-
},
|
|
138
|
-
|
|
139
150
|
/**
|
|
140
151
|
* @param {string[]} keyList
|
|
141
152
|
*/
|
|
@@ -82,10 +82,10 @@ const provider = {
|
|
|
82
82
|
multiMerge(pairs) {
|
|
83
83
|
// Note: We use `ON CONFLICT DO UPDATE` here instead of `INSERT OR REPLACE INTO`
|
|
84
84
|
// so the new JSON value is merged into the old one if there's an existing value
|
|
85
|
-
const query = `INSERT INTO keyvaluepairs (record_key, valueJSON)
|
|
86
|
-
VALUES (:key, JSON(:value))
|
|
87
|
-
ON CONFLICT DO UPDATE
|
|
88
|
-
SET valueJSON = JSON_PATCH(valueJSON, JSON(:value));
|
|
85
|
+
const query = `INSERT INTO keyvaluepairs (record_key, valueJSON)
|
|
86
|
+
VALUES (:key, JSON(:value))
|
|
87
|
+
ON CONFLICT DO UPDATE
|
|
88
|
+
SET valueJSON = JSON_PATCH(valueJSON, JSON(:value));
|
|
89
89
|
`;
|
|
90
90
|
const queryArguments = _.map(pairs, (pair) => {
|
|
91
91
|
const value = JSON.stringify(pair[1]);
|
|
@@ -94,6 +94,16 @@ const provider = {
|
|
|
94
94
|
return db.executeBatchAsync([[query, queryArguments]]);
|
|
95
95
|
},
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Merges an existing value with a new one by leveraging JSON_PATCH
|
|
99
|
+
* @param {String} key
|
|
100
|
+
* @param {*} changes - the delta for a specific key
|
|
101
|
+
* @return {Promise<void>}
|
|
102
|
+
*/
|
|
103
|
+
mergeItem(key, changes) {
|
|
104
|
+
return this.multiMerge([[key, changes]]);
|
|
105
|
+
},
|
|
106
|
+
|
|
97
107
|
/**
|
|
98
108
|
* Returns all keys available in storage
|
|
99
109
|
* @returns {Promise<String[]>}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-onyx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.55",
|
|
4
4
|
"author": "Expensify, Inc.",
|
|
5
5
|
"homepage": "https://expensify.com",
|
|
6
6
|
"description": "State management for React Native",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"babel-jest": "^26.2.2",
|
|
56
56
|
"babel-loader": "^8.2.5",
|
|
57
57
|
"eslint": "^7.6.0",
|
|
58
|
-
"eslint-config-expensify": "^2.0.
|
|
58
|
+
"eslint-config-expensify": "^2.0.38",
|
|
59
59
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
|
60
60
|
"eslint-plugin-react": "^7.31.10",
|
|
61
61
|
"jest": "^26.5.2",
|