react-native-onyx 2.0.117 → 2.0.118
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/API.md +2 -2
- package/dist/Onyx.d.ts +1 -1
- package/dist/Onyx.js +36 -152
- package/dist/OnyxCache.js +4 -1
- package/dist/OnyxMerge/index.d.ts +5 -0
- package/dist/OnyxMerge/index.js +30 -0
- package/dist/OnyxMerge/index.native.d.ts +5 -0
- package/dist/OnyxMerge/index.native.js +37 -0
- package/dist/OnyxMerge/types.d.ts +7 -0
- package/dist/OnyxMerge/types.js +2 -0
- package/dist/OnyxUtils.d.ts +36 -21
- package/dist/OnyxUtils.js +175 -45
- package/dist/storage/InstanceSync/index.web.d.ts +2 -2
- package/dist/storage/__mocks__/index.d.ts +9 -9
- package/dist/storage/index.js +2 -2
- package/dist/storage/providers/IDBKeyValProvider.js +8 -5
- package/dist/storage/providers/MemoryOnlyProvider.js +8 -5
- package/dist/storage/providers/NoopProvider.js +1 -1
- package/dist/storage/providers/SQLiteProvider.js +56 -22
- package/dist/storage/providers/types.d.ts +20 -22
- package/dist/types.d.ts +10 -1
- package/dist/utils.d.ts +34 -6
- package/dist/utils.js +92 -64
- package/package.json +1 -1
package/dist/OnyxUtils.js
CHANGED
|
@@ -31,6 +31,7 @@ exports.clearOnyxUtilsInternals = void 0;
|
|
|
31
31
|
const fast_equals_1 = require("fast-equals");
|
|
32
32
|
const clone_1 = __importDefault(require("lodash/clone"));
|
|
33
33
|
const pick_1 = __importDefault(require("lodash/pick"));
|
|
34
|
+
const underscore_1 = __importDefault(require("underscore"));
|
|
34
35
|
const DevTools_1 = __importDefault(require("./DevTools"));
|
|
35
36
|
const Logger = __importStar(require("./Logger"));
|
|
36
37
|
const OnyxCache_1 = __importStar(require("./OnyxCache"));
|
|
@@ -42,6 +43,7 @@ const utils_1 = __importDefault(require("./utils"));
|
|
|
42
43
|
const createDeferredTask_1 = __importDefault(require("./createDeferredTask"));
|
|
43
44
|
const GlobalSettings = __importStar(require("./GlobalSettings"));
|
|
44
45
|
const metrics_1 = __importDefault(require("./metrics"));
|
|
46
|
+
const logMessages_1 = __importDefault(require("./logMessages"));
|
|
45
47
|
// Method constants
|
|
46
48
|
const METHOD = {
|
|
47
49
|
SET: 'set',
|
|
@@ -285,7 +287,7 @@ function multiGet(keys) {
|
|
|
285
287
|
values.forEach(([key, value]) => {
|
|
286
288
|
if (skippableCollectionMemberIDs.size) {
|
|
287
289
|
try {
|
|
288
|
-
const [, collectionMemberID] =
|
|
290
|
+
const [, collectionMemberID] = splitCollectionMemberKey(key);
|
|
289
291
|
if (skippableCollectionMemberIDs.has(collectionMemberID)) {
|
|
290
292
|
// The key is a skippable one, so we skip this iteration.
|
|
291
293
|
return;
|
|
@@ -309,7 +311,7 @@ function multiGet(keys) {
|
|
|
309
311
|
* Note: just using `.map`, you'd end up with `Array<OnyxCollection<Report>|OnyxEntry<string>>`, which is not what we want. This preserves the order of the keys provided.
|
|
310
312
|
*/
|
|
311
313
|
function tupleGet(keys) {
|
|
312
|
-
return Promise.all(keys.map((key) =>
|
|
314
|
+
return Promise.all(keys.map((key) => get(key)));
|
|
313
315
|
}
|
|
314
316
|
/**
|
|
315
317
|
* Stores a subscription ID associated with a given key.
|
|
@@ -955,59 +957,75 @@ function broadcastUpdate(key, value, hasChanged) {
|
|
|
955
957
|
function hasPendingMergeForKey(key) {
|
|
956
958
|
return !!mergeQueue[key];
|
|
957
959
|
}
|
|
958
|
-
/**
|
|
959
|
-
* Removes a key from storage if the value is null.
|
|
960
|
-
* Otherwise removes all nested null values in objects,
|
|
961
|
-
* if shouldRemoveNestedNulls is true and returns the object.
|
|
962
|
-
*
|
|
963
|
-
* @returns The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely
|
|
964
|
-
*/
|
|
965
|
-
function removeNullValues(key, value, shouldRemoveNestedNulls = true) {
|
|
966
|
-
if (value === null) {
|
|
967
|
-
remove(key);
|
|
968
|
-
return { value, wasRemoved: true };
|
|
969
|
-
}
|
|
970
|
-
if (value === undefined) {
|
|
971
|
-
return { value, wasRemoved: false };
|
|
972
|
-
}
|
|
973
|
-
// We can remove all null values in an object by merging it with itself
|
|
974
|
-
// utils.fastMerge recursively goes through the object and removes all null values
|
|
975
|
-
// Passing two identical objects as source and target to fastMerge will not change it, but only remove the null values
|
|
976
|
-
return { value: shouldRemoveNestedNulls ? utils_1.default.removeNestedNullValues(value) : value, wasRemoved: false };
|
|
977
|
-
}
|
|
978
960
|
/**
|
|
979
961
|
* Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
|
|
980
962
|
* This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
|
|
981
963
|
* to an array of key-value pairs in the above format and removes key-value pairs that are being set to null
|
|
982
|
-
|
|
983
|
-
* @return an array of key - value pairs <[key, value]>
|
|
964
|
+
*
|
|
965
|
+
* @return an array of key - value pairs <[key, value]>
|
|
984
966
|
*/
|
|
985
|
-
function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
if (
|
|
989
|
-
|
|
967
|
+
function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls, replaceNullPatches) {
|
|
968
|
+
const pairs = [];
|
|
969
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
970
|
+
if (value === null) {
|
|
971
|
+
remove(key);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const valueWithoutNestedNullValues = (shouldRemoveNestedNulls !== null && shouldRemoveNestedNulls !== void 0 ? shouldRemoveNestedNulls : true) ? utils_1.default.removeNestedNullValues(value) : value;
|
|
975
|
+
if (valueWithoutNestedNullValues !== undefined) {
|
|
976
|
+
pairs.push([key, valueWithoutNestedNullValues, replaceNullPatches === null || replaceNullPatches === void 0 ? void 0 : replaceNullPatches[key]]);
|
|
990
977
|
}
|
|
991
|
-
|
|
992
|
-
|
|
978
|
+
});
|
|
979
|
+
return pairs;
|
|
993
980
|
}
|
|
994
981
|
/**
|
|
995
|
-
* Merges an array of changes with an existing value
|
|
982
|
+
* Merges an array of changes with an existing value or creates a single change.
|
|
996
983
|
*
|
|
997
|
-
* @param changes Array of changes that should be
|
|
984
|
+
* @param changes Array of changes that should be merged
|
|
985
|
+
* @param existingValue The existing value that should be merged with the changes
|
|
998
986
|
*/
|
|
999
|
-
function
|
|
987
|
+
function mergeChanges(changes, existingValue) {
|
|
988
|
+
return mergeInternal('merge', changes, existingValue);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Merges an array of changes with an existing value or creates a single change.
|
|
992
|
+
* It will also mark deep nested objects that need to be entirely replaced during the merge.
|
|
993
|
+
*
|
|
994
|
+
* @param changes Array of changes that should be merged
|
|
995
|
+
* @param existingValue The existing value that should be merged with the changes
|
|
996
|
+
*/
|
|
997
|
+
function mergeAndMarkChanges(changes, existingValue) {
|
|
998
|
+
return mergeInternal('mark', changes, existingValue);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Merges an array of changes with an existing value or creates a single change.
|
|
1002
|
+
*
|
|
1003
|
+
* @param changes Array of changes that should be merged
|
|
1004
|
+
* @param existingValue The existing value that should be merged with the changes
|
|
1005
|
+
*/
|
|
1006
|
+
function mergeInternal(mode, changes, existingValue) {
|
|
1000
1007
|
const lastChange = changes === null || changes === void 0 ? void 0 : changes.at(-1);
|
|
1001
1008
|
if (Array.isArray(lastChange)) {
|
|
1002
|
-
return lastChange;
|
|
1009
|
+
return { result: lastChange, replaceNullPatches: [] };
|
|
1003
1010
|
}
|
|
1004
1011
|
if (changes.some((change) => change && typeof change === 'object')) {
|
|
1005
1012
|
// Object values are then merged one after the other
|
|
1006
|
-
return changes.reduce((modifiedData, change) =>
|
|
1013
|
+
return changes.reduce((modifiedData, change) => {
|
|
1014
|
+
const options = mode === 'merge' ? { shouldRemoveNestedNulls: true, objectRemovalMode: 'replace' } : { objectRemovalMode: 'mark' };
|
|
1015
|
+
const { result, replaceNullPatches } = utils_1.default.fastMerge(modifiedData.result, change, options);
|
|
1016
|
+
// eslint-disable-next-line no-param-reassign
|
|
1017
|
+
modifiedData.result = result;
|
|
1018
|
+
// eslint-disable-next-line no-param-reassign
|
|
1019
|
+
modifiedData.replaceNullPatches = [...modifiedData.replaceNullPatches, ...replaceNullPatches];
|
|
1020
|
+
return modifiedData;
|
|
1021
|
+
}, {
|
|
1022
|
+
result: (existingValue !== null && existingValue !== void 0 ? existingValue : {}),
|
|
1023
|
+
replaceNullPatches: [],
|
|
1024
|
+
});
|
|
1007
1025
|
}
|
|
1008
1026
|
// If we have anything else we can't merge it so we'll
|
|
1009
1027
|
// simply return the last value that was queued
|
|
1010
|
-
return lastChange;
|
|
1028
|
+
return { result: lastChange, replaceNullPatches: [] };
|
|
1011
1029
|
}
|
|
1012
1030
|
/**
|
|
1013
1031
|
* Merge user provided default key value pairs.
|
|
@@ -1015,7 +1033,9 @@ function applyMerge(existingValue, changes, shouldRemoveNestedNulls) {
|
|
|
1015
1033
|
function initializeWithDefaultKeyStates() {
|
|
1016
1034
|
return storage_1.default.multiGet(Object.keys(defaultKeyStates)).then((pairs) => {
|
|
1017
1035
|
const existingDataAsObject = Object.fromEntries(pairs);
|
|
1018
|
-
const merged = utils_1.default.fastMerge(existingDataAsObject, defaultKeyStates
|
|
1036
|
+
const merged = utils_1.default.fastMerge(existingDataAsObject, defaultKeyStates, {
|
|
1037
|
+
shouldRemoveNestedNulls: true,
|
|
1038
|
+
}).result;
|
|
1019
1039
|
OnyxCache_1.default.merge(merged !== null && merged !== void 0 ? merged : {});
|
|
1020
1040
|
Object.entries(merged !== null && merged !== void 0 ? merged : {}).forEach(([key, value]) => keyChanged(key, value, existingDataAsObject));
|
|
1021
1041
|
});
|
|
@@ -1157,11 +1177,11 @@ function unsubscribeFromKey(subscriptionID) {
|
|
|
1157
1177
|
delete callbackToStateMapping[subscriptionID];
|
|
1158
1178
|
}
|
|
1159
1179
|
function updateSnapshots(data, mergeFn) {
|
|
1160
|
-
const snapshotCollectionKey =
|
|
1180
|
+
const snapshotCollectionKey = getSnapshotKey();
|
|
1161
1181
|
if (!snapshotCollectionKey)
|
|
1162
1182
|
return [];
|
|
1163
1183
|
const promises = [];
|
|
1164
|
-
const snapshotCollection =
|
|
1184
|
+
const snapshotCollection = getCachedCollection(snapshotCollectionKey);
|
|
1165
1185
|
Object.entries(snapshotCollection).forEach(([snapshotEntryKey, snapshotEntryValue]) => {
|
|
1166
1186
|
// Snapshots may not be present in cache. We don't know how to update them so we skip.
|
|
1167
1187
|
if (!snapshotEntryValue) {
|
|
@@ -1170,7 +1190,7 @@ function updateSnapshots(data, mergeFn) {
|
|
|
1170
1190
|
let updatedData = {};
|
|
1171
1191
|
data.forEach(({ key, value }) => {
|
|
1172
1192
|
// snapshots are normal keys so we want to skip update if they are written to Onyx
|
|
1173
|
-
if (
|
|
1193
|
+
if (isCollectionMemberKey(snapshotCollectionKey, key)) {
|
|
1174
1194
|
return;
|
|
1175
1195
|
}
|
|
1176
1196
|
if (typeof snapshotEntryValue !== 'object' || !('data' in snapshotEntryValue)) {
|
|
@@ -1209,6 +1229,115 @@ function updateSnapshots(data, mergeFn) {
|
|
|
1209
1229
|
});
|
|
1210
1230
|
return promises;
|
|
1211
1231
|
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Merges a collection based on their keys.
|
|
1234
|
+
* Serves as core implementation for `Onyx.mergeCollection()` public function, the difference being
|
|
1235
|
+
* that this internal function allows passing an additional `mergeReplaceNullPatches` parameter.
|
|
1236
|
+
*
|
|
1237
|
+
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
|
|
1238
|
+
* @param collection Object collection keyed by individual collection member keys and values
|
|
1239
|
+
* @param mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
|
|
1240
|
+
* tuples that we'll use to replace the nested objects of that collection member record with something else.
|
|
1241
|
+
*/
|
|
1242
|
+
function mergeCollectionWithPatches(collectionKey, collection, mergeReplaceNullPatches) {
|
|
1243
|
+
if (!isValidNonEmptyCollectionForMerge(collection)) {
|
|
1244
|
+
Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
|
|
1245
|
+
return Promise.resolve();
|
|
1246
|
+
}
|
|
1247
|
+
let resultCollection = collection;
|
|
1248
|
+
let resultCollectionKeys = Object.keys(resultCollection);
|
|
1249
|
+
// Confirm all the collection keys belong to the same parent
|
|
1250
|
+
if (!doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) {
|
|
1251
|
+
return Promise.resolve();
|
|
1252
|
+
}
|
|
1253
|
+
if (skippableCollectionMemberIDs.size) {
|
|
1254
|
+
resultCollection = resultCollectionKeys.reduce((result, key) => {
|
|
1255
|
+
try {
|
|
1256
|
+
const [, collectionMemberID] = splitCollectionMemberKey(key, collectionKey);
|
|
1257
|
+
// If the collection member key is a skippable one we set its value to null.
|
|
1258
|
+
// eslint-disable-next-line no-param-reassign
|
|
1259
|
+
result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null;
|
|
1260
|
+
}
|
|
1261
|
+
catch (_a) {
|
|
1262
|
+
// Something went wrong during split, so we assign the data to result anyway.
|
|
1263
|
+
// eslint-disable-next-line no-param-reassign
|
|
1264
|
+
result[key] = resultCollection[key];
|
|
1265
|
+
}
|
|
1266
|
+
return result;
|
|
1267
|
+
}, {});
|
|
1268
|
+
}
|
|
1269
|
+
resultCollectionKeys = Object.keys(resultCollection);
|
|
1270
|
+
return getAllKeys()
|
|
1271
|
+
.then((persistedKeys) => {
|
|
1272
|
+
// Split to keys that exist in storage and keys that don't
|
|
1273
|
+
const keys = resultCollectionKeys.filter((key) => {
|
|
1274
|
+
if (resultCollection[key] === null) {
|
|
1275
|
+
remove(key);
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
return true;
|
|
1279
|
+
});
|
|
1280
|
+
const existingKeys = keys.filter((key) => persistedKeys.has(key));
|
|
1281
|
+
const cachedCollectionForExistingKeys = getCachedCollection(collectionKey, existingKeys);
|
|
1282
|
+
const existingKeyCollection = existingKeys.reduce((obj, key) => {
|
|
1283
|
+
const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(resultCollection[key], cachedCollectionForExistingKeys[key]);
|
|
1284
|
+
if (!isCompatible) {
|
|
1285
|
+
Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'mergeCollection', existingValueType, newValueType));
|
|
1286
|
+
return obj;
|
|
1287
|
+
}
|
|
1288
|
+
// eslint-disable-next-line no-param-reassign
|
|
1289
|
+
obj[key] = resultCollection[key];
|
|
1290
|
+
return obj;
|
|
1291
|
+
}, {});
|
|
1292
|
+
const newCollection = {};
|
|
1293
|
+
keys.forEach((key) => {
|
|
1294
|
+
if (persistedKeys.has(key)) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
newCollection[key] = resultCollection[key];
|
|
1298
|
+
});
|
|
1299
|
+
// When (multi-)merging the values with the existing values in storage,
|
|
1300
|
+
// we don't want to remove nested null values from the data that we pass to the storage layer,
|
|
1301
|
+
// because the storage layer uses them to remove nested keys from storage natively.
|
|
1302
|
+
const keyValuePairsForExistingCollection = prepareKeyValuePairsForStorage(existingKeyCollection, false, mergeReplaceNullPatches);
|
|
1303
|
+
// We can safely remove nested null values when using (multi-)set,
|
|
1304
|
+
// because we will simply overwrite the existing values in storage.
|
|
1305
|
+
const keyValuePairsForNewCollection = prepareKeyValuePairsForStorage(newCollection, true);
|
|
1306
|
+
const promises = [];
|
|
1307
|
+
// We need to get the previously existing values so we can compare the new ones
|
|
1308
|
+
// against them, to avoid unnecessary subscriber updates.
|
|
1309
|
+
const previousCollectionPromise = Promise.all(existingKeys.map((key) => get(key).then((value) => [key, value]))).then(Object.fromEntries);
|
|
1310
|
+
// New keys will be added via multiSet while existing keys will be updated using multiMerge
|
|
1311
|
+
// This is because setting a key that doesn't exist yet with multiMerge will throw errors
|
|
1312
|
+
if (keyValuePairsForExistingCollection.length > 0) {
|
|
1313
|
+
promises.push(storage_1.default.multiMerge(keyValuePairsForExistingCollection));
|
|
1314
|
+
}
|
|
1315
|
+
if (keyValuePairsForNewCollection.length > 0) {
|
|
1316
|
+
promises.push(storage_1.default.multiSet(keyValuePairsForNewCollection));
|
|
1317
|
+
}
|
|
1318
|
+
// finalMergedCollection contains all the keys that were merged, without the keys of incompatible updates
|
|
1319
|
+
const finalMergedCollection = Object.assign(Object.assign({}, existingKeyCollection), newCollection);
|
|
1320
|
+
// Prefill cache if necessary by calling get() on any existing keys and then merge original data to cache
|
|
1321
|
+
// and update all subscribers
|
|
1322
|
+
const promiseUpdate = previousCollectionPromise.then((previousCollection) => {
|
|
1323
|
+
OnyxCache_1.default.merge(finalMergedCollection);
|
|
1324
|
+
return scheduleNotifyCollectionSubscribers(collectionKey, finalMergedCollection, previousCollection);
|
|
1325
|
+
});
|
|
1326
|
+
return Promise.all(promises)
|
|
1327
|
+
.catch((error) => evictStorageAndRetry(error, mergeCollectionWithPatches, collectionKey, resultCollection))
|
|
1328
|
+
.then(() => {
|
|
1329
|
+
sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection);
|
|
1330
|
+
return promiseUpdate;
|
|
1331
|
+
});
|
|
1332
|
+
})
|
|
1333
|
+
.then(() => undefined);
|
|
1334
|
+
}
|
|
1335
|
+
function logKeyChanged(onyxMethod, key, value, hasChanged) {
|
|
1336
|
+
Logger.logInfo(`${onyxMethod} called for key: ${key}${underscore_1.default.isObject(value) ? ` properties: ${underscore_1.default.keys(value).join(',')}` : ''} hasChanged: ${hasChanged}`);
|
|
1337
|
+
}
|
|
1338
|
+
function logKeyRemoved(onyxMethod, key) {
|
|
1339
|
+
Logger.logInfo(`${onyxMethod} called for key: ${key} => null passed, so key was removed`);
|
|
1340
|
+
}
|
|
1212
1341
|
/**
|
|
1213
1342
|
* Clear internal variables used in this file, useful in test environments.
|
|
1214
1343
|
*/
|
|
@@ -1252,9 +1381,9 @@ const OnyxUtils = {
|
|
|
1252
1381
|
evictStorageAndRetry,
|
|
1253
1382
|
broadcastUpdate,
|
|
1254
1383
|
hasPendingMergeForKey,
|
|
1255
|
-
removeNullValues,
|
|
1256
1384
|
prepareKeyValuePairsForStorage,
|
|
1257
|
-
|
|
1385
|
+
mergeChanges,
|
|
1386
|
+
mergeAndMarkChanges,
|
|
1258
1387
|
initializeWithDefaultKeyStates,
|
|
1259
1388
|
getSnapshotKey,
|
|
1260
1389
|
multiGet,
|
|
@@ -1270,6 +1399,9 @@ const OnyxUtils = {
|
|
|
1270
1399
|
addKeyToRecentlyAccessedIfNeeded,
|
|
1271
1400
|
reduceCollectionWithSelector,
|
|
1272
1401
|
updateSnapshots,
|
|
1402
|
+
mergeCollectionWithPatches,
|
|
1403
|
+
logKeyChanged,
|
|
1404
|
+
logKeyRemoved,
|
|
1273
1405
|
};
|
|
1274
1406
|
GlobalSettings.addGlobalSettingsChangeListener(({ enablePerformanceMetrics }) => {
|
|
1275
1407
|
if (!enablePerformanceMetrics) {
|
|
@@ -1289,8 +1421,6 @@ GlobalSettings.addGlobalSettingsChangeListener(({ enablePerformanceMetrics }) =>
|
|
|
1289
1421
|
// @ts-expect-error Reassign
|
|
1290
1422
|
getCollectionKeys = (0, metrics_1.default)(getCollectionKeys, 'OnyxUtils.getCollectionKeys');
|
|
1291
1423
|
// @ts-expect-error Reassign
|
|
1292
|
-
addEvictableKeysToRecentlyAccessedList = (0, metrics_1.default)(OnyxCache_1.default.addEvictableKeysToRecentlyAccessedList, 'OnyxCache.addEvictableKeysToRecentlyAccessedList');
|
|
1293
|
-
// @ts-expect-error Reassign
|
|
1294
1424
|
keysChanged = (0, metrics_1.default)(keysChanged, 'OnyxUtils.keysChanged');
|
|
1295
1425
|
// @ts-expect-error Reassign
|
|
1296
1426
|
keyChanged = (0, metrics_1.default)(keyChanged, 'OnyxUtils.keyChanged');
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* data changes and then stay up-to-date with everything happening in Onyx.
|
|
5
5
|
*/
|
|
6
6
|
import type { OnyxKey } from '../../types';
|
|
7
|
-
import type {
|
|
7
|
+
import type { StorageKeyList, OnStorageKeyChanged } from '../providers/types';
|
|
8
8
|
import type StorageProvider from '../providers/types';
|
|
9
9
|
/**
|
|
10
10
|
* Raise an event through `localStorage` to let other tabs know a value changed
|
|
11
11
|
* @param {String} onyxKey
|
|
12
12
|
*/
|
|
13
13
|
declare function raiseStorageSyncEvent(onyxKey: OnyxKey): void;
|
|
14
|
-
declare function raiseStorageSyncManyKeysEvent(onyxKeys:
|
|
14
|
+
declare function raiseStorageSyncManyKeysEvent(onyxKeys: StorageKeyList): void;
|
|
15
15
|
declare const InstanceSync: {
|
|
16
16
|
shouldBeUsed: boolean;
|
|
17
17
|
/**
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
declare const StorageMock: {
|
|
3
3
|
init: jest.Mock<void, [], any>;
|
|
4
4
|
getItem: jest.Mock<Promise<unknown>, [key: any], any>;
|
|
5
|
-
multiGet: jest.Mock<Promise<import("../providers/types").
|
|
6
|
-
setItem: jest.Mock<Promise<void
|
|
7
|
-
multiSet: jest.Mock<Promise<void
|
|
8
|
-
mergeItem: jest.Mock<Promise<void
|
|
9
|
-
multiMerge: jest.Mock<Promise<void
|
|
10
|
-
removeItem: jest.Mock<Promise<void
|
|
11
|
-
removeItems: jest.Mock<Promise<void
|
|
12
|
-
clear: jest.Mock<Promise<void
|
|
13
|
-
getAllKeys: jest.Mock<Promise<import("../providers/types").
|
|
5
|
+
multiGet: jest.Mock<Promise<import("../providers/types").StorageKeyValuePair[]>, [keys: import("../providers/types").StorageKeyList], any>;
|
|
6
|
+
setItem: jest.Mock<Promise<void>, [key: any, value: unknown], any>;
|
|
7
|
+
multiSet: jest.Mock<Promise<void>, [pairs: import("../providers/types").StorageKeyValuePair[]], any>;
|
|
8
|
+
mergeItem: jest.Mock<Promise<void>, [key: any, change: unknown, replaceNullPatches?: import("../../utils").FastMergeReplaceNullPatch[] | undefined], any>;
|
|
9
|
+
multiMerge: jest.Mock<Promise<void>, [pairs: import("../providers/types").StorageKeyValuePair[]], any>;
|
|
10
|
+
removeItem: jest.Mock<Promise<void>, [key: string], any>;
|
|
11
|
+
removeItems: jest.Mock<Promise<void>, [keys: import("../providers/types").StorageKeyList], any>;
|
|
12
|
+
clear: jest.Mock<Promise<void>, [], any>;
|
|
13
|
+
getAllKeys: jest.Mock<Promise<import("../providers/types").StorageKeyList>, [], any>;
|
|
14
14
|
getDatabaseSize: jest.Mock<Promise<{
|
|
15
15
|
bytesUsed: number;
|
|
16
16
|
bytesRemaining: number;
|
package/dist/storage/index.js
CHANGED
|
@@ -120,8 +120,8 @@ const storage = {
|
|
|
120
120
|
/**
|
|
121
121
|
* Merging an existing value with a new one
|
|
122
122
|
*/
|
|
123
|
-
mergeItem: (key,
|
|
124
|
-
const promise = provider.mergeItem(key,
|
|
123
|
+
mergeItem: (key, change, replaceNullPatches) => tryOrDegradePerformance(() => {
|
|
124
|
+
const promise = provider.mergeItem(key, change, replaceNullPatches);
|
|
125
125
|
if (shouldKeepInstancesSync) {
|
|
126
126
|
return promise.then(() => InstanceSync_1.default.mergeItem(key));
|
|
127
127
|
}
|
|
@@ -43,15 +43,18 @@ const provider = {
|
|
|
43
43
|
});
|
|
44
44
|
const upsertMany = pairsWithoutNull.map(([key, value], index) => {
|
|
45
45
|
const prev = values[index];
|
|
46
|
-
const newValue = utils_1.default.fastMerge(prev, value
|
|
46
|
+
const newValue = utils_1.default.fastMerge(prev, value, {
|
|
47
|
+
shouldRemoveNestedNulls: true,
|
|
48
|
+
objectRemovalMode: 'replace',
|
|
49
|
+
}).result;
|
|
47
50
|
return (0, idb_keyval_1.promisifyRequest)(store.put(newValue, key));
|
|
48
51
|
});
|
|
49
|
-
return Promise.all(upsertMany);
|
|
52
|
+
return Promise.all(upsertMany).then(() => undefined);
|
|
50
53
|
});
|
|
51
54
|
}),
|
|
52
|
-
mergeItem(key,
|
|
53
|
-
// Since Onyx
|
|
54
|
-
return provider.
|
|
55
|
+
mergeItem(key, change) {
|
|
56
|
+
// Since Onyx already merged the existing value with the changes, we can just set the value directly.
|
|
57
|
+
return provider.multiMerge([[key, change]]);
|
|
55
58
|
},
|
|
56
59
|
multiSet: (pairs) => {
|
|
57
60
|
const pairsWithoutNull = pairs.filter(([key, value]) => {
|
|
@@ -60,9 +60,9 @@ const provider = {
|
|
|
60
60
|
/**
|
|
61
61
|
* Merging an existing value with a new one
|
|
62
62
|
*/
|
|
63
|
-
mergeItem(key,
|
|
64
|
-
// Since Onyx already merged the existing value with the changes, we can just set the value directly
|
|
65
|
-
return this.
|
|
63
|
+
mergeItem(key, change) {
|
|
64
|
+
// Since Onyx already merged the existing value with the changes, we can just set the value directly.
|
|
65
|
+
return this.multiMerge([[key, change]]);
|
|
66
66
|
},
|
|
67
67
|
/**
|
|
68
68
|
* Multiple merging of existing and new values in a batch
|
|
@@ -71,10 +71,13 @@ const provider = {
|
|
|
71
71
|
multiMerge(pairs) {
|
|
72
72
|
underscore_1.default.forEach(pairs, ([key, value]) => {
|
|
73
73
|
const existingValue = store[key];
|
|
74
|
-
const newValue = utils_1.default.fastMerge(existingValue, value
|
|
74
|
+
const newValue = utils_1.default.fastMerge(existingValue, value, {
|
|
75
|
+
shouldRemoveNestedNulls: true,
|
|
76
|
+
objectRemovalMode: 'replace',
|
|
77
|
+
}).result;
|
|
75
78
|
set(key, newValue);
|
|
76
79
|
});
|
|
77
|
-
return Promise.resolve(
|
|
80
|
+
return Promise.resolve();
|
|
78
81
|
},
|
|
79
82
|
/**
|
|
80
83
|
* Remove given key and it's value from memory
|
|
@@ -14,6 +14,24 @@ const utils_1 = __importDefault(require("../../utils"));
|
|
|
14
14
|
(0, react_native_nitro_sqlite_1.enableSimpleNullHandling)();
|
|
15
15
|
const DB_NAME = 'OnyxDB';
|
|
16
16
|
let db;
|
|
17
|
+
/**
|
|
18
|
+
* Prevents the stringifying of the object markers.
|
|
19
|
+
*/
|
|
20
|
+
function objectMarkRemover(key, value) {
|
|
21
|
+
if (key === utils_1.default.ONYX_INTERNALS__REPLACE_OBJECT_MARK)
|
|
22
|
+
return undefined;
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Transforms the replace null patches into SQL queries to be passed to JSON_REPLACE.
|
|
27
|
+
*/
|
|
28
|
+
function generateJSONReplaceSQLQueries(key, patches) {
|
|
29
|
+
const queries = patches.map(([pathArray, value]) => {
|
|
30
|
+
const jsonPath = `$.${pathArray.join('.')}`;
|
|
31
|
+
return [jsonPath, JSON.stringify(value), key];
|
|
32
|
+
});
|
|
33
|
+
return queries;
|
|
34
|
+
}
|
|
17
35
|
const provider = {
|
|
18
36
|
/**
|
|
19
37
|
* The name of the provider that can be printed to the logs
|
|
@@ -53,7 +71,7 @@ const provider = {
|
|
|
53
71
|
});
|
|
54
72
|
},
|
|
55
73
|
setItem(key, value) {
|
|
56
|
-
return db.executeAsync('REPLACE INTO keyvaluepairs (record_key, valueJSON) VALUES (?, ?);', [key, JSON.stringify(value)]);
|
|
74
|
+
return db.executeAsync('REPLACE INTO keyvaluepairs (record_key, valueJSON) VALUES (?, ?);', [key, JSON.stringify(value)]).then(() => undefined);
|
|
57
75
|
},
|
|
58
76
|
multiSet(pairs) {
|
|
59
77
|
const query = 'REPLACE INTO keyvaluepairs (record_key, valueJSON) VALUES (?, json(?));';
|
|
@@ -61,41 +79,57 @@ const provider = {
|
|
|
61
79
|
if (utils_1.default.isEmptyObject(params)) {
|
|
62
80
|
return Promise.resolve();
|
|
63
81
|
}
|
|
64
|
-
return db.executeBatchAsync([{ query, params }]);
|
|
82
|
+
return db.executeBatchAsync([{ query, params }]).then(() => undefined);
|
|
65
83
|
},
|
|
66
84
|
multiMerge(pairs) {
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
const commands = [];
|
|
86
|
+
// Query to merge the change into the DB value.
|
|
87
|
+
const patchQuery = `INSERT INTO keyvaluepairs (record_key, valueJSON)
|
|
88
|
+
VALUES (:key, JSON(:value))
|
|
89
|
+
ON CONFLICT DO UPDATE
|
|
90
|
+
SET valueJSON = JSON_PATCH(valueJSON, JSON(:value));
|
|
73
91
|
`;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
const patchQueryArguments = [];
|
|
93
|
+
// Query to fully replace the nested objects of the DB value.
|
|
94
|
+
const replaceQuery = `UPDATE keyvaluepairs
|
|
95
|
+
SET valueJSON = JSON_REPLACE(valueJSON, ?, JSON(?))
|
|
96
|
+
WHERE record_key = ?;
|
|
97
|
+
`;
|
|
98
|
+
const replaceQueryArguments = [];
|
|
99
|
+
const nonNullishPairs = pairs.filter((pair) => pair[1] !== undefined);
|
|
100
|
+
for (const [key, value, replaceNullPatches] of nonNullishPairs) {
|
|
101
|
+
const changeWithoutMarkers = JSON.stringify(value, objectMarkRemover);
|
|
102
|
+
patchQueryArguments.push([key, changeWithoutMarkers]);
|
|
103
|
+
const patches = replaceNullPatches !== null && replaceNullPatches !== void 0 ? replaceNullPatches : [];
|
|
104
|
+
if (patches.length > 0) {
|
|
105
|
+
const queries = generateJSONReplaceSQLQueries(key, patches);
|
|
106
|
+
if (queries.length > 0) {
|
|
107
|
+
replaceQueryArguments.push(...queries);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
commands.push({ query: patchQuery, params: patchQueryArguments });
|
|
112
|
+
if (replaceQueryArguments.length > 0) {
|
|
113
|
+
commands.push({ query: replaceQuery, params: replaceQueryArguments });
|
|
84
114
|
}
|
|
85
|
-
return
|
|
115
|
+
return db.executeBatchAsync(commands).then(() => undefined);
|
|
116
|
+
},
|
|
117
|
+
mergeItem(key, change, replaceNullPatches) {
|
|
118
|
+
// Since Onyx already merged the existing value with the changes, we can just set the value directly.
|
|
119
|
+
return this.multiMerge([[key, change, replaceNullPatches]]);
|
|
86
120
|
},
|
|
87
121
|
getAllKeys: () => db.executeAsync('SELECT record_key FROM keyvaluepairs;').then(({ rows }) => {
|
|
88
122
|
// eslint-disable-next-line no-underscore-dangle
|
|
89
123
|
const result = rows === null || rows === void 0 ? void 0 : rows._array.map((row) => row.record_key);
|
|
90
124
|
return (result !== null && result !== void 0 ? result : []);
|
|
91
125
|
}),
|
|
92
|
-
removeItem: (key) => db.executeAsync('DELETE FROM keyvaluepairs WHERE record_key = ?;', [key]),
|
|
126
|
+
removeItem: (key) => db.executeAsync('DELETE FROM keyvaluepairs WHERE record_key = ?;', [key]).then(() => undefined),
|
|
93
127
|
removeItems: (keys) => {
|
|
94
128
|
const placeholders = keys.map(() => '?').join(',');
|
|
95
129
|
const query = `DELETE FROM keyvaluepairs WHERE record_key IN (${placeholders});`;
|
|
96
|
-
return db.executeAsync(query, keys);
|
|
130
|
+
return db.executeAsync(query, keys).then(() => undefined);
|
|
97
131
|
},
|
|
98
|
-
clear: () => db.executeAsync('DELETE FROM keyvaluepairs;', []),
|
|
132
|
+
clear: () => db.executeAsync('DELETE FROM keyvaluepairs;', []).then(() => undefined),
|
|
99
133
|
getDatabaseSize() {
|
|
100
134
|
return Promise.all([db.executeAsync('PRAGMA page_size;'), db.executeAsync('PRAGMA page_count;'), (0, react_native_device_info_1.getFreeDiskStorage)()]).then(([pageSizeResult, pageCountResult, bytesRemaining]) => {
|
|
101
135
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { BatchQueryResult, QueryResult } from 'react-native-nitro-sqlite';
|
|
2
1
|
import type { OnyxKey, OnyxValue } from '../../types';
|
|
3
|
-
type
|
|
4
|
-
type
|
|
5
|
-
type
|
|
2
|
+
import type { FastMergeReplaceNullPatch } from '../../utils';
|
|
3
|
+
type StorageKeyValuePair = [key: OnyxKey, value: OnyxValue<OnyxKey>, replaceNullPatches?: FastMergeReplaceNullPatch[]];
|
|
4
|
+
type StorageKeyList = OnyxKey[];
|
|
5
|
+
type DatabaseSize = {
|
|
6
|
+
bytesUsed: number;
|
|
7
|
+
bytesRemaining: number;
|
|
8
|
+
};
|
|
6
9
|
type OnStorageKeyChanged = <TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>) => void;
|
|
7
10
|
type StorageProvider = {
|
|
8
11
|
/**
|
|
@@ -20,53 +23,48 @@ type StorageProvider = {
|
|
|
20
23
|
/**
|
|
21
24
|
* Get multiple key-value pairs for the given array of keys in a batch
|
|
22
25
|
*/
|
|
23
|
-
multiGet: (keys:
|
|
26
|
+
multiGet: (keys: StorageKeyList) => Promise<StorageKeyValuePair[]>;
|
|
24
27
|
/**
|
|
25
28
|
* Sets the value for a given key. The only requirement is that the value should be serializable to JSON string
|
|
26
29
|
*/
|
|
27
|
-
setItem: <TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>) => Promise<
|
|
30
|
+
setItem: <TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>) => Promise<void>;
|
|
28
31
|
/**
|
|
29
32
|
* Stores multiple key-value pairs in a batch
|
|
30
33
|
*/
|
|
31
|
-
multiSet: (pairs:
|
|
34
|
+
multiSet: (pairs: StorageKeyValuePair[]) => Promise<void>;
|
|
32
35
|
/**
|
|
33
36
|
* Multiple merging of existing and new values in a batch
|
|
34
37
|
*/
|
|
35
|
-
multiMerge: (pairs:
|
|
38
|
+
multiMerge: (pairs: StorageKeyValuePair[]) => Promise<void>;
|
|
36
39
|
/**
|
|
37
|
-
* Merges an existing value with a new one
|
|
38
|
-
* @param
|
|
39
|
-
* @param preMergedValue - the pre-merged data from `Onyx.applyMerge`
|
|
40
|
-
* @param shouldSetValue - whether the data should be set instead of merged
|
|
40
|
+
* Merges an existing value with a new one
|
|
41
|
+
* @param change - the change to merge with the existing value
|
|
41
42
|
*/
|
|
42
|
-
mergeItem: <TKey extends OnyxKey>(key: TKey,
|
|
43
|
+
mergeItem: <TKey extends OnyxKey>(key: TKey, change: OnyxValue<TKey>, replaceNullPatches?: FastMergeReplaceNullPatch[]) => Promise<void>;
|
|
43
44
|
/**
|
|
44
45
|
* Returns all keys available in storage
|
|
45
46
|
*/
|
|
46
|
-
getAllKeys: () => Promise<
|
|
47
|
+
getAllKeys: () => Promise<StorageKeyList>;
|
|
47
48
|
/**
|
|
48
49
|
* Removes given key and its value from storage
|
|
49
50
|
*/
|
|
50
|
-
removeItem: (key: OnyxKey) => Promise<
|
|
51
|
+
removeItem: (key: OnyxKey) => Promise<void>;
|
|
51
52
|
/**
|
|
52
53
|
* Removes given keys and their values from storage
|
|
53
54
|
*/
|
|
54
|
-
removeItems: (keys:
|
|
55
|
+
removeItems: (keys: StorageKeyList) => Promise<void>;
|
|
55
56
|
/**
|
|
56
57
|
* Clears absolutely everything from storage
|
|
57
58
|
*/
|
|
58
|
-
clear: () => Promise<
|
|
59
|
+
clear: () => Promise<void>;
|
|
59
60
|
/**
|
|
60
61
|
* Gets the total bytes of the database file
|
|
61
62
|
*/
|
|
62
|
-
getDatabaseSize: () => Promise<
|
|
63
|
-
bytesUsed: number;
|
|
64
|
-
bytesRemaining: number;
|
|
65
|
-
}>;
|
|
63
|
+
getDatabaseSize: () => Promise<DatabaseSize>;
|
|
66
64
|
/**
|
|
67
65
|
* @param onStorageKeyChanged Storage synchronization mechanism keeping all opened tabs in sync
|
|
68
66
|
*/
|
|
69
67
|
keepInstancesSync?: (onStorageKeyChanged: OnStorageKeyChanged) => void;
|
|
70
68
|
};
|
|
71
69
|
export default StorageProvider;
|
|
72
|
-
export type {
|
|
70
|
+
export type { StorageKeyList, StorageKeyValuePair, OnStorageKeyChanged };
|