react-native-onyx 2.0.117 → 2.0.119

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 CHANGED
@@ -29,7 +29,7 @@ applied in the order they were called. Note: <code>Onyx.set()</code> calls do no
29
29
  <code>Onyx.merge()</code> and <code>Onyx.set()</code>.</p>
30
30
  </dd>
31
31
  <dt><a href="#mergeCollection">mergeCollection(collectionKey, collection)</a></dt>
32
- <dd><p>Merges a collection based on their keys</p>
32
+ <dd><p>Merges a collection based on their keys.</p>
33
33
  </dd>
34
34
  <dt><a href="#clear">clear(keysToPreserve)</a></dt>
35
35
  <dd><p>Clear out all the data in the store</p>
@@ -158,7 +158,7 @@ Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Wor
158
158
  <a name="mergeCollection"></a>
159
159
 
160
160
  ## mergeCollection(collectionKey, collection)
161
- Merges a collection based on their keys
161
+ Merges a collection based on their keys.
162
162
 
163
163
  **Kind**: global function
164
164
 
package/README.md CHANGED
@@ -59,6 +59,33 @@ API.Authenticate(params)
59
59
 
60
60
  The data will then be cached and stored via [`AsyncStorage`](https://github.com/react-native-async-storage/async-storage).
61
61
 
62
+ ### Performance Options for Large Objects
63
+
64
+ For performance-critical scenarios with large objects, `Onyx.set()` accepts optional flags to skip expensive operations:
65
+
66
+ ```javascript
67
+ Onyx.set(ONYXKEYS.LARGE_DATA, computedValue, {
68
+ skipCacheCheck: true, // Skip deep equality check
69
+ skipNullRemoval: true, // Skip null value pruning
70
+ });
71
+ ```
72
+
73
+ **Options:**
74
+ - `skipCacheCheck`: Skips the deep equality comparison with the cached value. By default, Onyx compares new values against cached ones to avoid unnecessary updates. For large objects, this comparison can be expensive.
75
+ - `skipNullRemoval`: Skips the removal of `null` values from nested objects. By default, Onyx removes `null` values before storage. Use this when `null` values are meaningful in your data structure.
76
+
77
+ #### When to Use SetOptions
78
+ - **Use `skipCacheCheck: true`** for:
79
+ - Large objects where deep equality checking is expensive
80
+ - Values that you know have changed
81
+
82
+ - **Use `skipNullRemoval: true`** for:
83
+ - Computed values where `null` represents a legitimate result
84
+ - Data structures where `null` has semantic meaning
85
+ - Values that should preserve their exact structure
86
+
87
+ **Note**: These options are recommended only for large objects where performance is critical. Most use cases should use the standard `Onyx.set(key, value)` syntax.
88
+
62
89
  ## Merging data
63
90
 
64
91
  We can also use `Onyx.merge()` to merge new `Object` or `Array` data in with existing data.
package/dist/Onyx.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as Logger from './Logger';
2
- import type { CollectionKeyBase, ConnectOptions, InitOptions, Mapping, OnyxKey, OnyxMergeCollectionInput, OnyxMergeInput, OnyxMultiSetInput, OnyxSetInput, OnyxUpdate } from './types';
2
+ import type { CollectionKeyBase, ConnectOptions, InitOptions, Mapping, OnyxKey, OnyxMergeCollectionInput, OnyxMergeInput, OnyxMultiSetInput, OnyxSetInput, OnyxUpdate, SetOptions } from './types';
3
3
  import type { Connection } from './OnyxConnectionManager';
4
4
  /** Initialize the store with actions and listening for storage events */
5
5
  declare function init({ keys, initialKeyStates, evictableKeys, maxCachedKeysCount, shouldSyncMultipleInstances, debugSetState, enablePerformanceMetrics, skippableCollectionMemberIDs, fullyMergedSnapshotKeys, }: InitOptions): void;
@@ -49,8 +49,9 @@ declare function disconnect(connection: Connection): void;
49
49
  *
50
50
  * @param key ONYXKEY to set
51
51
  * @param value value to store
52
+ * @param options optional configuration object
52
53
  */
53
- declare function set<TKey extends OnyxKey>(key: TKey, value: OnyxSetInput<TKey>): Promise<void>;
54
+ declare function set<TKey extends OnyxKey>(key: TKey, value: OnyxSetInput<TKey>, options?: SetOptions): Promise<void>;
54
55
  /**
55
56
  * Sets multiple keys and values
56
57
  *
@@ -77,7 +78,7 @@ declare function multiSet(data: OnyxMultiSetInput): Promise<void>;
77
78
  */
78
79
  declare function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>): Promise<void>;
