react-native-onyx 1.0.53 → 1.0.54
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 +88 -76
- 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 +65 -64
- package/lib/metrics/index.native.js +1 -0
- package/lib/storage/__mocks__/index.js +5 -3
- 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,24 @@ 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 {String} method
|
|
852
|
+
*/
|
|
853
|
+
function broadcastUpdate(key, value, method) {
|
|
854
|
+
// Logging properties only since values could be sensitive things we don't want to log
|
|
855
|
+
Logger.logInfo(`${method}() called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''}`);
|
|
856
|
+
|
|
857
|
+
// Update subscribers if the cached value has changed, or when the subscriber specifically requires
|
|
858
|
+
// all updates regardless of value changes (indicated by initWithStoredValues set to false).
|
|
859
|
+
const hasChanged = cache.hasValueChanged(key, value);
|
|
860
|
+
cache.set(key, value);
|
|
861
|
+
notifySubscribersOnNextTick(key, value, subscriber => hasChanged || subscriber.initWithStoredValues === false);
|
|
862
|
+
}
|
|
863
|
+
|
|
843
864
|
/**
|
|
844
865
|
* Write a value to our store with the given key
|
|
845
866
|
*
|
|
@@ -853,24 +874,9 @@ function set(key, value) {
|
|
|
853
874
|
return remove(key);
|
|
854
875
|
}
|
|
855
876
|
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
Logger.logAlert(`Onyx.set() called after Onyx.merge() for key: ${key}. It is recommended to use set() or merge() not both.`);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// If the value in the cache is the same as what we have then do not update subscribers unless they
|
|
862
|
-
// have initWithStoredValues: false then they MUST get all updates even if nothing has changed.
|
|
863
|
-
if (!cache.hasValueChanged(key, value)) {
|
|
864
|
-
cache.addToAccessedKeys(key);
|
|
865
|
-
notifySubscribersOnNextTick(key, value, subscriber => subscriber.initWithStoredValues === false);
|
|
866
|
-
return Promise.resolve();
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// Adds the key to cache when it's not available
|
|
870
|
-
cache.set(key, value);
|
|
871
|
-
notifySubscribersOnNextTick(key, value);
|
|
877
|
+
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
878
|
+
broadcastUpdate(key, value, 'set');
|
|
872
879
|
|
|
873
|
-
// Write the thing to persistent storage, which will trigger a storage event for any other tabs open on this domain
|
|
874
880
|
return Storage.setItem(key, value)
|
|
875
881
|
.catch(error => evictStorageAndRetry(error, set, key, value));
|
|
876
882
|
}
|
|
@@ -908,67 +914,43 @@ function multiSet(data) {
|
|
|
908
914
|
.catch(error => evictStorageAndRetry(error, multiSet, data));
|
|
909
915
|
}
|
|
910
916
|
|
|
911
|
-
// Key/value store of Onyx key and arrays of values to merge
|
|
912
|
-
const mergeQueue = {};
|
|
913
|
-
|
|
914
917
|
/**
|
|
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.
|
|
918
|
+
* Merges an array of changes with an existing value
|
|
928
919
|
*
|
|
929
920
|
* @private
|
|
930
|
-
* @param {
|
|
931
|
-
* @param {*}
|
|
932
|
-
*
|
|
921
|
+
* @param {Array<*>} changes Array of changes that should be applied to the existing value
|
|
922
|
+
* @param {*} existingValue
|
|
933
923
|
* @returns {*}
|
|
934
924
|
*/
|
|
935
|
-
function applyMerge(
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return _.reduce(mergeValues, (modifiedData, mergeValue) => [
|
|
941
|
-
...modifiedData,
|
|
942
|
-
...mergeValue,
|
|
943
|
-
], data || []);
|
|
925
|
+
function applyMerge(changes, existingValue) {
|
|
926
|
+
const lastChange = _.last(changes);
|
|
927
|
+
|
|
928
|
+
if (_.isArray(existingValue) || _.isArray(lastChange)) {
|
|
929
|
+
return lastChange;
|
|
944
930
|
}
|
|
945
931
|
|
|
946
|
-
if (_.isObject(
|
|
932
|
+
if (_.isObject(existingValue) || _.every(changes, _.isObject)) {
|
|
947
933
|
// Object values are merged one after the other
|
|
948
|
-
return _.reduce(
|
|
934
|
+
return _.reduce(changes, (modifiedData, change) => {
|
|
949
935
|
// lodash adds a small overhead so we don't use it here
|
|
950
936
|
// eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
|
|
951
|
-
const newData = Object.assign({}, fastMerge(modifiedData,
|
|
937
|
+
const newData = Object.assign({}, fastMerge(modifiedData, change));
|
|
952
938
|
|
|
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
939
|
// Remove all first level keys that are explicitly set to null.
|
|
956
|
-
return _.omit(newData,
|
|
957
|
-
},
|
|
940
|
+
return _.omit(newData, value => _.isNull(value));
|
|
941
|
+
}, existingValue || {});
|
|
958
942
|
}
|
|
959
943
|
|
|
960
944
|
// If we have anything else we can't merge it so we'll
|
|
961
945
|
// simply return the last value that was queued
|
|
962
|
-
return
|
|
946
|
+
return lastChange;
|
|
963
947
|
}
|
|
964
948
|
|
|
965
949
|
/**
|
|
966
950
|
* Merge a new value into an existing value at a key.
|
|
967
951
|
*
|
|
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
|
|
952
|
+
* The types of values that can be merged are `Object` and `Array`. To set another type of value use `Onyx.set()`.
|
|
953
|
+
* 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
954
|
*
|
|
973
955
|
* Calls to `Onyx.merge()` are batched so that any calls performed in a single tick will stack in a queue and get
|
|
974
956
|
* applied in the order they were called. Note: `Onyx.set()` calls do not work this way so use caution when mixing
|
|
@@ -981,26 +963,35 @@ function applyMerge(key, data) {
|
|
|
981
963
|
* Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Workspace'}
|
|
982
964
|
*
|
|
983
965
|
* @param {String} key ONYXKEYS key
|
|
984
|
-
* @param {(Object|Array)}
|
|
966
|
+
* @param {(Object|Array)} changes Object or Array value to merge
|
|
985
967
|
* @returns {Promise}
|
|
986
968
|
*/
|
|
987
|
-
function merge(key,
|
|
969
|
+
function merge(key, changes) {
|
|
970
|
+
// Merge attempts are batched together. The delta should be applied after a single call to get() to prevent a race condition.
|
|
971
|
+
// Using the initial value from storage in subsequent merge attempts will lead to an incorrect final merged value.
|
|
988
972
|
if (mergeQueue[key]) {
|
|
989
|
-
mergeQueue[key].push(
|
|
973
|
+
mergeQueue[key].push(changes);
|
|
990
974
|
return Promise.resolve();
|
|
991
975
|
}
|
|
976
|
+
mergeQueue[key] = [changes];
|
|
992
977
|
|
|
993
|
-
mergeQueue[key] = [value];
|
|
994
978
|
return get(key)
|
|
995
|
-
.then((
|
|
979
|
+
.then((existingValue) => {
|
|
996
980
|
try {
|
|
997
|
-
|
|
981
|
+
// 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]);
|
|
998
983
|
|
|
999
984
|
// Clean up the write queue so we
|
|
1000
985
|
// don't apply these changes again
|
|
1001
986
|
delete mergeQueue[key];
|
|
1002
987
|
|
|
1003
|
-
|
|
988
|
+
// After that we merge the batched changes with the existing value
|
|
989
|
+
const modifiedData = applyMerge([batchedChanges], existingValue);
|
|
990
|
+
|
|
991
|
+
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
992
|
+
broadcastUpdate(key, modifiedData, 'merge');
|
|
993
|
+
|
|
994
|
+
return Storage.mergeItem(key, batchedChanges, modifiedData);
|
|
1004
995
|
} catch (error) {
|
|
1005
996
|
Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
|
|
1006
997
|
}
|
|
@@ -1009,6 +1000,15 @@ function merge(key, value) {
|
|
|
1009
1000
|
});
|
|
1010
1001
|
}
|
|
1011
1002
|
|
|
1003
|
+
/**
|
|
1004
|
+
* @private
|
|
1005
|
+
* @param {String} key
|
|
1006
|
+
* @returns {Boolean}
|
|
1007
|
+
*/
|
|
1008
|
+
function hasPendingMergeForKey(key) {
|
|
1009
|
+
return Boolean(mergeQueue[key]);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
1012
|
/**
|
|
1013
1013
|
* Merge user provided default key value pairs.
|
|
1014
1014
|
* @private
|
|
@@ -1317,6 +1317,7 @@ const Onyx = {
|
|
|
1317
1317
|
multiSet,
|
|
1318
1318
|
merge,
|
|
1319
1319
|
mergeCollection,
|
|
1320
|
+
hasPendingMergeForKey,
|
|
1320
1321
|
update,
|
|
1321
1322
|
clear,
|
|
1322
1323
|
getAllKeys,
|
|
@@ -11,8 +11,7 @@ const set = jest.fn((key, value) => {
|
|
|
11
11
|
|
|
12
12
|
const localForageMock = {
|
|
13
13
|
setItem(key, value) {
|
|
14
|
-
set(key, value);
|
|
15
|
-
return Promise.resolve();
|
|
14
|
+
return set(key, value);
|
|
16
15
|
},
|
|
17
16
|
multiSet(pairs) {
|
|
18
17
|
const setPromises = _.map(pairs, ([key, value]) => this.setItem(key, value));
|
|
@@ -36,6 +35,9 @@ const localForageMock = {
|
|
|
36
35
|
|
|
37
36
|
return Promise.resolve(storageMapInternal);
|
|
38
37
|
},
|
|
38
|
+
mergeItem(key, _changes, modifiedData) {
|
|
39
|
+
return this.setItem(key, modifiedData);
|
|
40
|
+
},
|
|
39
41
|
removeItem(key) {
|
|
40
42
|
delete storageMapInternal[key];
|
|
41
43
|
return Promise.resolve();
|
|
@@ -68,7 +70,7 @@ const localForageMockSpy = {
|
|
|
68
70
|
multiGet: jest.fn(localForageMock.multiGet),
|
|
69
71
|
multiSet: jest.fn(localForageMock.multiSet),
|
|
70
72
|
multiMerge: jest.fn(localForageMock.multiMerge),
|
|
71
|
-
|
|
73
|
+
mergeItem: jest.fn(localForageMock.mergeItem),
|
|
72
74
|
getStorageMap: jest.fn(() => storageMapInternal),
|
|
73
75
|
setInitialMockData: jest.fn((data) => {
|
|
74
76
|
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.54",
|
|
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",
|