react-native-onyx 1.0.98 → 1.0.100
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 -57
- 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 +58 -19
- package/lib/storage/__mocks__/index.js +1 -2
- package/lib/storage/providers/IDBKeyVal.js +3 -2
- package/lib/utils.js +48 -35
- package/package.json +1 -1
package/lib/Onyx.js
CHANGED
|
@@ -1024,6 +1024,25 @@ function hasPendingMergeForKey(key) {
|
|
|
1024
1024
|
return Boolean(mergeQueue[key]);
|
|
1025
1025
|
}
|
|
1026
1026
|
|
|
1027
|
+
/**
|
|
1028
|
+
* Removes a key from storage if the value is null.
|
|
1029
|
+
* Otherwise removes all nested null values in objects and returns the object
|
|
1030
|
+
* @param {String} key
|
|
1031
|
+
* @param {Mixed} value
|
|
1032
|
+
* @returns {Mixed} `null` if the key got removed completely, otherwise the value without null values
|
|
1033
|
+
*/
|
|
1034
|
+
function removeNullValues(key, value) {
|
|
1035
|
+
if (_.isNull(value)) {
|
|
1036
|
+
remove(key);
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// We can remove all null values in an object by merging it with itself
|
|
1041
|
+
// utils.fastMerge recursively goes through the object and removes all null values
|
|
1042
|
+
// Passing two identical objects as source and target to fastMerge will not change it, but only remove the null values
|
|
1043
|
+
return utils.removeNestedNullValues(value);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1027
1046
|
/**
|
|
1028
1047
|
* Write a value to our store with the given key
|
|
1029
1048
|
*
|
|
@@ -1033,28 +1052,28 @@ function hasPendingMergeForKey(key) {
|
|
|
1033
1052
|
* @returns {Promise}
|
|
1034
1053
|
*/
|
|
1035
1054
|
function set(key, value) {
|
|
1036
|
-
|
|
1037
|
-
|
|
1055
|
+
const valueWithoutNull = removeNullValues(key, value);
|
|
1056
|
+
|
|
1057
|
+
if (valueWithoutNull === null) {
|
|
1058
|
+
return Promise.resolve();
|
|
1038
1059
|
}
|
|
1039
1060
|
|
|
1040
1061
|
if (hasPendingMergeForKey(key)) {
|
|
1041
1062
|
Logger.logAlert(`Onyx.set() called after Onyx.merge() for key: ${key}. It is recommended to use set() or merge() not both.`);
|
|
1042
1063
|
}
|
|
1043
1064
|
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
const hasChanged = cache.hasValueChanged(key, valueWithNullRemoved);
|
|
1065
|
+
const hasChanged = cache.hasValueChanged(key, valueWithoutNull);
|
|
1047
1066
|
|
|
1048
1067
|
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
1049
|
-
const updatePromise = broadcastUpdate(key,
|
|
1068
|
+
const updatePromise = broadcastUpdate(key, valueWithoutNull, hasChanged, 'set');
|
|
1050
1069
|
|
|
1051
1070
|
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
|
|
1052
1071
|
if (!hasChanged) {
|
|
1053
1072
|
return updatePromise;
|
|
1054
1073
|
}
|
|
1055
1074
|
|
|
1056
|
-
return Storage.setItem(key,
|
|
1057
|
-
.catch(error => evictStorageAndRetry(error, set, key,
|
|
1075
|
+
return Storage.setItem(key, valueWithoutNull)
|
|
1076
|
+
.catch(error => evictStorageAndRetry(error, set, key, valueWithoutNull))
|
|
1058
1077
|
.then(() => updatePromise);
|
|
1059
1078
|
}
|
|
1060
1079
|
|
|
@@ -1087,7 +1106,16 @@ function multiSet(data) {
|
|
|
1087
1106
|
return scheduleSubscriberUpdate(key, val);
|
|
1088
1107
|
});
|
|
1089
1108
|
|
|
1090
|
-
|
|
1109
|
+
const keyValuePairsWithoutNull = _.filter(_.map(keyValuePairs, ([key, value]) => {
|
|
1110
|
+
const valueWithoutNull = removeNullValues(key, value);
|
|
1111
|
+
|
|
1112
|
+
if (valueWithoutNull === null) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
return [key, valueWithoutNull];
|
|
1116
|
+
}), Boolean);
|
|
1117
|
+
|
|
1118
|
+
return Storage.multiSet(keyValuePairsWithoutNull)
|
|
1091
1119
|
.catch(error => evictStorageAndRetry(error, multiSet, data))
|
|
1092
1120
|
.then(() => Promise.all(updatePromises));
|
|
1093
1121
|
}
|
|
@@ -1098,9 +1126,10 @@ function multiSet(data) {
|
|
|
1098
1126
|
* @private
|
|
1099
1127
|
* @param {*} existingValue
|
|
1100
1128
|
* @param {Array<*>} changes Array of changes that should be applied to the existing value
|
|
1129
|
+
* @param {Boolean} shouldRemoveNullObjectValues
|
|
1101
1130
|
* @returns {*}
|
|
1102
1131
|
*/
|
|
1103
|
-
function applyMerge(existingValue, changes) {
|
|
1132
|
+
function applyMerge(existingValue, changes, shouldRemoveNullObjectValues) {
|
|
1104
1133
|
const lastChange = _.last(changes);
|
|
1105
1134
|
|
|
1106
1135
|
if (_.isArray(lastChange)) {
|
|
@@ -1109,7 +1138,7 @@ function applyMerge(existingValue, changes) {
|
|
|
1109
1138
|
|
|
1110
1139
|
if (_.some(changes, _.isObject)) {
|
|
1111
1140
|
// Object values are then merged one after the other
|
|
1112
|
-
return _.reduce(changes, (modifiedData, change) => utils.fastMerge(modifiedData, change),
|
|
1141
|
+
return _.reduce(changes, (modifiedData, change) => utils.fastMerge(modifiedData, change, shouldRemoveNullObjectValues),
|
|
1113
1142
|
existingValue || {});
|
|
1114
1143
|
}
|
|
1115
1144
|
|
|
@@ -1157,10 +1186,12 @@ function merge(key, changes) {
|
|
|
1157
1186
|
.then((existingValue) => {
|
|
1158
1187
|
try {
|
|
1159
1188
|
// 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)
|
|
1160
|
-
|
|
1189
|
+
// We don't want to remove null values from the "batchedChanges", because SQLite uses them to remove keys from storage natively.
|
|
1190
|
+
let batchedChanges = applyMerge(undefined, mergeQueue[key], false);
|
|
1161
1191
|
|
|
1162
1192
|
if (_.isNull(batchedChanges)) {
|
|
1163
|
-
|
|
1193
|
+
remove(key);
|
|
1194
|
+
return;
|
|
1164
1195
|
}
|
|
1165
1196
|
|
|
1166
1197
|
// The presence of a `null` in the merge queue instructs us to drop the existing value.
|
|
@@ -1172,15 +1203,16 @@ function merge(key, changes) {
|
|
|
1172
1203
|
delete mergeQueuePromise[key];
|
|
1173
1204
|
|
|
1174
1205
|
// After that we merge the batched changes with the existing value
|
|
1175
|
-
|
|
1176
|
-
|
|
1206
|
+
// We can remove null values from the "modifiedData", because "null" implicates that the user wants to remove a value from storage.
|
|
1207
|
+
// The "modifiedData" will be directly "set" in storage instead of being merged
|
|
1208
|
+
const modifiedData = shouldOverwriteExistingValue ? batchedChanges : applyMerge(existingValue, [batchedChanges], true);
|
|
1177
1209
|
|
|
1178
1210
|
// On native platforms we use SQLite which utilises JSON_PATCH to merge changes.
|
|
1179
|
-
// JSON_PATCH generally removes
|
|
1180
|
-
// When there is no existing value though, SQLite will just insert the changes as a new value and thus the
|
|
1181
|
-
// Therefore we need to remove
|
|
1211
|
+
// JSON_PATCH generally removes null values from the stored object.
|
|
1212
|
+
// When there is no existing value though, SQLite will just insert the changes as a new value and thus the null values won't be removed.
|
|
1213
|
+
// Therefore we need to remove null values from the `batchedChanges` which are sent to the SQLite, if no existing value is present.
|
|
1182
1214
|
if (!existingValue) {
|
|
1183
|
-
batchedChanges =
|
|
1215
|
+
batchedChanges = applyMerge(undefined, [batchedChanges], true);
|
|
1184
1216
|
}
|
|
1185
1217
|
|
|
1186
1218
|
const hasChanged = cache.hasValueChanged(key, modifiedData);
|
|
@@ -1349,6 +1381,13 @@ function mergeCollection(collectionKey, collection) {
|
|
|
1349
1381
|
.then((persistedKeys) => {
|
|
1350
1382
|
// Split to keys that exist in storage and keys that don't
|
|
1351
1383
|
const [existingKeys, newKeys] = _.chain(collection)
|
|
1384
|
+
.pick((value, key) => {
|
|
1385
|
+
if (_.isNull(value)) {
|
|
1386
|
+
remove(key);
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1389
|
+
return true;
|
|
1390
|
+
})
|
|
1352
1391
|
.keys()
|
|
1353
1392
|
.partition(key => persistedKeys.includes(key))
|
|
1354
1393
|
.value();
|
|
@@ -26,8 +26,7 @@ const idbKeyvalMock = {
|
|
|
26
26
|
multiMerge(pairs) {
|
|
27
27
|
_.forEach(pairs, ([key, value]) => {
|
|
28
28
|
const existingValue = storageMapInternal[key];
|
|
29
|
-
const newValue =
|
|
30
|
-
? utils.fastMerge(existingValue, value) : value;
|
|
29
|
+
const newValue = utils.fastMerge(existingValue, value);
|
|
31
30
|
|
|
32
31
|
set(key, newValue);
|
|
33
32
|
});
|
|
@@ -44,6 +44,7 @@ const provider = {
|
|
|
44
44
|
/**
|
|
45
45
|
* Multiple merging of existing and new values in a batch
|
|
46
46
|
* @param {Array<[key, value]>} pairs
|
|
47
|
+
* This function also removes all nested null values from an object.
|
|
47
48
|
* @return {Promise<void>}
|
|
48
49
|
*/
|
|
49
50
|
multiMerge: pairs => getCustomStore()('readwrite', (store) => {
|
|
@@ -55,8 +56,8 @@ const provider = {
|
|
|
55
56
|
return getValues.then((values) => {
|
|
56
57
|
const upsertMany = _.map(pairs, ([key, value], index) => {
|
|
57
58
|
const prev = values[index];
|
|
58
|
-
const newValue =
|
|
59
|
-
return promisifyRequest(store.put(
|
|
59
|
+
const newValue = utils.fastMerge(prev, value);
|
|
60
|
+
return promisifyRequest(store.put(newValue, key));
|
|
60
61
|
});
|
|
61
62
|
return Promise.all(upsertMany);
|
|
62
63
|
});
|
package/lib/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import _ from 'underscore';
|
|
2
2
|
|
|
3
3
|
function areObjectsEmpty(a, b) {
|
|
4
4
|
return (
|
|
@@ -18,16 +18,19 @@ function areObjectsEmpty(a, b) {
|
|
|
18
18
|
function isMergeableObject(val) {
|
|
19
19
|
const nonNullObject = val != null ? typeof val === 'object' : false;
|
|
20
20
|
return (nonNullObject
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
&& Object.prototype.toString.call(val) !== '[object RegExp]'
|
|
22
|
+
&& Object.prototype.toString.call(val) !== '[object Date]')
|
|
23
|
+
// eslint-disable-next-line rulesdir/prefer-underscore-method
|
|
24
|
+
&& !Array.isArray(val);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* @param {Object} target
|
|
27
29
|
* @param {Object} source
|
|
30
|
+
* @param {Boolean} shouldRemoveNullObjectValues
|
|
28
31
|
* @returns {Object}
|
|
29
32
|
*/
|
|
30
|
-
function mergeObject(target, source) {
|
|
33
|
+
function mergeObject(target, source, shouldRemoveNullObjectValues = true) {
|
|
31
34
|
const destination = {};
|
|
32
35
|
if (isMergeableObject(target)) {
|
|
33
36
|
// lodash adds a small overhead so we don't use it here
|
|
@@ -35,7 +38,14 @@ function mergeObject(target, source) {
|
|
|
35
38
|
const targetKeys = Object.keys(target);
|
|
36
39
|
for (let i = 0; i < targetKeys.length; ++i) {
|
|
37
40
|
const key = targetKeys[i];
|
|
38
|
-
|
|
41
|
+
|
|
42
|
+
// If shouldRemoveNullObjectValues is true, we want to remove null values from the merged object
|
|
43
|
+
const isSourceOrTargetNull = target[key] === null || source[key] === null;
|
|
44
|
+
const shouldOmitSourceKey = shouldRemoveNullObjectValues && isSourceOrTargetNull;
|
|
45
|
+
|
|
46
|
+
if (!shouldOmitSourceKey) {
|
|
47
|
+
destination[key] = target[key];
|
|
48
|
+
}
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
51
|
|
|
@@ -44,15 +54,24 @@ function mergeObject(target, source) {
|
|
|
44
54
|
const sourceKeys = Object.keys(source);
|
|
45
55
|
for (let i = 0; i < sourceKeys.length; ++i) {
|
|
46
56
|
const key = sourceKeys[i];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
|
|
58
|
+
// If shouldRemoveNullObjectValues is true, we want to remove null values from the merged object
|
|
59
|
+
const shouldOmitSourceKey = shouldRemoveNullObjectValues && source[key] === null;
|
|
60
|
+
|
|
61
|
+
// If we pass undefined as the updated value for a key, we want to generally ignore it
|
|
62
|
+
const isSourceKeyUndefined = source[key] === undefined;
|
|
63
|
+
|
|
64
|
+
if (!isSourceKeyUndefined && !shouldOmitSourceKey) {
|
|
65
|
+
const isSourceKeyMergable = isMergeableObject(source[key]);
|
|
66
|
+
|
|
67
|
+
if (isSourceKeyMergable && target[key]) {
|
|
68
|
+
if ((!shouldRemoveNullObjectValues || isSourceKeyMergable)) {
|
|
69
|
+
// eslint-disable-next-line no-use-before-define
|
|
70
|
+
destination[key] = fastMerge(target[key], source[key], shouldRemoveNullObjectValues);
|
|
71
|
+
}
|
|
72
|
+
} else if (!shouldRemoveNullObjectValues || source[key] !== null) {
|
|
73
|
+
destination[key] = source[key];
|
|
74
|
+
}
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
77
|
|
|
@@ -60,39 +79,33 @@ function mergeObject(target, source) {
|
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
/**
|
|
82
|
+
* Merges two objects and removes null values if "shouldRemoveNullObjectValues" is set to true
|
|
83
|
+
*
|
|
84
|
+
* We generally want to remove null values from objects written to disk and cache, because it decreases the amount of data stored in memory and on disk.
|
|
85
|
+
* On native, when merging an existing value with new changes, SQLite will use JSON_PATCH, which removes top-level nullish values.
|
|
86
|
+
* To be consistent with the behaviour for merge, we'll also want to remove null values for "set" operations.
|
|
87
|
+
*
|
|
63
88
|
* @param {Object|Array} target
|
|
64
89
|
* @param {Object|Array} source
|
|
90
|
+
* @param {Boolean} shouldRemoveNullObjectValues
|
|
65
91
|
* @returns {Object|Array}
|
|
66
92
|
*/
|
|
67
|
-
function fastMerge(target, source) {
|
|
93
|
+
function fastMerge(target, source, shouldRemoveNullObjectValues = true) {
|
|
68
94
|
// We have to ignore arrays and nullish values here,
|
|
69
95
|
// otherwise "mergeObject" will throw an error,
|
|
70
96
|
// because it expects an object as "source"
|
|
71
|
-
if (_.isArray(source) ||
|
|
97
|
+
if (_.isArray(source) || source === null || source === undefined) {
|
|
72
98
|
return source;
|
|
73
99
|
}
|
|
74
|
-
return mergeObject(target, source);
|
|
100
|
+
return mergeObject(target, source, shouldRemoveNullObjectValues);
|
|
75
101
|
}
|
|
76
102
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* To be consistent with the behaviour for merge, we'll also want to remove nullish values for "set" operations.
|
|
81
|
-
* On web, IndexedDB will keep the top-level keys along with a null value and this uses up storage and memory.
|
|
82
|
-
* This method will ensure that keys for null values are removed before an object is written to disk and cache so that all platforms are storing the data in the same efficient way.
|
|
83
|
-
* @private
|
|
84
|
-
* @param {*} value
|
|
85
|
-
* @returns {*}
|
|
86
|
-
*/
|
|
87
|
-
function removeNullObjectValues(value) {
|
|
88
|
-
if (_.isArray(value) || !_.isObject(value)) {
|
|
89
|
-
return value;
|
|
103
|
+
function removeNestedNullValues(value) {
|
|
104
|
+
if (typeof value === 'object' && !_.isArray(value)) {
|
|
105
|
+
return fastMerge(value, value);
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return objectWithoutNullObjectValues;
|
|
108
|
+
return value;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
|
-
export default {
|
|
98
|
-
|
|
111
|
+
export default {areObjectsEmpty, fastMerge, removeNestedNullValues};
|