79
80
  /**
80
- * Merges a collection based on their keys
81
+ * Merges a collection based on their keys.
81
82
  *
82
83
  * @example
83
84
  *
@@ -155,4 +156,4 @@ declare const Onyx: {
155
156
  registerLogger: typeof Logger.registerLogger;
156
157
  };
157
158
  export default Onyx;
158
- export type { OnyxUpdate, Mapping, ConnectOptions };
159
+ export type { OnyxUpdate, Mapping, ConnectOptions, SetOptions };
package/dist/Onyx.js CHANGED
@@ -26,8 +26,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- /* eslint-disable no-continue */
30
- const underscore_1 = __importDefault(require("underscore"));
31
29
  const Logger = __importStar(require("./Logger"));
32
30
  const OnyxCache_1 = __importStar(require("./OnyxCache"));
33
31
  const PerformanceUtils = __importStar(require("./PerformanceUtils"));
@@ -39,6 +37,7 @@ const logMessages_1 = __importDefault(require("./logMessages"));
39
37
  const OnyxConnectionManager_1 = __importDefault(require("./OnyxConnectionManager"));
40
38
  const GlobalSettings = __importStar(require("./GlobalSettings"));
41
39
  const metrics_1 = __importDefault(require("./metrics"));
40
+ const OnyxMerge_1 = __importDefault(require("./OnyxMerge"));
42
41
  /** Initialize the store with actions and listening for storage events */
43
42
  function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], maxCachedKeysCount = 1000, shouldSyncMultipleInstances = !!global.localStorage, debugSetState = false, enablePerformanceMetrics = false, skippableCollectionMemberIDs = [], fullyMergedSnapshotKeys = [], }) {
44
43
  var _a;
@@ -115,8 +114,9 @@ function disconnect(connection) {
115
114
  *
116
115
  * @param key ONYXKEY to set
117
116
  * @param value value to store
117
+ * @param options optional configuration object
118
118
  */
119
- function set(key, value) {
119
+ function set(key, value, options) {
120
120
  // When we use Onyx.set to set a key we want to clear the current delta changes from Onyx.merge that were queued
121
121
  // before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes.
122
122
  if (OnyxUtils_1.default.hasPendingMergeForKey(key)) {
@@ -151,31 +151,26 @@ function set(key, value) {
151
151
  Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'set', existingValueType, newValueType));
152
152
  return Promise.resolve();
153
153
  }
154
- // If the value is null, we remove the key from storage
155
- const { value: valueAfterRemoving, wasRemoved } = OnyxUtils_1.default.removeNullValues(key, value);
156
- const logSetCall = (hasChanged = true) => {
157
- // Logging properties only since values could be sensitive things we don't want to log
158
- Logger.logInfo(`set called for key: ${key}${underscore_1.default.isObject(value) ? ` properties: ${underscore_1.default.keys(value).join(',')}` : ''} hasChanged: ${hasChanged}`);
159
- };
160
- // Calling "OnyxUtils.removeNullValues" removes the key from storage and cache and updates the subscriber.
154
+ // If the change is null, we can just delete the key.
161
155
  // Therefore, we don't need to further broadcast and update the value so we can return early.
162
- if (wasRemoved) {
163
- logSetCall();
156
+ if (value === null) {
157
+ OnyxUtils_1.default.remove(key);
158
+ OnyxUtils_1.default.logKeyRemoved(OnyxUtils_1.default.METHOD.SET, key);
164
159
  return Promise.resolve();
165
160
  }
166
- const valueWithoutNullValues = valueAfterRemoving;
167
- const hasChanged = OnyxCache_1.default.hasValueChanged(key, valueWithoutNullValues);
168
- logSetCall(hasChanged);
161
+ const valueWithoutNestedNullValues = ((options === null || options === void 0 ? void 0 : options.skipNullRemoval) ? value : utils_1.default.removeNestedNullValues(value));
162
+ const hasChanged = (options === null || options === void 0 ? void 0 : options.skipCacheCheck) ? true : OnyxCache_1.default.hasValueChanged(key, valueWithoutNestedNullValues);
163
+ OnyxUtils_1.default.logKeyChanged(OnyxUtils_1.default.METHOD.SET, key, value, hasChanged);
169
164
  // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
170
- const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, valueWithoutNullValues, hasChanged);
165
+ const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged);
171
166
  // If the value has not changed or the key got removed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
172
167
  if (!hasChanged) {
173
168
  return updatePromise;
174
169
  }
175
- return storage_1.default.setItem(key, valueWithoutNullValues)
176
- .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, set, key, valueWithoutNullValues))
170
+ return storage_1.default.setItem(key, valueWithoutNestedNullValues)
171
+ .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, set, key, valueWithoutNestedNullValues))
177
172
  .then(() => {
178
- OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.SET, key, valueWithoutNullValues);
173
+ OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.SET, key, valueWithoutNestedNullValues);
179
174
  return updatePromise;
