Skip to content

Commit 895679a

Browse files
authored
Merge pull request #677 from shubham1206agra/update-optimization-1
Implemented partialSetCollection
2 parents 6978c02 + d230066 commit 895679a

File tree

5 files changed

+189
-5
lines changed

5 files changed

+189
-5
lines changed

API-INTERNAL.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<dt><a href="#setSkippableCollectionMemberIDs">setSkippableCollectionMemberIDs()</a></dt>
2424
<dd><p>Setter - sets the skippable collection member IDs.</p>
2525
</dd>
26-
<dt><a href="#initStoreValues">initStoreValues(keys, initialKeyStates, evictableKeys, fullyMergedSnapshotKeys)</a></dt>
26+
<dt><a href="#initStoreValues">initStoreValues(keys, initialKeyStates, evictableKeys)</a></dt>
2727
<dd><p>Sets the initial values for the Onyx store</p>
2828
</dd>
2929
<dt><a href="#maybeFlushBatchUpdates">maybeFlushBatchUpdates()</a></dt>
@@ -154,6 +154,10 @@ It will also mark deep nested objects that need to be entirely replaced during t
154154
Serves as core implementation for <code>Onyx.mergeCollection()</code> public function, the difference being
155155
that this internal function allows passing an additional <code>mergeReplaceNullPatches</code> parameter.</p>
156156
</dd>
157+
<dt><a href="#partialSetCollection">partialSetCollection(collectionKey, collection)</a></dt>
158+
<dd><p>Sets keys in a collection by replacing all targeted collection members with new values.
159+
Any existing collection members not included in the new data will not be removed.</p>
160+
</dd>
157161
<dt><a href="#clearOnyxUtilsInternals">clearOnyxUtilsInternals()</a></dt>
158162
<dd><p>Clear internal variables used in this file, useful in test environments.</p>
159163
</dd>
@@ -197,7 +201,7 @@ Setter - sets the skippable collection member IDs.
197201
**Kind**: global function
198202
<a name="initStoreValues"></a>
199203

200-
## initStoreValues(keys, initialKeyStates, evictableKeys, fullyMergedSnapshotKeys)
204+
## initStoreValues(keys, initialKeyStates, evictableKeys)
201205
Sets the initial values for the Onyx store
202206

203207
**Kind**: global function
@@ -207,7 +211,6 @@ Sets the initial values for the Onyx store
207211
| keys | `ONYXKEYS` constants object from Onyx.init() |
208212
| initialKeyStates | initial data to set when `init()` and `clear()` are called |
209213
| evictableKeys | This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged as "safe" for removal. |
210-
| fullyMergedSnapshotKeys | Array of snapshot collection keys where full merge is supported and data structure can be changed after merge. |
211214

212215
<a name="maybeFlushBatchUpdates"></a>
213216

