react-native-onyx 1.0.94 → 1.0.95
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 +52 -39
- 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.d.ts +5 -9
- package/lib/Onyx.js +18 -9
- package/lib/storage/providers/IDBKeyVal.js +1 -1
- package/lib/types.d.ts +38 -19
- package/lib/utils.js +32 -28
- package/package.json +1 -1
package/lib/Onyx.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {Component} from 'react';
|
|
2
|
-
import {PartialDeep} from 'type-fest';
|
|
3
2
|
import * as Logger from './Logger';
|
|
4
|
-
import {CollectionKey, CollectionKeyBase, DeepRecord, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey
|
|
3
|
+
import {CollectionKey, CollectionKeyBase, DeepRecord, KeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey} from './types';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Represents a mapping object where each `OnyxKey` maps to either a value of its corresponding type in `KeyValueMapping` or `null`.
|
|
@@ -79,14 +78,14 @@ type OnyxUpdate =
|
|
|
79
78
|
| {
|
|
80
79
|
onyxMethod: typeof METHOD.MERGE;
|
|
81
80
|
key: TKey;
|
|
82
|
-
value:
|
|
81
|
+
value: NullishDeep<KeyValueMapping[TKey]>;
|
|
83
82
|
};
|
|
84
83
|
}[OnyxKey]
|
|
85
84
|
| {
|
|
86
85
|
[TKey in CollectionKeyBase]: {
|
|
87
86
|
onyxMethod: typeof METHOD.MERGE_COLLECTION;
|
|
88
87
|
key: TKey;
|
|
89
|
-
value: Record<`${TKey}${string}`,
|
|
88
|
+
value: Record<`${TKey}${string}`, NullishDeep<KeyValueMapping[TKey]>>;
|
|
90
89
|
};
|
|
91
90
|
}[CollectionKeyBase];
|
|
92
91
|
|
|
@@ -202,7 +201,7 @@ declare function multiSet(data: Partial<NullableKeyValueMapping>): Promise<void>
|
|
|
202
201
|
* @param key ONYXKEYS key
|
|
203
202
|
* @param value Object or Array value to merge
|
|
204
203
|
*/
|
|
205
|
-
declare function merge<TKey extends OnyxKey>(key: TKey, value:
|
|
204
|
+
declare function merge<TKey extends OnyxKey>(key: TKey, value: NullishDeep<KeyValueMapping[TKey]>): Promise<void>;
|
|
206
205
|
|
|
207
206
|
/**
|
|
208
207
|
* Clear out all the data in the store
|
|
@@ -244,10 +243,7 @@ declare function clear(keysToPreserve?: OnyxKey[]): Promise<void>;
|
|
|
244
243
|
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
|
|
245
244
|
* @param collection Object collection keyed by individual collection member keys and values
|
|
246
245
|
*/
|
|
247
|
-
declare function mergeCollection<TKey extends CollectionKeyBase, TMap>(
|
|
248
|
-
collectionKey: TKey,
|
|
249
|
-
collection: Collection<TKey, TMap, PartialDeep<KeyValueMapping[TKey]>>,
|
|
250
|
-
): Promise<void>;
|
|
246
|
+
declare function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: Collection<TKey, TMap, NullishDeep<KeyValueMapping[TKey]>>): Promise<void>;
|
|
251
247
|
|
|
252
248
|
/**
|
|
253
249
|
* Insert API responses and lifecycle data into Onyx
|
package/lib/Onyx.js
CHANGED
|
@@ -1041,7 +1041,13 @@ function set(key, value) {
|
|
|
1041
1041
|
Logger.logAlert(`Onyx.set() called after Onyx.merge() for key: ${key}. It is recommended to use set() or merge() not both.`);
|
|
1042
1042
|
}
|
|
1043
1043
|
|
|
1044
|
-
|
|
1044
|
+
// We can remove all null values in an object by merging it with itself
|
|
1045
|
+
// utils.fastMerge recursively goes through the object and removes all null values
|
|
1046
|
+
// Passing two identical objects as source and target to fastMerge will not change it, but only remove the null values
|
|
1047
|
+
let valueWithNullRemoved = value;
|
|
1048
|
+
if (typeof value === 'object' && !_.isArray(value)) {
|
|
1049
|
+
valueWithNullRemoved = utils.fastMerge(value, value);
|
|
1050
|
+
}
|
|
1045
1051
|
|
|
1046
1052
|
const hasChanged = cache.hasValueChanged(key, valueWithNullRemoved);
|
|
1047
1053
|
|
|
@@ -1098,9 +1104,10 @@ function multiSet(data) {
|
|
|
1098
1104
|
* @private
|
|
1099
1105
|
* @param {*} existingValue
|
|
1100
1106
|
* @param {Array<*>} changes Array of changes that should be applied to the existing value
|
|
1107
|
+
* @param {Boolean} shouldRemoveNullObjectValues
|
|
1101
1108
|
* @returns {*}
|
|
1102
1109
|
*/
|
|
1103
|
-
function applyMerge(existingValue, changes) {
|
|
1110
|
+
function applyMerge(existingValue, changes, shouldRemoveNullObjectValues) {
|
|
1104
1111
|
const lastChange = _.last(changes);
|
|
1105
1112
|
|
|
1106
1113
|
if (_.isArray(lastChange)) {
|
|
@@ -1109,7 +1116,7 @@ function applyMerge(existingValue, changes) {
|
|
|
1109
1116
|
|
|
1110
1117
|
if (_.some(changes, _.isObject)) {
|
|
1111
1118
|
// Object values are then merged one after the other
|
|
1112
|
-
return _.reduce(changes, (modifiedData, change) => utils.fastMerge(modifiedData, change),
|
|
1119
|
+
return _.reduce(changes, (modifiedData, change) => utils.fastMerge(modifiedData, change, shouldRemoveNullObjectValues),
|
|
1113
1120
|
existingValue || {});
|
|
1114
1121
|
}
|
|
1115
1122
|
|
|
@@ -1157,7 +1164,8 @@ function merge(key, changes) {
|
|
|
1157
1164
|
.then((existingValue) => {
|
|
1158
1165
|
try {
|
|
1159
1166
|
// 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
|
-
|
|
1167
|
+
// We don't want to remove null values from the "batchedChanges", because SQLite uses them to remove keys from storage natively.
|
|
1168
|
+
let batchedChanges = applyMerge(undefined, mergeQueue[key], false);
|
|
1161
1169
|
|
|
1162
1170
|
if (_.isNull(batchedChanges)) {
|
|
1163
1171
|
return remove(key);
|
|
@@ -1172,15 +1180,16 @@ function merge(key, changes) {
|
|
|
1172
1180
|
delete mergeQueuePromise[key];
|
|
1173
1181
|
|
|
1174
1182
|
// After that we merge the batched changes with the existing value
|
|
1175
|
-
|
|
1176
|
-
|
|
1183
|
+
// We can remove null values from the "modifiedData", because "null" implicates that the user wants to remove a value from storage.
|
|
1184
|
+
// The "modifiedData" will be directly "set" in storage instead of being merged
|
|
1185
|
+
const modifiedData = shouldOverwriteExistingValue ? batchedChanges : applyMerge(existingValue, [batchedChanges], true);
|
|
1177
1186
|
|
|
1178
1187
|
// On native platforms we use SQLite which utilises JSON_PATCH to merge changes.
|
|
1179
1188
|
// 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
|
|
1181
|
-
// Therefore we need to remove
|
|
1189
|
+
// 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.
|
|
1190
|
+
// Therefore we need to remove null values from the `batchedChanges` which are sent to the SQLite, if no existing value is present.
|
|
1182
1191
|
if (!existingValue) {
|
|
1183
|
-
batchedChanges =
|
|
1192
|
+
batchedChanges = applyMerge(undefined, mergeQueue[key], true);
|
|
1184
1193
|
}
|
|
1185
1194
|
|
|
1186
1195
|
const hasChanged = cache.hasValueChanged(key, modifiedData);
|
|
@@ -56,7 +56,7 @@ const provider = {
|
|
|
56
56
|
const upsertMany = _.map(pairs, ([key, value], index) => {
|
|
57
57
|
const prev = values[index];
|
|
58
58
|
const newValue = _.isObject(prev) ? utils.fastMerge(prev, value) : value;
|
|
59
|
-
return promisifyRequest(store.put(
|
|
59
|
+
return promisifyRequest(store.put(newValue, key));
|
|
60
60
|
});
|
|
61
61
|
return Promise.all(upsertMany);
|
|
62
62
|
});
|
package/lib/types.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {Merge} from 'type-fest';
|
|
2
|
+
import {BuiltIns} from 'type-fest/source/internal';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Represents a deeply nested record. It maps keys to values,
|
|
5
6
|
* and those values can either be of type `TValue` or further nested `DeepRecord` instances.
|
|
6
7
|
*/
|
|
7
|
-
type DeepRecord<TKey extends string | number | symbol, TValue> = {
|
|
8
|
+
type DeepRecord<TKey extends string | number | symbol, TValue> = {
|
|
9
|
+
[key: string]: TValue | DeepRecord<TKey, TValue>;
|
|
10
|
+
};
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Represents type options to configure all Onyx methods.
|
|
@@ -180,26 +183,42 @@ type OnyxEntry<TOnyxValue> = TOnyxValue | null;
|
|
|
180
183
|
*/
|
|
181
184
|
type OnyxCollection<TOnyxValue> = OnyxEntry<Record<string, TOnyxValue | null>>;
|
|
182
185
|
|
|
186
|
+
type NonTransformableTypes =
|
|
187
|
+
| BuiltIns
|
|
188
|
+
| ((...args: any[]) => unknown)
|
|
189
|
+
| Map<unknown, unknown>
|
|
190
|
+
| Set<unknown>
|
|
191
|
+
| ReadonlyMap<unknown, unknown>
|
|
192
|
+
| ReadonlySet<unknown>
|
|
193
|
+
| unknown[]
|
|
194
|
+
| readonly unknown[];
|
|
195
|
+
|
|
183
196
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
197
|
+
* Create a type from another type with all keys and nested keys set to optional or null.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* const settings: Settings = {
|
|
201
|
+
* textEditor: {
|
|
202
|
+
* fontSize: 14;
|
|
203
|
+
* fontColor: '#000000';
|
|
204
|
+
* fontWeight: 400;
|
|
205
|
+
* }
|
|
206
|
+
* autosave: true;
|
|
207
|
+
* };
|
|
186
208
|
*
|
|
187
|
-
*
|
|
209
|
+
* const applySavedSettings = (savedSettings: NullishDeep<Settings>) => {
|
|
210
|
+
* return {...settings, ...savedSettings};
|
|
211
|
+
* }
|
|
212
|
+
*
|
|
213
|
+
* settings = applySavedSettings({textEditor: {fontWeight: 500, fontColor: null}});
|
|
188
214
|
*/
|
|
189
|
-
type
|
|
190
|
-
[P in keyof T]: T[P] | null;
|
|
191
|
-
};
|
|
215
|
+
type NullishDeep<T> = T extends NonTransformableTypes ? T : T extends object ? NullishObjectDeep<T> : unknown;
|
|
192
216
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
Key,
|
|
199
|
-
KeyValueMapping,
|
|
200
|
-
OnyxCollection,
|
|
201
|
-
OnyxEntry,
|
|
202
|
-
OnyxKey,
|
|
203
|
-
Selector,
|
|
204
|
-
NullableProperties,
|
|
217
|
+
/**
|
|
218
|
+
Same as `NullishDeep`, but accepts only `object`s as inputs. Internal helper for `NullishDeep`.
|
|
219
|
+
*/
|
|
220
|
+
type NullishObjectDeep<ObjectType extends object> = {
|
|
221
|
+
[KeyType in keyof ObjectType]?: NullishDeep<ObjectType[KeyType]> | null;
|
|
205
222
|
};
|
|
223
|
+
|
|
224
|
+
export {CollectionKey, CollectionKeyBase, CustomTypeOptions, DeepRecord, Key, KeyValueMapping, OnyxCollection, OnyxEntry, OnyxKey, Selector, NullishDeep};
|
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 (
|
|
@@ -25,9 +25,12 @@ function isMergeableObject(val) {
|
|
|
25
25
|
/**
|
|
26
26
|
* @param {Object} target
|
|
27
27
|
* @param {Object} source
|
|
28
|
+
* @param {Boolean} shouldRemoveNullObjectValues
|
|
28
29
|
* @returns {Object}
|
|
29
30
|
*/
|
|
30
|
-
function mergeObject(target, source) {
|
|
31
|
+
function mergeObject(target, source, shouldRemoveNullObjectValues = true) {
|
|
32
|
+
const targetAndSourceIdentical = target === source;
|
|
33
|
+
|
|
31
34
|
const destination = {};
|
|
32
35
|
if (isMergeableObject(target)) {
|
|
33
36
|
// lodash adds a small overhead so we don't use it here
|
|
@@ -35,6 +38,13 @@ 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];
|
|
41
|
+
|
|
42
|
+
// If shouldRemoveNullObjectValues is true, we want to remove null values from the merged object
|
|
43
|
+
if (shouldRemoveNullObjectValues && (target[key] === null || source[key] === null)) {
|
|
44
|
+
// eslint-disable-next-line no-continue
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
destination[key] = target[key];
|
|
39
49
|
}
|
|
40
50
|
}
|
|
@@ -44,15 +54,22 @@ 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
|
-
|
|
57
|
+
|
|
58
|
+
// If shouldRemoveNullObjectValues is true, we want to remove null values from the merged object
|
|
59
|
+
if (shouldRemoveNullObjectValues && source[key] === null) {
|
|
48
60
|
// eslint-disable-next-line no-continue
|
|
49
61
|
continue;
|
|
50
62
|
}
|
|
63
|
+
|
|
51
64
|
if (!isMergeableObject(source[key]) || !target[key]) {
|
|
65
|
+
if (targetAndSourceIdentical) {
|
|
66
|
+
// eslint-disable-next-line no-continue
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
52
69
|
destination[key] = source[key];
|
|
53
70
|
} else {
|
|
54
71
|
// eslint-disable-next-line no-use-before-define
|
|
55
|
-
destination[key] = fastMerge(target[key], source[key]);
|
|
72
|
+
destination[key] = fastMerge(target[key], source[key], shouldRemoveNullObjectValues);
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
75
|
|
|
@@ -60,39 +77,26 @@ function mergeObject(target, source) {
|
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
/**
|
|
80
|
+
* Merges two objects and removes null values if "shouldRemoveNullObjectValues" is set to true
|
|
81
|
+
*
|
|
82
|
+
* 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.
|
|
83
|
+
* On native, when merging an existing value with new changes, SQLite will use JSON_PATCH, which removes top-level nullish values.
|
|
84
|
+
* To be consistent with the behaviour for merge, we'll also want to remove null values for "set" operations.
|
|
85
|
+
*
|
|
63
86
|
* @param {Object|Array} target
|
|
64
87
|
* @param {Object|Array} source
|
|
88
|
+
* @param {Boolean} shouldRemoveNullObjectValues
|
|
65
89
|
* @returns {Object|Array}
|
|
66
90
|
*/
|
|
67
|
-
function fastMerge(target, source) {
|
|
91
|
+
function fastMerge(target, source, shouldRemoveNullObjectValues = true) {
|
|
68
92
|
// We have to ignore arrays and nullish values here,
|
|
69
93
|
// otherwise "mergeObject" will throw an error,
|
|
70
94
|
// because it expects an object as "source"
|
|
71
|
-
if (_.isArray(source) ||
|
|
95
|
+
if (_.isArray(source) || source === null || source === undefined) {
|
|
72
96
|
return source;
|
|
73
97
|
}
|
|
74
|
-
return mergeObject(target, source);
|
|
75
|
-
}
|
|
76
|
-
|
|
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;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const objectWithoutNullObjectValues = _.omit(value, objectValue => _.isNull(objectValue));
|
|
93
|
-
|
|
94
|
-
return objectWithoutNullObjectValues;
|
|
98
|
+
return mergeObject(target, source, shouldRemoveNullObjectValues);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
export default {
|
|
101
|
+
export default {areObjectsEmpty, fastMerge};
|
|
98
102
|
|