180
175
  });
181
176
  }
@@ -276,8 +271,6 @@ function merge(key, changes) {
276
271
  return Promise.resolve();
277
272
  }
278
273
  try {
279
- // 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)
280
- // We don't want to remove null values from the "batchedDeltaChanges", because SQLite uses them to remove keys from storage natively.
281
274
  const validChanges = mergeQueue[key].filter((change) => {
282
275
  const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue);
283
276
  if (!isCompatible) {
@@ -288,42 +281,18 @@ function merge(key, changes) {
288
281
  if (!validChanges.length) {
289
282
  return Promise.resolve();
290
283
  }
291
- const batchedDeltaChanges = OnyxUtils_1.default.applyMerge(undefined, validChanges, false);
292
- // Case (1): When there is no existing value in storage, we want to set the value instead of merge it.
293
- // Case (2): The presence of a top-level `null` in the merge queue instructs us to drop the whole existing value.
294
- // In this case, we can't simply merge the batched changes with the existing value, because then the null in the merge queue would have no effect
295
- const shouldSetValue = !existingValue || mergeQueue[key].includes(null);
296
- // Clean up the write queue, so we don't apply these changes again
284
+ // Clean up the write queue, so we don't apply these changes again.
297
285
  delete mergeQueue[key];
298
286
  delete mergeQueuePromise[key];
299
- const logMergeCall = (hasChanged = true) => {
300
- // Logging properties only since values could be sensitive things we don't want to log
301
- Logger.logInfo(`merge called for key: ${key}${underscore_1.default.isObject(batchedDeltaChanges) ? ` properties: ${underscore_1.default.keys(batchedDeltaChanges).join(',')}` : ''} hasChanged: ${hasChanged}`);
302
- };
303
- // If the batched changes equal null, we want to remove the key from storage, to reduce storage size
304
- const { wasRemoved } = OnyxUtils_1.default.removeNullValues(key, batchedDeltaChanges);
305
- // Calling "OnyxUtils.removeNullValues" removes the key from storage and cache and updates the subscriber.
287
+ // If the last change is null, we can just delete the key.
306
288
  // Therefore, we don't need to further broadcast and update the value so we can return early.
307
- if (wasRemoved) {
308
- logMergeCall();
289
+ if (validChanges.at(-1) === null) {
290
+ OnyxUtils_1.default.remove(key);
291
+ OnyxUtils_1.default.logKeyRemoved(OnyxUtils_1.default.METHOD.MERGE, key);
309
292
  return Promise.resolve();
310
293
  }
311
- // For providers that can't handle delta changes, we need to merge the batched changes with the existing value beforehand.
312
- // The "preMergedValue" will be directly "set" in storage instead of being merged
313
- // Therefore we merge the batched changes with the existing value to get the final merged value that will be stored.
314
- // We can remove null values from the "preMergedValue", because "null" implicates that the user wants to remove a value from storage.
315
- const preMergedValue = OnyxUtils_1.default.applyMerge(shouldSetValue ? undefined : existingValue, [batchedDeltaChanges], true);
316
- // In cache, we don't want to remove the key if it's null to improve performance and speed up the next merge.
317
- const hasChanged = OnyxCache_1.default.hasValueChanged(key, preMergedValue);
318
- logMergeCall(hasChanged);
319
- // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
320
- const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, preMergedValue, hasChanged);
321
- // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
322
- if (!hasChanged) {
323
- return updatePromise;
324
- }
325
- return storage_1.default.mergeItem(key, batchedDeltaChanges, preMergedValue, shouldSetValue).then(() => {
326
- OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MERGE, key, changes, preMergedValue);
294
+ return OnyxMerge_1.default.applyMerge(key, existingValue, validChanges).then(({ mergedValue, updatePromise }) => {
295
+ OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MERGE, key, changes, mergedValue);
327
296
  return updatePromise;
328
297
  });
329
298
  }
@@ -335,7 +304,7 @@ function merge(key, changes) {
335
304
  return mergeQueuePromise[key];
336
305
  }
337
306
  /**
338
- * Merges a collection based on their keys
307
+ * Merges a collection based on their keys.
339
308
  *
340
309
  * @example
341
310
  *
@@ -348,98 +317,7 @@ function merge(key, changes) {
348
317
  * @param collection Object collection keyed by individual collection member keys and values
349
318
  */
350
319
  function mergeCollection(collectionKey, collection) {
351
- if (!OnyxUtils_1.default.isValidNonEmptyCollectionForMerge(collection)) {
352
- Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
353
- return Promise.resolve();
354
- }
355
- let resultCollection = collection;
356
- let resultCollectionKeys = Object.keys(resultCollection);
357
- // Confirm all the collection keys belong to the same parent
358
- if (!OnyxUtils_1.default.doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) {
359
- return Promise.resolve();
360
- }
361
- const skippableCollectionMemberIDs = OnyxUtils_1.default.getSkippableCollectionMemberIDs();
362
- if (skippableCollectionMemberIDs.size) {
363
- resultCollection = resultCollectionKeys.reduce((result, key) => {
364
- try {
365
- const [, collectionMemberID] = OnyxUtils_1.default.splitCollectionMemberKey(key, collectionKey);
366
- // If the collection member key is a skippable one we set its value to null.
367
- // eslint-disable-next-line no-param-reassign
368
- result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null;
369
- }
370
- catch (_a) {
371
- // Something went wrong during split, so we assign the data to result anyway.
372
- // eslint-disable-next-line no-param-reassign
373
- result[key] = resultCollection[key];
374
- }
375
- return result;
376
- }, {});
377
- }
378
- resultCollectionKeys = Object.keys(resultCollection);
379
- return OnyxUtils_1.default.getAllKeys()
380
- .then((persistedKeys) => {
381
- // Split to keys that exist in storage and keys that don't
382
- const keys = resultCollectionKeys.filter((key) => {
383
- if (resultCollection[key] === null) {
384
- OnyxUtils_1.default.remove(key);
385
- return false;
386
- }
387
- return true;
388
- });
389
- const existingKeys = keys.filter((key) => persistedKeys.has(key));
390
- const cachedCollectionForExistingKeys = OnyxUtils_1.default.getCachedCollection(collectionKey, existingKeys);
391
- const existingKeyCollection = existingKeys.reduce((obj, key) => {
392
- const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(resultCollection[key], cachedCollectionForExistingKeys[key]);
393
- if (!isCompatible) {
394
- Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'mergeCollection', existingValueType, newValueType));
395
- return obj;
396
- }
397
- // eslint-disable-next-line no-param-reassign
398
- obj[key] = resultCollection[key];
399
- return obj;
400
- }, {});
401
- const newCollection = {};
402
- keys.forEach((key) => {
403
- if (persistedKeys.has(key)) {
404
- return;
405
- }
406
- newCollection[key] = resultCollection[key];
407
- });
408
- // When (multi-)merging the values with the existing values in storage,
409
- // we don't want to remove nested null values from the data that we pass to the storage layer,
410
- // because the storage layer uses them to remove nested keys from storage natively.
411
- const keyValuePairsForExistingCollection = OnyxUtils_1.default.prepareKeyValuePairsForStorage(existingKeyCollection, false);
412
- // We can safely remove nested null values when using (multi-)set,
413
- // because we will simply overwrite the existing values in storage.
414
- const keyValuePairsForNewCollection = OnyxUtils_1.default.prepareKeyValuePairsForStorage(newCollection, true);
415
- const promises = [];
416
- // We need to get the previously existing values so we can compare the new ones
417
- // against them, to avoid unnecessary subscriber updates.
418
- const previousCollectionPromise = Promise.all(existingKeys.map((key) => OnyxUtils_1.default.get(key).then((value) => [key, value]))).then(Object.fromEntries);
419
- // New keys will be added via multiSet while existing keys will be updated using multiMerge
420
- // This is because setting a key that doesn't exist yet with multiMerge will throw errors
421
- if (keyValuePairsForExistingCollection.length > 0) {
422
- promises.push(storage_1.default.multiMerge(keyValuePairsForExistingCollection));
423
- }
424
- if (keyValuePairsForNewCollection.length > 0) {
425
- promises.push(storage_1.default.multiSet(keyValuePairsForNewCollection));
426
- }
427
- // finalMergedCollection contains all the keys that were merged, without the keys of incompatible updates
428
- const finalMergedCollection = Object.assign(Object.assign({}, existingKeyCollection), newCollection);
429
- // Prefill cache if necessary by calling get() on any existing keys and then merge original data to cache
430
- // and update all subscribers
431
- const promiseUpdate = previousCollectionPromise.then((previousCollection) => {
432
- OnyxCache_1.default.merge(finalMergedCollection);
433
- return OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(collectionKey, finalMergedCollection, previousCollection);
434
- });
435
- return Promise.all(promises)
436
- .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, mergeCollection, collectionKey, resultCollection))
437
- .then(() => {
438
- OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MERGE_COLLECTION, undefined, resultCollection);
439
- return promiseUpdate;
440
- });
441
- })
442
- .then(() => undefined);
320
+ return OnyxUtils_1.default.mergeCollectionWithPatches(collectionKey, collection);
443
321
  }
