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 +2 -2
- package/README.md +27 -0
- package/dist/Onyx.d.ts +5 -4
- package/dist/Onyx.js +38 -153
- 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 +19 -1
- package/dist/utils.d.ts +34 -6
- package/dist/utils.js +92 -64
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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 (
|
|
163
|
-
|
|
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
|
|
167
|
-
const hasChanged = OnyxCache_1.default.hasValueChanged(key,
|
|
168
|
-
|
|
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,
|
|
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,
|
|
176
|
-
.catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, set, key,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
308
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
|
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] =
|
|
512
|
+
queue.set[key] = batchedChanges.result;
|
|
635
513
|
}
|
|
636
514
|
else {
|
|
637
515
|
// eslint-disable-next-line no-param-reassign
|
|
638
|
-
queue.merge[key] =
|
|
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(() =>
|
|
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
|
-
|
|
658
|
-
|
|
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,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,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 };
|
package/dist/OnyxUtils.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|