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/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
- if (_.isNull(value)) {
1037
- return remove(key);
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 valueWithNullRemoved = utils.removeNullObjectValues(value);
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, valueWithNullRemoved, hasChanged, 'set');
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, valueWithNullRemoved)
1057
- .catch(error => evictStorageAndRetry(error, set, key, valueWithNullRemoved))
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
- return Storage.multiSet(keyValuePairs)
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
- let batchedChanges = applyMerge(undefined, mergeQueue[key]);
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
- return remove(key);
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
- const updatedValue = shouldOverwriteExistingValue ? batchedChanges : applyMerge(existingValue, [batchedChanges]);
1176
- const modifiedData = utils.removeNullObjectValues(updatedValue);
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 top-level nullish values from the stored object.
1180
- // When there is no existing value though, SQLite will just insert the changes as a new value and thus the top-level nullish values won't be removed.
1181
- // Therefore we need to remove nullish values from the `batchedChanges` which are sent to the SQLite, if no existing value is present.
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 = utils.removeNullObjectValues(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 = _.isObject(existingValue)
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 = _.isObject(prev) ? utils.fastMerge(prev, value) : value;
59
- return promisifyRequest(store.put(utils.removeNullObjectValues(newValue), key));
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 * as _ from 'underscore';
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
- && Object.prototype.toString.call(val) !== '[object RegExp]'
22
- && Object.prototype.toString.call(val) !== '[object Date]');
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
- destination[key] = target[key];
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
- if (source[key] === undefined) {
48
- // eslint-disable-next-line no-continue
49
- continue;
50
- }
51
- if (!isMergeableObject(source[key]) || !target[key]) {
52
- destination[key] = source[key];
53
- } else {
54
- // eslint-disable-next-line no-use-before-define
55
- destination[key] = fastMerge(target[key], source[key]);
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) || _.isNull(source) || _.isUndefined(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
- * We generally want to remove top-level nullish values from objects written to disk and cache, because it decreases the amount of data stored in memory and on disk.
79
- * On native, when merging an existing value with new changes, SQLite will use JSON_PATCH, which removes top-level nullish values.
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
- const objectWithoutNullObjectValues = _.omit(value, objectValue => _.isNull(objectValue));
93
-
94
- return objectWithoutNullObjectValues;
108
+ return value;
95
109
  }
96
110
 
97
- export default {removeNullObjectValues, areObjectsEmpty, fastMerge};
98
-
111
+ export default {areObjectsEmpty, fastMerge, removeNestedNullValues};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "1.0.98",
3
+ "version": "1.0.100",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",