Skip to content

Commit ca4a4ac

Browse files
fabioh8010TMisiukiewicz
authored andcommitted
Load all storage data into cache during init
1 parent 434871e commit ca4a4ac

11 files changed

Lines changed: 112 additions & 383 deletions

File tree

lib/OnyxUtils.ts

Lines changed: 37 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import _ from 'underscore';
44
import DevTools from './DevTools';
55
import * as Logger from './Logger';
66
import type Onyx from './Onyx';
7-
import cache, {TASK} from './OnyxCache';
7+
import cache from './OnyxCache';
88
import * as Str from './Str';
99
import Storage from './storage';
1010
import type {
@@ -262,143 +262,22 @@ function get<TKey extends OnyxKey, TValue extends OnyxValue<TKey>>(key: TKey): P
262262
return Promise.resolve(cache.get(key) as TValue);
263263
}
264264

265-
// RAM-only keys should never read from storage (they may have stale persisted data
266-
// from before the key was migrated to RAM-only). Mark as nullish so future get() calls
267-
// short-circuit via hasCacheForKey and avoid re-running this branch.
268-
if (isRamOnlyKey(key)) {
269-
cache.addNullishStorageKey(key);
270-
return Promise.resolve(undefined as TValue);
271-
}
272-
273-
const taskName = `${TASK.GET}:${key}` as const;
274-
275-
// When a value retrieving task for this key is still running hook to it
276-
if (cache.hasPendingTask(taskName)) {
277-
return cache.getTaskPromise(taskName) as Promise<TValue>;
278-
}
279-
280-
// Otherwise retrieve the value from storage and capture a promise to aid concurrent usages
281-
const promise = Storage.getItem(key)
282-
.then((val) => {
283-
if (skippableCollectionMemberIDs.size) {
284-
try {
285-
const [, collectionMemberID] = splitCollectionMemberKey(key);
286-
if (skippableCollectionMemberIDs.has(collectionMemberID)) {
287-
// The key is a skippable one, so we set the value to undefined.
288-
// eslint-disable-next-line no-param-reassign
289-
val = undefined as OnyxValue<TKey>;
290-
}
291-
} catch (e) {
292-
// The key is not a collection one or something went wrong during split, so we proceed with the function's logic.
293-
}
294-
}
295-
296-
if (val === undefined) {
297-
cache.addNullishStorageKey(key);
298-
return undefined;
299-
}
300-
301-
cache.set(key, val);
302-
return val;
303-
})
304-
.catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
305-
306-
return cache.captureTask(taskName, promise) as Promise<TValue>;
265+
return Promise.resolve(undefined as TValue);
307266
}
308267

309268
// multiGet the data first from the cache and then from the storage for the missing keys.
310269
function multiGet<TKey extends OnyxKey>(keys: CollectionKeyBase[]): Promise<Map<OnyxKey, OnyxValue<TKey>>> {
311-
// Keys that are not in the cache
312-
const missingKeys: OnyxKey[] = [];
313-
314-
// Tasks that are pending
315-
const pendingTasks: Array<Promise<OnyxValue<TKey>>> = [];
316-
317-
// Keys for the tasks that are pending
318-
const pendingKeys: OnyxKey[] = [];
319-
320270
// Data to be sent back to the invoker
321271
const dataMap = new Map<OnyxKey, OnyxValue<TKey>>();
322272

323-
/**
324-
* We are going to iterate over all the matching keys and check if we have the data in the cache.
325-
* 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
326-
* for the key. If there is such task, then we add the promise to the pendingTasks array and the key to the pendingKeys
327-
* array. If there is no pending task then we add the key to the missingKeys array.
328-
*
329-
* These missingKeys will be later used to multiGet the data from the storage.
330-
*/
331273
for (const key of keys) {
332-
// RAM-only keys should never read from storage as they may have stale persisted data
333-
// from before the key was migrated to RAM-only.
334-
if (isRamOnlyKey(key)) {
335-
if (cache.hasCacheForKey(key)) {
336-
dataMap.set(key, cache.get(key) as OnyxValue<TKey>);
337-
}
338-
continue;
339-
}
340-
341274
const cacheValue = cache.get(key) as OnyxValue<TKey>;
342275
if (cacheValue) {
343276
dataMap.set(key, cacheValue);
344-
continue;
345-
}
346-
347-
const pendingKey = `${TASK.GET}:${key}` as const;
348-
if (cache.hasPendingTask(pendingKey)) {
349-
pendingTasks.push(cache.getTaskPromise(pendingKey) as Promise<OnyxValue<TKey>>);
350-
pendingKeys.push(key);
351-
} else {
352-
missingKeys.push(key);
353277
}
354278
}
355279

356-
return (
357-
Promise.all(pendingTasks)
358-
// Wait for all the pending tasks to resolve and then add the data to the data map.
359-
.then((values) => {
360-
for (const [index, value] of values.entries()) {
361-
dataMap.set(pendingKeys[index], value);
362-
}
363-
364-
return Promise.resolve();
365-
})
366-
// Get the missing keys using multiGet from the storage.
367-
.then(() => {
368-
if (missingKeys.length === 0) {
369-
return Promise.resolve(undefined);
370-
}
371-
372-
return Storage.multiGet(missingKeys);
373-
})
374-
// Add the data from the missing keys to the data map and also merge it to the cache.
375-
.then((values) => {
376-
if (!values || values.length === 0) {
377-
return dataMap;
378-
}
379-
380-
// temp object is used to merge the missing data into the cache
381-
const temp: OnyxCollection<KeyValueMapping[TKey]> = {};
382-
for (const [key, value] of values) {
383-
if (skippableCollectionMemberIDs.size) {
384-
try {
385-
const [, collectionMemberID] = splitCollectionMemberKey(key);
386-
if (skippableCollectionMemberIDs.has(collectionMemberID)) {
387-
// The key is a skippable one, so we skip this iteration.
388-
continue;
389-
}
390-
} catch (e) {
391-
// The key is not a collection one or something went wrong during split, so we proceed with the function's logic.
392-
}
393-
}
394-
395-
dataMap.set(key, value as OnyxValue<TKey>);
396-
temp[key] = value as OnyxValue<TKey>;
397-
}
398-
cache.merge(temp);
399-
return dataMap;
400-
})
401-
);
280+
return Promise.resolve(dataMap);
402281
}
403282