444
322
  /**
445
323
  * Clear out all the data in the store
@@ -628,35 +506,42 @@ function update(data) {
628
506
  const operations = updateQueue[key];
629
507
  // Remove the collection-related key from the updateQueue so that it won't be processed individually.
630
508
  delete updateQueue[key];
631
- const updatedValue = OnyxUtils_1.default.applyMerge(undefined, operations, false);
509
+ const batchedChanges = OnyxUtils_1.default.mergeAndMarkChanges(operations);
632
510
  if (operations[0] === null) {
633
511
  // eslint-disable-next-line no-param-reassign
634
- queue.set[key] = updatedValue;
512
+ queue.set[key] = batchedChanges.result;
635
513
  }
636
514
  else {
637
515
  // eslint-disable-next-line no-param-reassign
638
- queue.merge[key] = updatedValue;
516
+ queue.merge[key] = batchedChanges.result;
517
+ if (batchedChanges.replaceNullPatches.length > 0) {
518
+ // eslint-disable-next-line no-param-reassign
519
+ queue.mergeReplaceNullPatches[key] = batchedChanges.replaceNullPatches;
520
+ }
639
521
  }
640
522
  return queue;
641
523
  }, {
642
524
  merge: {},
525
+ mergeReplaceNullPatches: {},
643
526
  set: {},
644
527
  });
645
528
  if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.merge)) {
646
- promises.push(() => mergeCollection(collectionKey, batchedCollectionUpdates.merge));
529
+ promises.push(() => OnyxUtils_1.default.mergeCollectionWithPatches(collectionKey, batchedCollectionUpdates.merge, batchedCollectionUpdates.mergeReplaceNullPatches));
647
530
  }
648
531
  if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) {
649
532
  promises.push(() => multiSet(batchedCollectionUpdates.set));
650
533
  }
651
534
  });
652
535
  Object.entries(updateQueue).forEach(([key, operations]) => {
653
- const batchedChanges = OnyxUtils_1.default.applyMerge(undefined, operations, false);
654
536
  if (operations[0] === null) {
537
+ const batchedChanges = OnyxUtils_1.default.mergeChanges(operations).result;
655
538
  promises.push(() => set(key, batchedChanges));
539
+ return;
656
540
  }
657
- else {
658
- promises.push(() => merge(key, batchedChanges));
659
- }
541
+ const mergePromises = operations.map((operation) => {
542
+ return merge(key, operation);
543
+ });
544
+ promises.push(() => { var _a; return (_a = mergePromises.at(0)) !== null && _a !== void 0 ? _a : Promise.resolve(); });
660
545
  });
661
546
  const snapshotPromises = OnyxUtils_1.default.updateSnapshots(data, merge);
662
547
  // We need to run the snapshot updates before the other updates so the snapshot data can be updated before the loading state in the snapshot
package/dist/OnyxCache.js CHANGED
@@ -165,7 +165,10 @@ class OnyxCache {
165
165
  if (typeof data !== 'object' || Array.isArray(data)) {
166
166
  throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs');
167
167
  }
168
- this.storageMap = Object.assign({}, utils_1.default.fastMerge(this.storageMap, data));
168
+ this.storageMap = Object.assign({}, utils_1.default.fastMerge(this.storageMap, data, {
169
+ shouldRemoveNestedNulls: true,
170
+ objectRemovalMode: 'replace',
171
+ }).result);
169
172
  Object.entries(data).forEach(([key, value]) => {
170
173
  this.addKey(key);
171
174
  this.addToAccessedKeys(key);
@@ -0,0 +1,5 @@
1
+ import type { ApplyMerge } from './types';
2
+ declare const OnyxMerge: {
3
+ applyMerge: ApplyMerge;
4
+ };
5
+ export default OnyxMerge;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const OnyxCache_1 = __importDefault(require("../OnyxCache"));
7
+ const OnyxUtils_1 = __importDefault(require("../OnyxUtils"));
8
+ const storage_1 = __importDefault(require("../storage"));
9
+ const applyMerge = (key, existingValue, validChanges) => {
10
+ const { result: mergedValue } = OnyxUtils_1.default.mergeChanges(validChanges, existingValue);
11
+ // In cache, we don't want to remove the key if it's null to improve performance and speed up the next merge.
12
+ const hasChanged = OnyxCache_1.default.hasValueChanged(key, mergedValue);
13
+ // Logging properties only since values could be sensitive things we don't want to log.
14
+ OnyxUtils_1.default.logKeyChanged(OnyxUtils_1.default.METHOD.MERGE, key, mergedValue, hasChanged);
15
+ // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
16
+ const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, mergedValue, hasChanged);
17
+ // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
18
+ if (!hasChanged) {
19
+ return Promise.resolve({ mergedValue, updatePromise });
20
+ }
21
+ // For web platforms we use `setItem` since the object was already merged with its changes before.
22
+ return storage_1.default.setItem(key, mergedValue).then(() => ({
23
+ mergedValue,
24
+ updatePromise,
25
+ }));
26
+ };
27
+ const OnyxMerge = {
28
+ applyMerge,
29
+ };
30
+ exports.default = OnyxMerge;
@@ -0,0 +1,5 @@
1
+ import type { ApplyMerge } from './types';
2
+ declare const OnyxMerge: {
3
+ applyMerge: ApplyMerge;
4
+ };
5
+ export default OnyxMerge;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const OnyxUtils_1 = __importDefault(require("../OnyxUtils"));
7
+ const OnyxCache_1 = __importDefault(require("../OnyxCache"));
8
+ const storage_1 = __importDefault(require("../storage"));
9
+ const applyMerge = (key, existingValue, validChanges) => {
10
+ // If any of the changes is null, we need to discard the existing value.
11
+ const baseValue = validChanges.includes(null) ? undefined : existingValue;
12
+ // We first batch the changes into a single change with object removal marks,
13
+ // so that SQLite can merge the changes more efficiently.
14
+ const { result: batchedChanges, replaceNullPatches } = OnyxUtils_1.default.mergeAndMarkChanges(validChanges);
15
+ // We then merge the batched changes with the existing value, because we need to final merged value to broadcast to subscribers.
16
+ const { result: mergedValue } = OnyxUtils_1.default.mergeChanges([batchedChanges], baseValue);
17
+ // In cache, we don't want to remove the key if it's null to improve performance and speed up the next merge.
18
+ const hasChanged = OnyxCache_1.default.hasValueChanged(key, mergedValue);
19
+ // Logging properties only since values could be sensitive things we don't want to log.
20
+ OnyxUtils_1.default.logKeyChanged(OnyxUtils_1.default.METHOD.MERGE, key, mergedValue, hasChanged);
21
+ // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
22
+ const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, mergedValue, hasChanged);
23
+ // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
24
+ if (!hasChanged) {
25
+ return Promise.resolve({ mergedValue, updatePromise });
26
+ }
27
+ // For native platforms we use `mergeItem` that will take advantage of JSON_PATCH and JSON_REPLACE SQL operations to
28
+ // merge the object in a performant way.
29
+ return storage_1.default.mergeItem(key, batchedChanges, replaceNullPatches).then(() => ({
30
+ mergedValue,
31
+ updatePromise,
32
+ }));
33
+ };
34
+ const OnyxMerge = {
35
+ applyMerge,
36
+ };
37
+ exports.default = OnyxMerge;
@@ -0,0 +1,7 @@
1
+ import type { OnyxInput, OnyxKey } from '../types';
2
+ type ApplyMergeResult<TValue> = {
3
+ mergedValue: TValue;
4
+ updatePromise: Promise<void>;
5
+ };
6
+ type ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<OnyxKey> | undefined, TChange extends OnyxInput<OnyxKey> | null>(key: TKey, existingValue: TValue, validChanges: TChange[]) => Promise<ApplyMergeResult<TChange>>;
7
+ export type { ApplyMerge, ApplyMergeResult };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,8 +1,10 @@
1
1
  import type { ValueOf } from 'type-fest';
2
2
  import type Onyx from './Onyx';
3
- import type { CollectionKey, CollectionKeyBase, ConnectOptions, DeepRecord, KeyValueMapping, Mapping, OnyxCollection, OnyxEntry, OnyxInput, OnyxKey, OnyxMergeCollectionInput, OnyxUpdate, OnyxValue, Selector } from './types';
3
+ import type { CollectionKey, CollectionKeyBase, ConnectOptions, DeepRecord, KeyValueMapping, Mapping, MultiMergeReplaceNullPatches, OnyxCollection, OnyxEntry, OnyxInput, OnyxKey, OnyxMergeCollectionInput, OnyxUpdate, OnyxValue, Selector } from './types';
4
+ import type { FastMergeResult } from './utils';
4
5
  import type { WithOnyxState } from './withOnyx/types';
5
6
  import type { DeferredTask } from './createDeferredTask';
7
+ import type { StorageKeyValuePair } from './storage/providers/types';
6
8
  declare const METHOD: {
7
9
  readonly SET: "set";
8
10
  readonly MERGE: "merge";
@@ -194,32 +196,29 @@ declare function evictStorageAndRetry<TMethod extends typeof Onyx.set | typeof O
194
196
  */