@@ -523,6 +526,19 @@ that this internal function allows passing an additional `mergeReplaceNullPatche
523526
| collection | Object collection keyed by individual collection member keys and values |
524527
| 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. |
525528

529+
<a name="partialSetCollection"></a>
530+
531+
## partialSetCollection(collectionKey, collection)
532+
Sets keys in a collection by replacing all targeted collection members with new values.
533+
Any existing collection members not included in the new data will not be removed.
534+
535+
**Kind**: global function
536+
537+
| Param | Description |
538+
| --- | --- |
539+
| collectionKey | e.g. `ONYXKEYS.COLLECTION.REPORT` |
540+
| collection | Object collection keyed by individual collection member keys and values |
541+
526542
<a name="clearOnyxUtilsInternals"></a>
527543

528544
## clearOnyxUtilsInternals()

lib/Onyx.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ function update(data: OnyxUpdate[]): Promise<void> {
629629
);
630630
}
631631
if (!utils.isEmptyObject(batchedCollectionUpdates.set)) {
632-
promises.push(() => multiSet(batchedCollectionUpdates.set));
632+
promises.push(() => OnyxUtils.partialSetCollection(collectionKey, batchedCollectionUpdates.set as Collection<CollectionKey, unknown, unknown>));
633633
}
634634
});
635635

lib/OnyxUtils.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,6 +1616,60 @@ function mergeCollectionWithPatches<TKey extends CollectionKeyBase, TMap>(
16161616
.then(() => undefined);
16171617
}
16181618

1619+
/**
1620+
* Sets keys in a collection by replacing all targeted collection members with new values.
1621+
* Any existing collection members not included in the new data will not be removed.
1622+
*
1623+
* @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
1624+
* @param collection Object collection keyed by individual collection member keys and values
1625+
*/
1626+
function partialSetCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey, TMap>): Promise<void> {
1627+
let resultCollection: OnyxInputKeyValueMapping = collection;
1628+
let resultCollectionKeys = Object.keys(resultCollection);
1629+
1630+
// Confirm all the collection keys belong to the same parent
1631+
if (!doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) {
1632+
Logger.logAlert(`setCollection called with keys that do not belong to the same parent ${collectionKey}. Skipping this update.`);
1633+
return Promise.resolve();
1634+
}
1635+
1636+
if (skippableCollectionMemberIDs.size) {
1637+
resultCollection = resultCollectionKeys.reduce((result: OnyxInputKeyValueMapping, key) => {
1638+
try {
1639+
const [, collectionMemberID] = splitCollectionMemberKey(key, collectionKey);
1640+
// If the collection member key is a skippable one we set its value to null.
1641+
// eslint-disable-next-line no-param-reassign
1642+
result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null;
1643+
} catch {
1644+
// Something went wrong during split, so we assign the data to result anyway.
1645+
// eslint-disable-next-line no-param-reassign
1646+
result[key] = resultCollection[key];
1647+
}
1648+
1649+
return result;
1650+
}, {});
1651+
}
1652+
resultCollectionKeys = Object.keys(resultCollection);
1653+
1654+
return getAllKeys().then((persistedKeys) => {
1655+
const mutableCollection: OnyxInputKeyValueMapping = {...resultCollection};
1656+
const existingKeys = resultCollectionKeys.filter((key) => persistedKeys.has(key));
1657+
const previousCollection = getCachedCollection(collectionKey, existingKeys);
1658+
const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true);
1659+
1660+
keyValuePairs.forEach(([key, value]) => cache.set(key, value));
1661+
1662+
const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
1663+
1664+
return Storage.multiSet(keyValuePairs)
1665+
.catch((error) => evictStorageAndRetry(error, partialSetCollection, collectionKey, collection))
1666+
.then(() => {
1667+
sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
1668+
return updatePromise;
1669+
});
1670+
});
1671+
}
1672+
16191673
function logKeyChanged(onyxMethod: Extract<OnyxMethod, 'set' | 'merge'>, key: OnyxKey, value: unknown, hasChanged: boolean) {
16201674
Logger.logInfo(`${onyxMethod} called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''} hasChanged: ${hasChanged}`);
16211675
}
@@ -1686,6 +1740,7 @@ const OnyxUtils = {
16861740
reduceCollectionWithSelector,
16871741
updateSnapshots,
16881742
mergeCollectionWithPatches,
1743+
partialSetCollection,
16891744
logKeyChanged,
16901745
logKeyRemoved,
16911746
};

tests/perf-test/OnyxUtils.perf-test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {measureAsyncFunction, measureFunction} from 'reassure';
2+
import {randBoolean} from '@ngneat/falso';
23
import createRandomReportAction, {getRandomReportActions} from '../utils/collections/reportActions';
34
import type {OnyxKey, Selector} from '../../lib';
45
import Onyx from '../../lib';
@@ -306,6 +307,20 @@ describe('OnyxUtils', () => {
306307
});
307308
});
308309

310+
describe('partialSetCollection', () => {
311+
test('one call with 10k heavy objects', async () => {
312+
const changedReportActions = Object.fromEntries(
313+
Object.entries(mockedReportActionsMap).map(([k, v]) => [k, randBoolean() ? v : createRandomReportAction(Number(v.reportActionID))] as const),
314+
) as GenericCollection;
315+
await measureAsyncFunction(() => OnyxUtils.partialSetCollection(collectionKey, changedReportActions), {
316+
beforeEach: async () => {
317+
await Onyx.setCollection(collectionKey, mockedReportActionsMap as GenericCollection);
318+
},
319+
afterEach: clearOnyxAfterEachMeasure,
320+
});
321+
});
322+
});
323+
309324
describe('keysChanged', () => {
310325
test('one call with 10k heavy objects to update 10k subscribers', async () => {
311326
const subscriptionMap = new Map<string, number>();

tests/unit/onyxUtilsTest.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import Onyx from '../../lib';
22
import OnyxUtils from '../../lib/OnyxUtils';
33
import type {GenericDeepRecord} from '../types';
44
import utils from '../../lib/utils';
5-
import type {Collection} from '../../lib/types';
5+
import type {Collection, OnyxCollection} from '../../lib/types';
6+
import type GenericCollection from '../utils/GenericCollection';
67

78
const testObject: GenericDeepRecord = {
89
a: 'a',
@@ -71,6 +72,7 @@ const ONYXKEYS = {
7172
TEST_KEY: 'test_',
7273
TEST_LEVEL_KEY: 'test_level_',
7374
TEST_LEVEL_LAST_KEY: 'test_level_last_',
75+
ROUTES: 'routes_',
7476
},
7577
};
7678

@@ -123,6 +125,102 @@ describe('OnyxUtils', () => {
123125
});
124126
});
125127

128+
describe('partialSetCollection', () => {
129+
beforeEach(() => {
130+
Onyx.clear();
131+
});
132+
133+
afterEach(() => {
134+
Onyx.clear();
135+
});
136+
it('should replace all existing collection members with new values and keep old ones intact', async () => {
137+
let result: OnyxCollection<unknown>;
138+
const routeA = `${ONYXKEYS.COLLECTION.ROUTES}A`;
139+
const routeB = `${ONYXKEYS.COLLECTION.ROUTES}B`;
140+
const routeB1 = `${ONYXKEYS.COLLECTION.ROUTES}B1`;
141+
const routeC = `${ONYXKEYS.COLLECTION.ROUTES}C`;
142+
143+
const connection = Onyx.connect({
144+
key: ONYXKEYS.COLLECTION.ROUTES,
145+
initWithStoredValues: false,
146+
callback: (value) => (result = value),
147+
waitForCollectionCallback: true,
148+
});
149+
150+
// Set initial collection state
151+
await Onyx.setCollection(ONYXKEYS.COLLECTION.ROUTES, {
152+
[routeA]: {name: 'Route A'},
153+
[routeB1]: {name: 'Route B1'},
154+
[routeC]: {name: 'Route C'},
155+
} as GenericCollection);
156+
157+
// Replace with new collection data
158+
await OnyxUtils.partialSetCollection(ONYXKEYS.COLLECTION.ROUTES, {
159+
[routeA]: {name: 'New Route A'},
160+
[routeB]: {name: 'New Route B'},
161+
[routeC]: {name: 'New Route C'},
162+
} as GenericCollection);
163+
164+
expect(result).toEqual({
165+
[routeA]: {name: 'New Route A'},
166+
[routeB]: {name: 'New Route B'},
167+
[routeB1]: {name: 'Route B1'},
168+
[routeC]: {name: 'New Route C'},
169+
});
170+
await Onyx.disconnect(connection);
171+
});
172+
173+
it('should not replace anything in the collection with empty values', async () => {
174+
let result: OnyxCollection<unknown>;
175+
const routeA = `${ONYXKEYS.COLLECTION.ROUTES}A`;
176+
177+
const connection = Onyx.connect({
178+
key: ONYXKEYS.COLLECTION.ROUTES,
179+
initWithStoredValues: false,
180+
callback: (value) => (result = value),
181+
waitForCollectionCallback: true,
182+
});
183+
184+
await Onyx.mergeCollection(ONYXKEYS.COLLECTION.ROUTES, {
185+
[routeA]: {name: 'Route A'},
186+
} as GenericCollection);
187+
188+
await OnyxUtils.partialSetCollection(ONYXKEYS.COLLECTION.ROUTES, {} as GenericCollection);
189+
190+
expect(result).toEqual({
191+
[routeA]: {name: 'Route A'},
192+
});
193+
await Onyx.disconnect(connection);
194+
});
195+
196+
it('should reject collection items with invalid keys', async () => {
197+
let result: OnyxCollection<unknown>;
198+
const routeA = `${ONYXKEYS.COLLECTION.ROUTES}A`;
199+
const invalidRoute = 'invalid_route';
200+
201+
const connection = Onyx.connect({
202+
key: ONYXKEYS.COLLECTION.ROUTES,
203+
initWithStoredValues: false,
204+
callback: (value) => (result = value),
205+
waitForCollectionCallback: true,
206+
});
207+
208+
await Onyx.mergeCollection(ONYXKEYS.COLLECTION.ROUTES, {
209+
[routeA]: {name: 'Route A'},
210+
} as GenericCollection);
211+
212+
await OnyxUtils.partialSetCollection(ONYXKEYS.COLLECTION.ROUTES, {
213+
[invalidRoute]: {name: 'Invalid Route'},
214+
} as GenericCollection);
215+
216+
expect(result).toEqual({
217+
[routeA]: {name: 'Route A'},
218+
});
219+
220+
await Onyx.disconnect(connection);
221+
});
222+
});
223+
126224
describe('keysChanged', () => {
127225
beforeEach(() => {
128226
Onyx.clear();

0 commit comments

Comments
 (0)