Skip to content

Commit 922083b

Browse files
authored
Merge pull request #563 from callstack-internal/hur/improve-native-app-startup
perf: improve native apps startup
2 parents 12235ee + c454e9f commit 922083b

File tree

3 files changed

+99
-89
lines changed

3 files changed

+99
-89
lines changed

lib/Onyx.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,11 @@ function connect<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKey>): nu
155155
}
156156

157157
// We did not opt into using waitForCollectionCallback mode so the callback is called for every matching key.
158-
// eslint-disable-next-line @typescript-eslint/prefer-for-of
159-
for (let i = 0; i < matchingKeys.length; i++) {
160-
OnyxUtils.get(matchingKeys[i]).then((val) => OnyxUtils.sendDataToConnection(mapping, val as OnyxValue<TKey>, matchingKeys[i] as TKey, true));
161-
}
158+
OnyxUtils.multiGet(matchingKeys).then((values) => {
159+
values.forEach((val, key) => {
160+
OnyxUtils.sendDataToConnection(mapping, val as OnyxValue<TKey>, key as TKey, true);
161+
});
162+
});
162163
return;
163164
}
164165

@@ -438,7 +439,8 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
438439

439440
// Confirm all the collection keys belong to the same parent
440441
let hasCollectionKeyCheckFailed = false;
441-
Object.keys(mergedCollection).forEach((dataKey) => {
442+
const mergedCollectionKeys = Object.keys(mergedCollection);
443+
mergedCollectionKeys.forEach((dataKey) => {
442444
if (OnyxUtils.isKeyMatch(collectionKey, dataKey)) {
443445
return;
444446
}
@@ -459,7 +461,7 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
459461
return OnyxUtils.getAllKeys()
460462
.then((persistedKeys) => {
461463
// Split to keys that exist in storage and keys that don't
462-
const keys = Object.keys(mergedCollection).filter((key) => {
464+
const keys = mergedCollectionKeys.filter((key) => {
463465
if (mergedCollection[key] === null) {
464466
OnyxUtils.remove(key);
465467
return false;
@@ -471,8 +473,6 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
471473

472474
const cachedCollectionForExistingKeys = OnyxUtils.getCachedCollection(collectionKey, existingKeys);
473475

474-
const newKeys = keys.filter((key) => !persistedKeys.has(key));
475-
476476
const existingKeyCollection = existingKeys.reduce((obj: OnyxInputKeyValueMapping, key) => {
477477
const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(mergedCollection[key], cachedCollectionForExistingKeys[key]);
478478
if (!isCompatible) {
@@ -484,11 +484,13 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
484484
return obj;
485485
}, {}) as Record<OnyxKey, OnyxInput<TKey>>;
486486

487-
const newCollection = newKeys.reduce((obj: OnyxInputKeyValueMapping, key) => {
488-
// eslint-disable-next-line no-param-reassign
489-
obj[key] = mergedCollection[key];
490-
return obj;
491-
}, {}) as Record<OnyxKey, OnyxInput<TKey>>;
487+
const newCollection: Record<OnyxKey, OnyxInput<TKey>> = {};
488+
keys.forEach((key) => {
489+
if (persistedKeys.has(key)) {
490+
return;
491+
}
492+
newCollection[key] = mergedCollection[key];
493+
});
492494

493495
// When (multi-)merging the values with the existing values in storage,
494496
// we don't want to remove nested null values from the data that we pass to the storage layer,

lib/OnyxUtils.ts

Lines changed: 78 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,80 @@ function get<TKey extends OnyxKey, TValue extends OnyxValue<TKey>>(key: TKey): P
245245
return cache.captureTask(taskName, promise) as Promise<TValue>;
246246
}
247247

248+
// multiGet the data first from the cache and then from the storage for the missing keys.
249+
function multiGet<TKey extends OnyxKey>(keys: CollectionKeyBase[]): Promise<Map<OnyxKey, OnyxValue<TKey>>> {
250+
// Keys that are not in the cache
251+
const missingKeys: OnyxKey[] = [];
252+
253+
// Tasks that are pending
254+
const pendingTasks: Array<Promise<OnyxValue<TKey>>> = [];
255+
256+
// Keys for the tasks that are pending
257+
const pendingKeys: OnyxKey[] = [];
258+
259+
// Data to be sent back to the invoker
260+
const dataMap = new Map<OnyxKey, OnyxValue<TKey>>();
261+
262+
/**
263+
* We are going to iterate over all the matching keys and check if we have the data in the cache.
264+
* If we do then we add it to the data object. If we do not have them, then we check if there is a pending task
265+
* for the key. If there is such task, then we add the promise to the pendingTasks array and the key to the pendingKeys
266+
* array. If there is no pending task then we add the key to the missingKeys array.
267+
*
268+
* These missingKeys will be later used to multiGet the data from the storage.
269+
*/
270+
keys.forEach((key) => {
271+
const cacheValue = cache.get(key) as OnyxValue<TKey>;
272+
if (cacheValue) {
273+
dataMap.set(key, cacheValue);
274+
return;
275+
}
276+
277+
const pendingKey = `get:${key}`;
278+
if (cache.hasPendingTask(pendingKey)) {
279+
pendingTasks.push(cache.getTaskPromise(pendingKey) as Promise<OnyxValue<TKey>>);
280+
pendingKeys.push(key);
281+
} else {
282+
missingKeys.push(key);
283+
}
284+
});
285+
286+
return (
287+
Promise.all(pendingTasks)
288+
// Wait for all the pending tasks to resolve and then add the data to the data map.
289+
.then((values) => {
290+
values.forEach((value, index) => {
291+
dataMap.set(pendingKeys[index], value);
292+
});
293+
294+
return Promise.resolve();
295+
})
296+
// Get the missing keys using multiGet from the storage.
297+
.then(() => {
298+
if (missingKeys.length === 0) {
299+
return Promise.resolve(undefined);
300+
}
301+
302+
return Storage.multiGet(missingKeys);
303+
})
304+
// Add the data from the missing keys to the data map and also merge it to the cache.
305+
.then((values) => {
306+
if (!values || values.length === 0) {
307+
return dataMap;
308+
}
309+
310+
// temp object is used to merge the missing data into the cache
311+
const temp: OnyxCollection<KeyValueMapping[TKey]> = {};
312+
values.forEach(([key, value]) => {
313+
dataMap.set(key, value as OnyxValue<TKey>);
314+
temp[key] = value as OnyxValue<TKey>;
315+
});
316+
cache.merge(temp);
317+
return dataMap;
318+
})
319+
);
320+
}
321+
248322
/** Returns current key names stored in persisted storage */
249323
function getAllKeys(): Promise<Set<OnyxKey>> {
250324
// When we've already read stored keys, resolve right away
@@ -843,75 +917,10 @@ function addKeyToRecentlyAccessedIfNeeded<TKey extends OnyxKey>(mapping: Mapping
843917
* Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.
844918
*/
845919
function getCollectionDataAndSendAsObject<TKey extends OnyxKey>(matchingKeys: CollectionKeyBase[], mapping: Mapping<TKey>): void {
846-
// Keys that are not in the cache
847-
const missingKeys: OnyxKey[] = [];
848-
// Tasks that are pending
849-
const pendingTasks: Array<Promise<OnyxValue<TKey>>> = [];
850-
// Keys for the tasks that are pending
851-
const pendingKeys: OnyxKey[] = [];
852-
853-
// We are going to combine all the data from the matching keys into a single object
854-
const data: OnyxCollection<KeyValueMapping[TKey]> = {};
855-
856-
/**
857-
* We are going to iterate over all the matching keys and check if we have the data in the cache.
858-
* If we do then we add it to the data object. If we do not then we check if there is a pending task
859-
* for the key. If there is then we add the promise to the pendingTasks array and the key to the pendingKeys
860-
* array. If there is no pending task then we add the key to the missingKeys array.
861-
*
862-
* These missingKeys will be later to use to multiGet the data from the storage.
863-
*/
864-
matchingKeys.forEach((key) => {
865-
const cacheValue = cache.get(key) as OnyxValue<TKey>;
866-
if (cacheValue) {
867-
data[key] = cacheValue;
868-
return;
869-
}
870-
871-
const pendingKey = `get:${key}`;
872-
if (cache.hasPendingTask(pendingKey)) {
873-
pendingTasks.push(cache.getTaskPromise(pendingKey) as Promise<OnyxValue<TKey>>);
874-
pendingKeys.push(key);
875-
} else {
876-
missingKeys.push(key);
877-
}
920+
multiGet(matchingKeys).then((dataMap) => {
921+
const data = Object.fromEntries(dataMap.entries()) as OnyxValue<TKey>;
922+
sendDataToConnection(mapping, data, undefined, true);
878923
});
879-
880-
Promise.all(pendingTasks)
881-
// We are going to wait for all the pending tasks to resolve and then add the data to the data object.
882-
.then((values) => {
883-
values.forEach((value, index) => {
884-
data[pendingKeys[index]] = value;
885-
});
886-
887-
return Promise.resolve();
888-
})
889-
// We are going to get the missing keys using multiGet from the storage.
890-
.then(() => {
891-
if (missingKeys.length === 0) {
892-
return Promise.resolve(undefined);
893-
}
894-
return Storage.multiGet(missingKeys);
895-
})
896-
// We are going to add the data from the missing keys to the data object and also merge it to the cache.
897-
.then((values) => {
898-
if (!values || values.length === 0) {
899-
return Promise.resolve();
900-
}
901-
902-
// temp object is used to merge the missing data into the cache
903-
const temp: OnyxCollection<KeyValueMapping[TKey]> = {};
904-
values.forEach(([key, value]) => {
905-
data[key] = value as OnyxValue<TKey>;
906-
temp[key] = value as OnyxValue<TKey>;
907-
});
908-
cache.merge(temp);
909-
return Promise.resolve();
910-
})
911-
// We are going to send the data to the subscriber.
912-
.finally(() => {
913-
sendDataToConnection(mapping, data as OnyxValue<TKey>, undefined, true);
914-
});
915924
}
916925

917926
/**
@@ -1150,6 +1159,7 @@ const OnyxUtils = {
11501159
applyMerge,
11511160
initializeWithDefaultKeyStates,
11521161
getSnapshotKey,
1162+
multiGet,
11531163
};
11541164

11551165
export default OnyxUtils;

lib/utils.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function isEmptyObject<T>(obj: T | EmptyValue): obj is EmptyValue {
1717
*/
1818
function isMergeableObject(value: unknown): value is Record<string, unknown> {
1919
const isNonNullObject = value != null ? typeof value === 'object' : false;
20-
return isNonNullObject && Object.prototype.toString.call(value) !== '[object RegExp]' && Object.prototype.toString.call(value) !== '[object Date]' && !Array.isArray(value);
20+
return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value);
2121
}
2222

2323
/**
@@ -37,9 +37,8 @@ function mergeObject<TObject extends Record<string, unknown>>(target: TObject |
3737
// If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object
3838
// and therefore we need to omit keys where either the source or target value is null.
3939
if (targetObject) {
40-
const targetKeys = Object.keys(targetObject);
41-
for (let i = 0; i < targetKeys.length; ++i) {
42-
const key = targetKeys[i];
40+
// eslint-disable-next-line no-restricted-syntax, guard-for-in
41+
for (const key in targetObject) {
4342
const sourceValue = source?.[key];
4443
const targetValue = targetObject?.[key];
4544

@@ -58,10 +57,9 @@ function mergeObject<TObject extends Record<string, unknown>>(target: TObject |
5857
}
5958

6059
// After copying over all keys from the target object, we want to merge the source object into the destination object.
61-
const sourceKeys = Object.keys(source);
62-
for (let i = 0; i < sourceKeys.length; ++i) {
63-
const key = sourceKeys[i];
64-
const sourceValue = source?.[key];
60+
// eslint-disable-next-line no-restricted-syntax, guard-for-in
61+
for (const key in source) {
62+
const sourceValue = source?.[key] as Record<string, unknown>;
6563
const targetValue = targetObject?.[key];
6664

6765
// If undefined is passed as the source value for a key, we want to generally ignore it.

0 commit comments

Comments
 (0)