Skip to content

Commit 22f0083

Browse files
committed
implement storing collections in cache as by index
1 parent 5796442 commit 22f0083

2 files changed

Lines changed: 188 additions & 19 deletions

File tree

lib/OnyxCache.ts

Lines changed: 142 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class OnyxCache {
3131
/** A map of cached values */
3232
private storageMap: Record<OnyxKey, OnyxValue<OnyxKey>>;
3333

34+
/** A map of cached collection values for faster access */
35+
private collectionMap: Record<OnyxKey, Record<OnyxKey, OnyxValue<OnyxKey>>>;
36+
3437
/**
3538
* Captured pending tasks for already running storage methods
3639
* Using a map yields better performance on operations such a delete
@@ -49,11 +52,15 @@ class OnyxCache {
4952
/** List of keys that have been directly subscribed to or recently modified from least to most recent */
5053
private recentlyAccessedKeys: OnyxKey[] = [];
5154

55+
/** Set of collection keys for fast lookup */
56+
private collectionKeys = new Set<OnyxKey>();
57+
5258
constructor() {
5359
this.storageKeys = new Set();
5460
this.nullishStorageKeys = new Set();
5561
this.recentKeys = new Set();
5662
this.storageMap = {};
63+
this.collectionMap = {};
5764
this.pendingPromises = new Map();
5865

5966
// bind all public methods to prevent problems with `this`
@@ -83,6 +90,10 @@ class OnyxCache {
8390
'addLastAccessedKey',
8491
'addEvictableKeysToRecentlyAccessedList',
8592
'getKeyForEviction',
93+
'setCollectionKeys',
94+
'isCollectionKey',
95+
'getCollectionKey',
96+
'getCollectionData',
8697
);
8798
}
8899

@@ -130,6 +141,17 @@ class OnyxCache {
130141

131142
/** Check whether cache has data for the given key */
132143
hasCacheForKey(key: OnyxKey): boolean {
144+
// Check if this is a collection key
145+
if (this.isCollectionKey(key)) {
146+
return this.collectionMap[key] !== undefined;
147+
}
148+
149+
// Check if this is a collection member key
150+
const collectionKey = this.getCollectionKey(key);
151+
if (collectionKey) {
152+
return (this.collectionMap[collectionKey] && this.collectionMap[collectionKey][key] !== undefined) || this.hasNullishStorageKey(key);
153+
}
154+
133155
return this.storageMap[key] !== undefined || this.hasNullishStorageKey(key);
134156
}
135157

@@ -141,6 +163,18 @@ class OnyxCache {
141163
if (shouldReindexCache) {
142164
this.addToAccessedKeys(key);
143165
}
166+
167+
// Check if this is a collection key request
168+
if (this.isCollectionKey(key)) {
169+
return this.collectionMap[key];
170+
}
171+
172+
// Check if this is a collection member key
173+
const collectionKey = this.getCollectionKey(key);
174+
if (collectionKey && this.collectionMap[collectionKey]) {
175+
return this.collectionMap[collectionKey][key];
176+
}
177+
144178
return this.storageMap[key];
145179
}
146180

@@ -157,18 +191,47 @@ class OnyxCache {
157191
this.nullishStorageKeys.delete(key);
158192

159193
if (value === null || value === undefined) {
160-
delete this.storageMap[key];
194+
// Handle deletion for collection keys
195+
const collectionKey = this.getCollectionKey(key);
196+
if (collectionKey && this.collectionMap[collectionKey]) {
197+
delete this.collectionMap[collectionKey][key];
198+
} else {
199+
delete this.storageMap[key];
200+
}
161201
return undefined;
162202
}
163203

164-
this.storageMap[key] = value;
204+
// Check if this is a collection member key
205+
const collectionKey = this.getCollectionKey(key);
206+
if (collectionKey) {
207+
// Initialize collection if it doesn't exist
208+
if (!this.collectionMap[collectionKey]) {
209+
this.collectionMap[collectionKey] = {};
210+
}
211+
this.collectionMap[collectionKey][key] = value;
212+
} else {
213+
// Regular key or collection key itself
214+
this.storageMap[key] = value;
215+
}
165216

166217
return value;
167218
}
168219

169220
/** Forget the cached value for the given key */
170221
drop(key: OnyxKey): void {
171-
delete this.storageMap[key];
222+
// Check if this is a collection key - drop entire collection
223+
if (this.isCollectionKey(key)) {
224+
delete this.collectionMap[key];
225+
} else {
226+
// Check if this is a collection member key
227+
const collectionKey = this.getCollectionKey(key);
228+
if (collectionKey && this.collectionMap[collectionKey]) {
229+
delete this.collectionMap[collectionKey][key];
230+
} else {
231+
delete this.storageMap[key];
232+
}
233+
}
234+
172235
this.storageKeys.delete(key);
173236
this.recentKeys.delete(key);
174237
}
@@ -182,7 +245,32 @@ class OnyxCache {
182245
throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs');
183246
}
184247

185-
this.storageMap = {...utils.fastMerge(this.storageMap, data)};
248+
// Separate collection keys from regular keys
249+
const collectionData: Record<OnyxKey, Record<OnyxKey, OnyxValue<OnyxKey>>> = {};
250+
const regularData: Record<OnyxKey, OnyxValue<OnyxKey>> = {};
251+
252+
Object.entries(data).forEach(([key, value]) => {
253+
const collectionKey = this.getCollectionKey(key);
254+
if (collectionKey) {
255+
if (!collectionData[collectionKey]) {
256+
collectionData[collectionKey] = {};
257+
}
258+
collectionData[collectionKey][key] = value;
259+
} else {
260+
regularData[key] = value;
261+
}
262+
});
263+
264+
// Merge regular data
265+
this.storageMap = {...utils.fastMerge(this.storageMap, regularData)};
266+
267+
// Merge collection data
268+
Object.entries(collectionData).forEach(([collectionKey, memberData]) => {
269+
if (!this.collectionMap[collectionKey]) {
270+
this.collectionMap[collectionKey] = {};
271+
}
272+
this.collectionMap[collectionKey] = {...utils.fastMerge(this.collectionMap[collectionKey], memberData)};
273+
});
186274

187275
Object.entries(data).forEach(([key, value]) => {
188276
this.addKey(key);
@@ -260,7 +348,13 @@ class OnyxCache {
260348
}
261349

262350
for (const key of keysToRemove) {
263-
delete this.storageMap[key];
351+
// Check if this is a collection member key
352+
const collectionKey = this.getCollectionKey(key);
353+
if (collectionKey && this.collectionMap[collectionKey]) {
354+
delete this.collectionMap[collectionKey][key];
355+
} else {
356+
delete this.storageMap[key];
357+
}
264358
this.recentKeys.delete(key);
265359
}
266360
}
@@ -272,7 +366,8 @@ class OnyxCache {
272366

273367
/** Check if the value has changed */
274368
hasValueChanged(key: OnyxKey, value: OnyxValue<OnyxKey>): boolean {
275-
return !deepEqual(this.storageMap[key], value);
369+
const currentValue = this.get(key, false);
370+
return !deepEqual(currentValue, value);
276371
}
277372

278373
/**
@@ -358,6 +453,47 @@ class OnyxCache {
358453
getKeyForEviction(): OnyxKey | undefined {
359454
return this.recentlyAccessedKeys.find((key) => !this.evictionBlocklist[key]);
360455
}
456+
457+
/**
458+
* Set the collection keys for optimized storage
459+
*/
460+
setCollectionKeys(collectionKeys: Set<OnyxKey>): void {
461+
this.collectionKeys = collectionKeys;
462+
// Initialize collection maps for existing collection keys
463+
collectionKeys.forEach((collectionKey) => {
464+
if (this.collectionMap[collectionKey]) {
465+
return;
466+
}
467+
468+
this.collectionMap[collectionKey] = {};
469+
});
470+
}
471+
472+
/**
473+
* Check if a key is a collection key
474+
*/
475+
isCollectionKey(key: OnyxKey): boolean {
476+
return this.collectionKeys.has(key);
477+
}
478+
479+
/**
480+
* Get the collection key for a given member key
481+
*/
482+
getCollectionKey(key: OnyxKey): OnyxKey | null {
483+
for (const collectionKey of this.collectionKeys) {
484+
if (key.startsWith(collectionKey) && key.length > collectionKey.length) {
485+
return collectionKey;
486+
}
487+
}
488+
return null;
489+
}
490+
491+
/**
492+
* Get all data for a collection key
493+
*/
494+
getCollectionData(collectionKey: OnyxKey): Record<OnyxKey, OnyxValue<OnyxKey>> | undefined {
495+
return this.collectionMap[collectionKey];
496+
}
361497
}
362498

363499
const instance = new OnyxCache();

lib/OnyxUtils.ts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ function initStoreValues(keys: DeepRecord<string, OnyxKey>, initialKeyStates: Pa
150150
// Let Onyx know about which keys are safe to evict
151151
cache.setEvictionAllowList(evictableKeys);
152152

153+
// Set collection keys in cache for optimized storage
154+
cache.setCollectionKeys(onyxCollectionKeySet);
155+
153156
if (typeof keys.COLLECTION === 'object' && typeof keys.COLLECTION.SNAPSHOT === 'string') {
154157
snapshotKey = keys.COLLECTION.SNAPSHOT;
155158
}
@@ -522,23 +525,30 @@ function tryGetCachedValue<TKey extends OnyxKey>(key: TKey, mapping?: Partial<Ma
522525
let val = cache.get(key);
523526

524527
if (isCollectionKey(key)) {
525-
const allCacheKeys = cache.getAllKeys();
526-
527-
// It is possible we haven't loaded all keys yet so we do not know if the
528-
// collection actually exists.
529-
if (allCacheKeys.size === 0) {
530-
return;
531-
}
528+
// Try optimized collection data retrieval first
529+
const collectionData = cache.getCollectionData(key);
530+
if (collectionData && Object.keys(collectionData).length > 0) {
531+
val = collectionData;
532+
} else {
533+
// Fallback to original logic
534+
const allCacheKeys = cache.getAllKeys();
532535

533-
const values: OnyxCollection<KeyValueMapping[TKey]> = {};
534-
allCacheKeys.forEach((cacheKey) => {
535-
if (!cacheKey.startsWith(key)) {
536+
// It is possible we haven't loaded all keys yet so we do not know if the
537+
// collection actually exists.
538+
if (allCacheKeys.size === 0) {
536539
return;
537540
}
538541

539-
values[cacheKey] = cache.get(cacheKey);
540-
});
541-
val = values;
542+
const values: OnyxCollection<KeyValueMapping[TKey]> = {};
543+
allCacheKeys.forEach((cacheKey) => {
544+
if (!cacheKey.startsWith(key)) {
545+
return;
546+
}
547+
548+
values[cacheKey] = cache.get(cacheKey);
549+
});
550+
val = values;
551+
}
542552
}
543553

544554
if (mapping?.selector) {
@@ -553,6 +563,26 @@ function tryGetCachedValue<TKey extends OnyxKey>(key: TKey, mapping?: Partial<Ma
553563
}
554564

555565
function getCachedCollection<TKey extends CollectionKeyBase>(collectionKey: TKey, collectionMemberKeys?: string[]): NonNullable<OnyxCollection<KeyValueMapping[TKey]>> {
566+
// Use optimized collection data retrieval
567+
const collectionData = cache.getCollectionData(collectionKey);
568+
if (collectionData) {
569+
// If we have specific member keys, filter the collection
570+
if (collectionMemberKeys) {
571+
const filteredCollection: OnyxCollection<KeyValueMapping[TKey]> = {};
572+
collectionMemberKeys.forEach((key) => {
573+
if (collectionData[key] !== undefined) {
574+
filteredCollection[key] = collectionData[key];
575+
} else if (cache.hasNullishStorageKey(key)) {
576+
filteredCollection[key] = cache.get(key);
577+
}
578+
});
579+
return filteredCollection;
580+
}
581+
// Return a copy to avoid mutations affecting the cache
582+
return {...collectionData};
583+
}
584+
585+
// Fallback to original implementation if collection data not available
556586
const allKeys = collectionMemberKeys || cache.getAllKeys();
557587
const collection: OnyxCollection<KeyValueMapping[TKey]> = {};
558588

@@ -1293,6 +1323,9 @@ function subscribeToKey<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKe
12931323
// can send data back to the subscriber. Note that multiple keys can match as a subscriber could either be
12941324
// subscribed to a "collection key" or a single key.
12951325
const matchingKeys: string[] = [];
1326+
1327+
// For now, use the original key matching logic to ensure compatibility
1328+
// TODO: Optimize this after ensuring the cache collection data is always properly populated
12961329
keys.forEach((key) => {
12971330
if (!isKeyMatch(mapping.key, key)) {
12981331
return;

0 commit comments

Comments
 (0)