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/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] = OnyxUtils.splitCollectionMemberKey(key);
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) => OnyxUtils.get(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
- return Object.entries(data).reduce((pairs, [key, value]) => {
987
- const { value: valueAfterRemoving, wasRemoved } = removeNullValues(key, value, shouldRemoveNestedNulls);
988
- if (!wasRemoved && valueAfterRemoving !== undefined) {
989
- pairs.push([key, valueAfterRemoving]);
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
- return pairs;
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 applied to the existing value
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 applyMerge(existingValue, changes, shouldRemoveNestedNulls) {
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) => utils_1.default.fastMerge(modifiedData, change, shouldRemoveNestedNulls), (existingValue || {}));
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 = OnyxUtils.getSnapshotKey();
1180
+ const snapshotCollectionKey = getSnapshotKey();
1161
1181
  if (!snapshotCollectionKey)
1162
1182
  return [];
1163
1183
  const promises = [];
1164
- const snapshotCollection = OnyxUtils.getCachedCollection(snapshotCollectionKey);
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 (OnyxUtils.isCollectionMemberKey(snapshotCollectionKey, key)) {
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
- applyMerge,
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 { KeyList, OnStorageKeyChanged } from '../providers/types';
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: KeyList): void;
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").KeyValuePairList>, [keys: import("../providers/types").KeyList], any>;
6
- setItem: jest.Mock<Promise<void | import("react-native-nitro-sqlite").QueryResult<import("react-native-nitro-sqlite").QueryResultRow>>, [key: any, value: unknown], any>;
7
- multiSet: jest.Mock<Promise<void | import("react-native-nitro-sqlite").BatchQueryResult>, [pairs: import("../providers/types").KeyValuePairList], any>;
8
- mergeItem: jest.Mock<Promise<void | import("react-native-nitro-sqlite").BatchQueryResult>, [key: any, deltaChanges: unknown, preMergedValue: unknown, shouldSetValue?: boolean | undefined], any>;
9
- multiMerge: jest.Mock<Promise<void | import("react-native-nitro-sqlite").BatchQueryResult | IDBValidKey[]>, [pairs: import("../providers/types").KeyValuePairList], any>;
10
- removeItem: jest.Mock<Promise<void | import("react-native-nitro-sqlite").QueryResult<import("react-native-nitro-sqlite").QueryResultRow>>, [key: string], any>;
11
- removeItems: jest.Mock<Promise<void | import("react-native-nitro-sqlite").QueryResult<import("react-native-nitro-sqlite").QueryResultRow>>, [keys: import("../providers/types").KeyList], any>;
12
- clear: jest.Mock<Promise<void | import("react-native-nitro-sqlite").QueryResult<import("react-native-nitro-sqlite").QueryResultRow>>, [], any>;
13
- getAllKeys: jest.Mock<Promise<import("../providers/types").KeyList>, [], any>;
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;
@@ -120,8 +120,8 @@ const storage = {
120
120
  /**
121
121
  * Merging an existing value with a new one
122
122
  */
123
- mergeItem: (key, deltaChanges, preMergedValue, shouldSetValue = false) => tryOrDegradePerformance(() => {
124
- const promise = provider.mergeItem(key, deltaChanges, preMergedValue, shouldSetValue);
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, _deltaChanges, preMergedValue) {
53
- // Since Onyx also merged the existing value with the changes, we can just set the value directly
54
- return provider.setItem(key, preMergedValue);
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, _deltaChanges, preMergedValue) {
64
- // Since Onyx already merged the existing value with the changes, we can just set the value directly
65
- return this.setItem(key, preMergedValue);
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
@@ -46,7 +46,7 @@ const provider = {
46
46
  * This function also removes all nested null values from an object.
47
47
  */
48
48
  multiMerge() {
49
- return Promise.resolve([]);
49
+ return Promise.resolve();
50
50
  },
51
51
  /**
52
52
  * 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
- // Note: We use `ON CONFLICT DO UPDATE` here instead of `INSERT OR REPLACE INTO`
68
- // so the new JSON value is merged into the old one if there's an existing value
69
- const query = `INSERT INTO keyvaluepairs (record_key, valueJSON)
70
- VALUES (:key, JSON(:value))
71
- ON CONFLICT DO UPDATE
72
- SET valueJSON = JSON_PATCH(valueJSON, JSON(:value));
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 nonUndefinedPairs = pairs.filter((pair) => pair[1] !== undefined);
75
- const params = nonUndefinedPairs.map((pair) => {
76
- const value = JSON.stringify(pair[1]);
77
- return [pair[0], value];
78
- });
79
- return db.executeBatchAsync([{ query, params }]);
80
- },
81
- mergeItem(key, deltaChanges, preMergedValue, shouldSetValue) {
82
- if (shouldSetValue) {
83
- return this.setItem(key, preMergedValue);
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 this.multiMerge([[key, deltaChanges]]);
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 KeyValuePair = [OnyxKey, OnyxValue<OnyxKey>];
4
- type KeyList = OnyxKey[];
5
- type KeyValuePairList = KeyValuePair[];
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: KeyList) => Promise<KeyValuePairList>;
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<QueryResult | void>;
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: KeyValuePairList) => Promise<BatchQueryResult | void>;
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: KeyValuePairList) => Promise<BatchQueryResult | IDBValidKey[] | void>;
38
+ multiMerge: (pairs: StorageKeyValuePair[]) => Promise<void>;
36
39
  /**
37
- * Merges an existing value with a new one by leveraging JSON_PATCH
38
- * @param deltaChanges - the delta for a specific key
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, deltaChanges: OnyxValue<TKey>, preMergedValue: OnyxValue<TKey>, shouldSetValue?: boolean) => Promise<BatchQueryResult | void>;
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<KeyList>;
47
+ getAllKeys: () => Promise<StorageKeyList>;
47
48
  /**
48
49
  * Removes given key and its value from storage
49
50
  */
50
- removeItem: (key: OnyxKey) => Promise<QueryResult | void>;
51
+ removeItem: (key: OnyxKey) => Promise<void>;
51
52
  /**
52
53
  * Removes given keys and their values from storage
53
54
  */
54
- removeItems: (keys: KeyList) => Promise<QueryResult | void>;
55
+ removeItems: (keys: StorageKeyList) => Promise<void>;
55
56
  /**
56
57
  * Clears absolutely everything from storage
57
58
  */
58
- clear: () => Promise<QueryResult | void>;
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 { KeyList, KeyValuePair, KeyValuePairList, OnStorageKeyChanged };
70
+ export type { StorageKeyList, StorageKeyValuePair, OnStorageKeyChanged };