404283
/**
@@ -442,29 +321,7 @@ function deleteKeyBySubscriptions(subscriptionID: number) {
442321

443322
/** Returns current key names stored in persisted storage */
444323
function getAllKeys(): Promise<Set<OnyxKey>> {
445-
// When we've already read stored keys, resolve right away
446-
const cachedKeys = cache.getAllKeys();
447-
if (cachedKeys.size > 0) {
448-
return Promise.resolve(cachedKeys);
449-
}
450-
451-
// When a value retrieving task for all keys is still running hook to it
452-
if (cache.hasPendingTask(TASK.GET_ALL_KEYS)) {
453-
return cache.getTaskPromise(TASK.GET_ALL_KEYS) as Promise<Set<OnyxKey>>;
454-
}
455-
456-
// Otherwise retrieve the keys from storage and capture a promise to aid concurrent usages
457-
const promise = Storage.getAllKeys().then((keys) => {
458-
// Filter out RAM-only keys from storage results as they may be stale entries
459-
// from before the key was migrated to RAM-only.
460-
const filteredKeys = keys.filter((key) => !isRamOnlyKey(key));
461-
cache.setAllKeys(filteredKeys);
462-
463-
// return the updated set of keys
464-
return cache.getAllKeys();
465-
});
466-
467-
return cache.captureTask(TASK.GET_ALL_KEYS, promise) as Promise<Set<OnyxKey>>;
324+
return Promise.resolve(cache.getAllKeys());
468325
}
469326