195
197
  declare function broadcastUpdate<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, hasChanged?: boolean): Promise<void>;
196
198
  declare function hasPendingMergeForKey(key: OnyxKey): boolean;
197
- type RemoveNullValuesOutput<Value extends OnyxInput<OnyxKey> | undefined> = {
198
- value: Value;
199
- wasRemoved: boolean;
200
- };
201
- /**
202
- * Removes a key from storage if the value is null.
203
- * Otherwise removes all nested null values in objects,
204
- * if shouldRemoveNestedNulls is true and returns the object.
205
- *
206
- * @returns The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely
207
- */
208
- declare function removeNullValues<Value extends OnyxInput<OnyxKey> | undefined>(key: OnyxKey, value: Value, shouldRemoveNestedNulls?: boolean): RemoveNullValuesOutput<Value>;
209
199
  /**
210
200
  * Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
211
201
  * This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
212
202
  * to an array of key-value pairs in the above format and removes key-value pairs that are being set to null
213
-
214
- * @return an array of key - value pairs <[key, value]>
203
+ *
204
+ * @return an array of key - value pairs <[key, value]>
215
205
  */
216
- declare function prepareKeyValuePairsForStorage(data: Record<OnyxKey, OnyxInput<OnyxKey>>, shouldRemoveNestedNulls: boolean): Array<[OnyxKey, OnyxInput<OnyxKey>]>;
206
+ declare function prepareKeyValuePairsForStorage(data: Record<OnyxKey, OnyxInput<OnyxKey>>, shouldRemoveNestedNulls?: boolean, replaceNullPatches?: MultiMergeReplaceNullPatches): StorageKeyValuePair[];
217
207
  /**
218
- * Merges an array of changes with an existing value
208
+ * Merges an array of changes with an existing value or creates a single change.
219
209
  *
220
- * @param changes Array of changes that should be applied to the existing value
210
+ * @param changes Array of changes that should be merged
211
+ * @param existingValue The existing value that should be merged with the changes
221
212
  */
