diff --git a/API-INTERNAL.md b/API-INTERNAL.md
index 8da520402..3f63741aa 100644
--- a/API-INTERNAL.md
+++ b/API-INTERNAL.md
@@ -23,7 +23,7 @@
setSkippableCollectionMemberIDs()
Setter - sets the skippable collection member IDs.
-initStoreValues(keys, initialKeyStates, evictableKeys, fullyMergedSnapshotKeys)
+initStoreValues(keys, initialKeyStates, evictableKeys)
Sets the initial values for the Onyx store
maybeFlushBatchUpdates()
@@ -154,6 +154,10 @@ It will also mark deep nested objects that need to be entirely replaced during t
Serves as core implementation for Onyx.mergeCollection() public function, the difference being
that this internal function allows passing an additional mergeReplaceNullPatches parameter.
+partialSetCollection(collectionKey, collection)
+Sets keys in a collection by replacing all targeted collection members with new values.
+Any existing collection members not included in the new data will not be removed.
+
clearOnyxUtilsInternals()
Clear internal variables used in this file, useful in test environments.
@@ -197,7 +201,7 @@ Setter - sets the skippable collection member IDs.
**Kind**: global function
-## initStoreValues(keys, initialKeyStates, evictableKeys, fullyMergedSnapshotKeys)
+## initStoreValues(keys, initialKeyStates, evictableKeys)
Sets the initial values for the Onyx store
**Kind**: global function
@@ -207,7 +211,6 @@ Sets the initial values for the Onyx store
| keys | `ONYXKEYS` constants object from Onyx.init() |
| initialKeyStates | initial data to set when `init()` and `clear()` are called |
| evictableKeys | This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged as "safe" for removal. |
-| fullyMergedSnapshotKeys | Array of snapshot collection keys where full merge is supported and data structure can be changed after merge. |
@@ -523,6 +526,19 @@ that this internal function allows passing an additional `mergeReplaceNullPatche
| collection | Object collection keyed by individual collection member keys and values |
| mergeReplaceNullPatches | Record where the key is a collection member key and the value is a list of tuples that we'll use to replace the nested objects of that collection member record with something else. |
+
+
+## partialSetCollection(collectionKey, collection)
+Sets keys in a collection by replacing all targeted collection members with new values.
+Any existing collection members not included in the new data will not be removed.
+
+**Kind**: global function
+
+| Param | Description |
+| --- | --- |
+| collectionKey | e.g. `ONYXKEYS.COLLECTION.REPORT` |
+| collection | Object collection keyed by individual collection member keys and values |
+
## clearOnyxUtilsInternals()
diff --git a/lib/Onyx.ts b/lib/Onyx.ts
index 5b9065c7e..be2878f6a 100644
--- a/lib/Onyx.ts
+++ b/lib/Onyx.ts
@@ -629,7 +629,7 @@ function update(data: OnyxUpdate[]): Promise {
);
}
if (!utils.isEmptyObject(batchedCollectionUpdates.set)) {
- promises.push(() => multiSet(batchedCollectionUpdates.set));
+ promises.push(() => OnyxUtils.partialSetCollection(collectionKey, batchedCollectionUpdates.set as Collection));
}
});
diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts
index a288b1a19..b6e1bf8db 100644
--- a/lib/OnyxUtils.ts
+++ b/lib/OnyxUtils.ts
@@ -1616,6 +1616,60 @@ function mergeCollectionWithPatches(
.then(() => undefined);
}
+/**
+ * Sets keys in a collection by replacing all targeted collection members with new values.
+ * Any existing collection members not included in the new data will not be removed.
+ *
+ * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
+ * @param collection Object collection keyed by individual collection member keys and values
+ */
+function partialSetCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise {
+ let resultCollection: OnyxInputKeyValueMapping = collection;
+ let resultCollectionKeys = Object.keys(resultCollection);
+
+ // Confirm all the collection keys belong to the same parent
+ if (!doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) {
+ Logger.logAlert(`setCollection called with keys that do not belong to the same parent ${collectionKey}. Skipping this update.`);
+ return Promise.resolve();
+ }
+
+ if (skippableCollectionMemberIDs.size) {
+ resultCollection = resultCollectionKeys.reduce((result: OnyxInputKeyValueMapping, key) => {
+ try {
+ const [, collectionMemberID] = splitCollectionMemberKey(key, collectionKey);
+ // If the collection member key is a skippable one we set its value to null.
+ // eslint-disable-next-line no-param-reassign
+ result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null;
+ } catch {
+ // Something went wrong during split, so we assign the data to result anyway.
+ // eslint-disable-next-line no-param-reassign
+ result[key] = resultCollection[key];
+ }
+
+ return result;
+ }, {});
+ }
+ resultCollectionKeys = Object.keys(resultCollection);
+
+ return getAllKeys().then((persistedKeys) => {
+ const mutableCollection: OnyxInputKeyValueMapping = {...resultCollection};
+ const existingKeys = resultCollectionKeys.filter((key) => persistedKeys.has(key));
+ const previousCollection = getCachedCollection(collectionKey, existingKeys);
+ const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true);
+
+ keyValuePairs.forEach(([key, value]) => cache.set(key, value));
+
+ const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
+
+ return Storage.multiSet(keyValuePairs)
+ .catch((error) => evictStorageAndRetry(error, partialSetCollection, collectionKey, collection))
+ .then(() => {
+ sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
+ return updatePromise;
+ });
+ });
+}
+
function logKeyChanged(onyxMethod: Extract, key: OnyxKey, value: unknown, hasChanged: boolean) {
Logger.logInfo(`${onyxMethod} called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''} hasChanged: ${hasChanged}`);
}
@@ -1686,6 +1740,7 @@ const OnyxUtils = {
reduceCollectionWithSelector,
updateSnapshots,
mergeCollectionWithPatches,
+ partialSetCollection,
logKeyChanged,
logKeyRemoved,
};
diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts
index 9b6f0bcaf..399b53ba3 100644
--- a/tests/perf-test/OnyxUtils.perf-test.ts
+++ b/tests/perf-test/OnyxUtils.perf-test.ts
@@ -1,4 +1,5 @@
import {measureAsyncFunction, measureFunction} from 'reassure';
+import {randBoolean} from '@ngneat/falso';
import createRandomReportAction, {getRandomReportActions} from '../utils/collections/reportActions';
import type {OnyxKey, Selector} from '../../lib';
import Onyx from '../../lib';
@@ -306,6 +307,20 @@ describe('OnyxUtils', () => {
});
});
+ describe('partialSetCollection', () => {
+ test('one call with 10k heavy objects', async () => {
+ const changedReportActions = Object.fromEntries(
+ Object.entries(mockedReportActionsMap).map(([k, v]) => [k, randBoolean() ? v : createRandomReportAction(Number(v.reportActionID))] as const),
+ ) as GenericCollection;
+ await measureAsyncFunction(() => OnyxUtils.partialSetCollection(collectionKey, changedReportActions), {
+ beforeEach: async () => {
+ await Onyx.setCollection(collectionKey, mockedReportActionsMap as GenericCollection);
+ },
+ afterEach: clearOnyxAfterEachMeasure,
+ });
+ });
+ });
+
describe('keysChanged', () => {
test('one call with 10k heavy objects to update 10k subscribers', async () => {
const subscriptionMap = new Map();
diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts
index 2024238aa..7d2679392 100644
--- a/tests/unit/onyxUtilsTest.ts
+++ b/tests/unit/onyxUtilsTest.ts
@@ -2,7 +2,8 @@ import Onyx from '../../lib';
import OnyxUtils from '../../lib/OnyxUtils';
import type {GenericDeepRecord} from '../types';
import utils from '../../lib/utils';
-import type {Collection} from '../../lib/types';
+import type {Collection, OnyxCollection} from '../../lib/types';
+import type GenericCollection from '../utils/GenericCollection';
const testObject: GenericDeepRecord = {
a: 'a',
@@ -71,6 +72,7 @@ const ONYXKEYS = {
TEST_KEY: 'test_',
TEST_LEVEL_KEY: 'test_level_',
TEST_LEVEL_LAST_KEY: 'test_level_last_',
+ ROUTES: 'routes_',
},
};
@@ -123,6 +125,102 @@ describe('OnyxUtils', () => {
});
});
+ describe('partialSetCollection', () => {
+ beforeEach(() => {
+ Onyx.clear();
+ });
+
+ afterEach(() => {
+ Onyx.clear();
+ });
+ it('should replace all existing collection members with new values and keep old ones intact', async () => {
+ let result: OnyxCollection;
+ const routeA = `${ONYXKEYS.COLLECTION.ROUTES}A`;
+ const routeB = `${ONYXKEYS.COLLECTION.ROUTES}B`;
+ const routeB1 = `${ONYXKEYS.COLLECTION.ROUTES}B1`;
+ const routeC = `${ONYXKEYS.COLLECTION.ROUTES}C`;
+
+ const connection = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.ROUTES,
+ initWithStoredValues: false,
+ callback: (value) => (result = value),
+ waitForCollectionCallback: true,
+ });
+
+ // Set initial collection state
+ await Onyx.setCollection(ONYXKEYS.COLLECTION.ROUTES, {
+ [routeA]: {name: 'Route A'},
+ [routeB1]: {name: 'Route B1'},
+ [routeC]: {name: 'Route C'},
+ } as GenericCollection);
+
+ // Replace with new collection data
+ await OnyxUtils.partialSetCollection(ONYXKEYS.COLLECTION.ROUTES, {
+ [routeA]: {name: 'New Route A'},
+ [routeB]: {name: 'New Route B'},
+ [routeC]: {name: 'New Route C'},
+ } as GenericCollection);
+
+ expect(result).toEqual({
+ [routeA]: {name: 'New Route A'},
+ [routeB]: {name: 'New Route B'},
+ [routeB1]: {name: 'Route B1'},
+ [routeC]: {name: 'New Route C'},
+ });
+ await Onyx.disconnect(connection);
+ });
+
+ it('should not replace anything in the collection with empty values', async () => {
+ let result: OnyxCollection;
+ const routeA = `${ONYXKEYS.COLLECTION.ROUTES}A`;
+
+ const connection = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.ROUTES,
+ initWithStoredValues: false,
+ callback: (value) => (result = value),
+ waitForCollectionCallback: true,
+ });
+
+ await Onyx.mergeCollection(ONYXKEYS.COLLECTION.ROUTES, {
+ [routeA]: {name: 'Route A'},
+ } as GenericCollection);
+
+ await OnyxUtils.partialSetCollection(ONYXKEYS.COLLECTION.ROUTES, {} as GenericCollection);
+
+ expect(result).toEqual({
+ [routeA]: {name: 'Route A'},
+ });
+ await Onyx.disconnect(connection);
+ });
+
+ it('should reject collection items with invalid keys', async () => {
+ let result: OnyxCollection;
+ const routeA = `${ONYXKEYS.COLLECTION.ROUTES}A`;
+ const invalidRoute = 'invalid_route';
+
+ const connection = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.ROUTES,
+ initWithStoredValues: false,
+ callback: (value) => (result = value),
+ waitForCollectionCallback: true,
+ });
+
+ await Onyx.mergeCollection(ONYXKEYS.COLLECTION.ROUTES, {
+ [routeA]: {name: 'Route A'},
+ } as GenericCollection);
+
+ await OnyxUtils.partialSetCollection(ONYXKEYS.COLLECTION.ROUTES, {
+ [invalidRoute]: {name: 'Invalid Route'},
+ } as GenericCollection);
+
+ expect(result).toEqual({
+ [routeA]: {name: 'Route A'},
+ });
+
+ await Onyx.disconnect(connection);
+ });
+ });
+
describe('keysChanged', () => {
beforeEach(() => {
Onyx.clear();