470327
/**
@@ -1075,18 +932,44 @@ function mergeInternal<TValue extends OnyxInput<OnyxKey> | undefined, TChange ex
1075932
* Merge user provided default key value pairs.
1076933
*/
1077934
function initializeWithDefaultKeyStates(): Promise<void> {
1078-
// Filter out RAM-only keys from storage reads as they may have stale persisted data
1079-
// from before the key was migrated to RAM-only.
1080-
const keysToFetch = Object.keys(defaultKeyStates).filter((key) => !isRamOnlyKey(key));
1081-
return Storage.multiGet(keysToFetch).then((pairs) => {
1082-
const existingDataAsObject = Object.fromEntries(pairs) as Record<string, unknown>;
935+
// Eagerly load the entire database into cache in a single batch read.
936+
// This is faster than lazy-loading individual keys because:
937+
// 1. One DB transaction instead of hundreds
938+
// 2. All subsequent reads are synchronous cache hits
939+
return Storage.getAll().then((pairs) => {
940+
const allDataFromStorage: Record<string, unknown> = {};
941+
942+
for (const [key, value] of pairs) {
943+
// RAM-only keys should never be loaded from storage as they may have stale persisted data
944+
// from before the key was migrated to RAM-only.
945+
if (isRamOnlyKey(key)) {
946+
continue;
947+
}
948+
allDataFromStorage[key] = value;
949+
}
950+
951+
// Load all storage data into cache silently (no subscriber notifications)
952+
cache.setAllKeys(Object.keys(allDataFromStorage));
953+
cache.merge(allDataFromStorage);
954+
955+
// Extract only the default key states from storage and merge with defaults
956+
const defaultKeysFromStorage = Object.keys(defaultKeyStates).reduce((obj: Record<string, unknown>, key) => {
957+
if (key in allDataFromStorage) {
958+
// eslint-disable-next-line no-param-reassign
959+
obj[key] = allDataFromStorage[key];
960+
}
961+
return obj;
962+
}, {});
1083963

1084-
const merged = utils.fastMerge(existingDataAsObject, defaultKeyStates, {
964+
const merged = utils.fastMerge(defaultKeysFromStorage, defaultKeyStates, {
1085965
shouldRemoveNestedNulls: true,
1086966
}).result;
1087967
cache.merge(merged ?? {});
1088968

1089-
for (const [key, value] of Object.entries(merged ?? {})) keyChanged(key, value);
969+
// Only notify subscribers for default key states — same as before.
970+
// Other keys will be picked up by subscribers when they connect.
971+
// TODO: Maybe we dont need this.
972+
// for (const [key, value] of Object.entries(merged ?? {})) keyChanged(key, value);
1090973
});
1091974
}
1092975

lib/storage/__mocks__/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const StorageMock = {
1616
removeItems: jest.fn(MemoryOnlyProvider.removeItems),
1717
clear: jest.fn(MemoryOnlyProvider.clear),
1818
getAllKeys: jest.fn(MemoryOnlyProvider.getAllKeys),
19+
getAll: jest.fn(MemoryOnlyProvider.getAll),
1920
getDatabaseSize: jest.fn(MemoryOnlyProvider.getDatabaseSize),
2021
keepInstancesSync: jest.fn(),
2122

lib/storage/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ const storage: Storage = {
187187
*/
188188
getAllKeys: () => tryOrDegradePerformance(() => provider.getAllKeys()),
189189

190+
/**
191+
* Returns all key-value pairs from storage in a single batch operation
192+
*/
193+
getAll: () => tryOrDegradePerformance(() => provider.getAll()),
194+
190195
/**
191196
* Gets the total bytes of the store
192197
*/
@@ -220,6 +225,7 @@ GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => {
220225
storage.removeItems = decorateWithMetrics(storage.removeItems, 'Storage.removeItems');
221226
storage.clear = decorateWithMetrics(storage.clear, 'Storage.clear');
222227
storage.getAllKeys = decorateWithMetrics(storage.getAllKeys, 'Storage.getAllKeys');
228+
storage.getAll = decorateWithMetrics(storage.getAll, 'Storage.getAll');
223229
});
224230

225231
export default storage;

lib/storage/providers/IDBKeyValProvider/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import utils from '../../../utils';
44
import type StorageProvider from '../types';
55
import type {OnyxKey, OnyxValue} from '../../../types';
66
import createStore from './createStore';
7+
import type {StorageKeyValuePair} from '../types';
78

89
const DB_NAME = 'OnyxDB';
910
const STORE_NAME = 'keyvaluepairs';
@@ -109,6 +110,13 @@ const provider: StorageProvider<UseStore | undefined> = {
109110

110111
return IDB.keys(provider.store);
111112
},
113+
getAll() {
114+
if (!provider.store) {
115+
throw new Error('Store not initialized!');
116+
}
117+
118+
return IDB.entries(provider.store) as Promise<StorageKeyValuePair[]>;
119+
},
112120
getItem(key) {
113121
if (!provider.store) {
114122
throw new Error('Store not initialized!');

lib/storage/providers/MemoryOnlyProvider.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ const provider: StorageProvider<Store> = {
135135
return Promise.resolve(_.keys(provider.store));
136136
},
137137

138+
/**
139+
* Returns all key-value pairs from memory
140+
*/
141+
getAll() {
142+
return Promise.resolve(Object.entries(provider.store) as StorageKeyValuePair[]);
143+
},
144+
138145
/**
139146
* Gets the total bytes of the store.
140147
* `bytesRemaining` will always be `Number.POSITIVE_INFINITY` since we don't have a hard limit on memory.

lib/storage/providers/NoopProvider.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ const provider: StorageProvider<unknown> = {
8787
return Promise.resolve([]);
8888
},
8989

90+
/**
91+
* Returns all key-value pairs from storage
92+
*/
93+
getAll() {
94+
return Promise.resolve([]);
95+
},
96+
9097
/**
9198
* Gets the total bytes of the store.
9299
* `bytesRemaining` will always be `Number.POSITIVE_INFINITY` since we don't have a hard limit on memory.

lib/storage/providers/SQLiteProvider.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,17 @@ const provider: StorageProvider<NitroSQLiteConnection | undefined> = {
198198
return (result ?? []) as StorageKeyList;
199199
});
200200
},
201+
getAll() {
202+
if (!provider.store) {
203+
throw new Error('Store is not initialized!');
204+
}
205+
206+
return provider.store.executeAsync<OnyxSQLiteKeyValuePair>('SELECT record_key, valueJSON FROM keyvaluepairs;').then(({rows}) => {
207+
// eslint-disable-next-line no-underscore-dangle
208+
const result = rows?._array.map((row) => [row.record_key, JSON.parse(row.valueJSON)]);
209+
return (result ?? []) as StorageKeyValuePair[];
210+
});
211+
},
201212
removeItem(key) {
202213
if (!provider.store) {
203214
throw new Error('Store is not initialized!');

lib/storage/providers/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ type StorageProvider<TStore> = {
6060
*/
6161
getAllKeys: () => Promise<StorageKeyList>;
6262

63+
/**
64+
* Returns all key-value pairs from storage in a single batch operation.
65+
* More efficient than getAllKeys + multiGet for loading the entire database.
66+
*/
67+
getAll: () => Promise<StorageKeyValuePair[]>;
68+
6369
/**
6470
* Removes given key and its value from storage
6571
*/

0 commit comments

Comments
 (0)