222
- declare function applyMerge<TValue extends OnyxInput<OnyxKey> | undefined, TChange extends OnyxInput<OnyxKey> | undefined>(existingValue: TValue, changes: TChange[], shouldRemoveNestedNulls: boolean): TChange;
213
+ declare function mergeChanges<TValue extends OnyxInput<OnyxKey> | undefined, TChange extends OnyxInput<OnyxKey> | undefined>(changes: TChange[], existingValue?: TValue): FastMergeResult<TChange>;
214
+ /**
215
+ * Merges an array of changes with an existing value or creates a single change.
216
+ * It will also mark deep nested objects that need to be entirely replaced during the merge.
217
+ *
218
+ * @param changes Array of changes that should be merged
219
+ * @param existingValue The existing value that should be merged with the changes
220
+ */
221
+ declare function mergeAndMarkChanges<TValue extends OnyxInput<OnyxKey> | undefined, TChange extends OnyxInput<OnyxKey> | undefined>(changes: TChange[], existingValue?: TValue): FastMergeResult<TChange>;
223
222
  /**
224
223
  * Merge user provided default key value pairs.
225
224
  */
@@ -246,6 +245,19 @@ declare function subscribeToKey<TKey extends OnyxKey>(connectOptions: ConnectOpt
246
245
  */
247
246
  declare function unsubscribeFromKey(subscriptionID: number): void;
248
247
  declare function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<() => Promise<void>>;
248
+ /**
249
+ * Merges a collection based on their keys.
250
+ * Serves as core implementation for `Onyx.mergeCollection()` public function, the difference being
251
+ * that this internal function allows passing an additional `mergeReplaceNullPatches` parameter.
252
+ *
253
+ * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
254
+ * @param collection Object collection keyed by individual collection member keys and values
255
+ * @param mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
256
+ * tuples that we'll use to replace the nested objects of that collection member record with something else.
257
+ */
258
+ declare function mergeCollectionWithPatches<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey, TMap>, mergeReplaceNullPatches?: MultiMergeReplaceNullPatches): Promise<void>;
259
+ declare function logKeyChanged(onyxMethod: Extract<OnyxMethod, 'set' | 'merge'>, key: OnyxKey, value: unknown, hasChanged: boolean): void;
260
+ declare function logKeyRemoved(onyxMethod: Extract<OnyxMethod, 'set' | 'merge'>, key: OnyxKey): void;
249
261
  /**
250
262
  * Clear internal variables used in this file, useful in test environments.
251
263
  */
@@ -288,9 +300,9 @@ declare const OnyxUtils: {
288
300
  evictStorageAndRetry: typeof evictStorageAndRetry;
289
301
  broadcastUpdate: typeof broadcastUpdate;
290
302
  hasPendingMergeForKey: typeof hasPendingMergeForKey;
291
- removeNullValues: typeof removeNullValues;
292
303
  prepareKeyValuePairsForStorage: typeof prepareKeyValuePairsForStorage;
293
- applyMerge: typeof applyMerge;
304
+ mergeChanges: typeof mergeChanges;
305
+ mergeAndMarkChanges: typeof mergeAndMarkChanges;
294
306
  initializeWithDefaultKeyStates: typeof initializeWithDefaultKeyStates;
295
307
  getSnapshotKey: typeof getSnapshotKey;
296
308
  multiGet: typeof multiGet;
@@ -306,6 +318,9 @@ declare const OnyxUtils: {
306
318
  addKeyToRecentlyAccessedIfNeeded: typeof addKeyToRecentlyAccessedIfNeeded;
307
319
  reduceCollectionWithSelector: typeof reduceCollectionWithSelector;
308
320
  updateSnapshots: typeof updateSnapshots;
321
+ mergeCollectionWithPatches: typeof mergeCollectionWithPatches;
322
+ logKeyChanged: typeof logKeyChanged;
323
+ logKeyRemoved: typeof logKeyRemoved;
309
324
  };
310
325
  export type { OnyxMethod };
311
326
  export default OnyxUtils;