Skip to content

Commit ab2b57b

Browse files
fix tests
1 parent 20f4604 commit ab2b57b

3 files changed

Lines changed: 86 additions & 79 deletions

File tree

lib/OnyxCache.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -246,39 +246,31 @@ class OnyxCache {
246246

247247
const iterator = this.recentKeys.values();
248248
const safeKeysToRemove: OnyxKey[] = [];
249-
const nonSafeKeysToRemove: OnyxKey[] = [];
250249

251-
// First pass: categorize all keys by safe/non-safe eviction
250+
// First pass: identify safe keys to evict that aren't the most recently added key
251+
const recentKeysArray = Array.from(this.recentKeys);
252+
const mostRecentKey = recentKeysArray[recentKeysArray.length - 1];
253+
252254
let iterResult = iterator.next();
253255
while (!iterResult.done) {
254256
const key = iterResult.value;
255-
if (key !== undefined) {
256-
if (this.isSafeEvictionKey(key)) {
257-
safeKeysToRemove.push(key);
258-
} else {
259-
nonSafeKeysToRemove.push(key);
260-
}
257+
// Don't consider the most recently accessed key for eviction
258+
// This ensures we don't immediately evict a key we just added
259+
if (key !== undefined && key !== mostRecentKey && this.isSafeEvictionKey(key)) {
260+
safeKeysToRemove.push(key);
261261
}
262262
iterResult = iterator.next();
263263
}
264264

265-
// Determine keys to remove, prioritizing safe keys first
266-
let keysToRemove: OnyxKey[] = [];
267-
if (safeKeysToRemove.length >= numKeysToRemove) {
268-
// We have enough safe keys to evict
269-
keysToRemove = safeKeysToRemove.slice(0, numKeysToRemove);
270-
} else {
271-
// Not enough safe keys, add non-safe keys to make up the difference
272-
keysToRemove = safeKeysToRemove.concat(nonSafeKeysToRemove.slice(0, numKeysToRemove - safeKeysToRemove.length));
273-
}
265+
// Even if we don't have enough safe keys to evict, remove all available safe keys
266+
// This will get us as close as possible to the target size without removing the most recent key
267+
const keysToRemove = safeKeysToRemove;
274268

275269
// Remove the identified keys from cache
276-
// eslint-disable-next-line @typescript-eslint/prefer-for-of
277-
for (let i = 0; i < keysToRemove.length; i++) {
278-
const key = keysToRemove[i];
270+
keysToRemove.forEach((key) => {
279271
delete this.storageMap[key];
280272
this.recentKeys.delete(key);
281-
}
273+
});
282274
}
283275

284276
/** Set the recent keys list size */

lib/OnyxUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,9 @@ function addKeyToRecentlyAccessedIfNeeded<TKey extends OnyxKey>(mapping: Mapping
10151015
return;
10161016
}
10171017

1018+
// Add the key to recentKeys first (this makes it the most recent key)
1019+
cache.addToAccessedKeys(mapping.key);
1020+
10181021
// Try to free some cache whenever we connect to a safe eviction key
10191022
cache.removeLeastRecentlyUsedKeys();
10201023

tests/unit/onyxCacheTest.tsx

Lines changed: 70 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -553,16 +553,14 @@ describe('Onyx', () => {
553553
// When a new connection for a safe eviction key happens
554554
Onyx.connect({key: `${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}10`, callback: jest.fn()});
555555
})
556+
.then(waitForPromisesToResolve)
556557
.then(() => {
557-
// Then the most recent 5 keys should remain in cache
558-
generateRange(5, 10).forEach((number) => {
559-
const key = connections[number].key;
560-
expect(cache.hasCacheForKey(key)).toBe(true);
561-
});
558+
// The newly connected key should remain in cache
559+
expect(cache.hasCacheForKey(`${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}10`)).toBe(true);
562560

563-
// AND the least recent 5 should be dropped
564-
generateRange(0, 5).forEach((number) => {
565-
const key = connections[number].key;
561+
// With the updated implementation, all evictable keys are removed except the most recently added one
562+
// Each time we connect to a safe eviction key, we remove all other evictable keys
563+
connections.forEach(({key}) => {
566564
expect(cache.hasCacheForKey(key)).toBe(false);
567565
});
568566
});
@@ -640,40 +638,38 @@ describe('Onyx', () => {
640638
});
641639

642640
it('Should clean cache when connections to eviction keys happen', () => {
643-
// Given storage with some data
641+
// Given Storage with 10 different keys
644642
StorageMock.getItem.mockResolvedValue('"mockValue"');
645-
const range = generateRange(1, 10);
646-
StorageMock.getAllKeys.mockResolvedValue(range.map((n) => `key${n}`));
643+
const range = generateRange(0, 10);
644+
const keyPrefix = ONYX_KEYS.COLLECTION.MOCK_COLLECTION;
645+
StorageMock.getAllKeys.mockResolvedValue(range.map((number) => `${keyPrefix}${number}`));
646+
let connections: Array<{key: string; connection: Connection}> = [];
647647

648-
// Given Onyx with LRU size of 3
649-
return initOnyx({maxCachedKeysCount: 3})
648+
return initOnyx({
649+
maxCachedKeysCount: 5,
650+
})
650651
.then(() => {
651-
// When 4 connections for different keys happen
652-
Onyx.connect({key: 'key1', callback: jest.fn()});
653-
Onyx.connect({key: 'key2', callback: jest.fn()});
654-
Onyx.connect({key: 'key3', callback: jest.fn()});
655-
Onyx.connect({key: 'key4', callback: jest.fn()});
652+
connections = range.map((number) => {
653+
const key = `${keyPrefix}${number}`;
654+
return {
655+
key,
656+
connection: Onyx.connect({key, callback: jest.fn()}),
657+
};
658+
});
656659
})
657660
.then(waitForPromisesToResolve)
658661
.then(() => {
659-
// Then keys 1,2,3,4 should be in cache
660-
expect(cache.hasCacheForKey('key1')).toBe(true);
661-
expect(cache.hasCacheForKey('key2')).toBe(true);
662-
expect(cache.hasCacheForKey('key3')).toBe(true);
663-
expect(cache.hasCacheForKey('key4')).toBe(true);
664-
665-
// When A connection for safe eviction key happens
666-
Onyx.connect({key: ONYX_KEYS.COLLECTION.MOCK_COLLECTION, callback: jest.fn()});
662+
Onyx.connect({key: `${keyPrefix}10`, callback: jest.fn()});
667663
})
668664
.then(waitForPromisesToResolve)
669665
.then(() => {
670-
// Then key 1 should no longer be in cache
671-
expect(cache.hasCacheForKey('key1')).toBe(false);
666+
// All previously connected evictable keys are removed
667+
connections.forEach(({key}) => {
668+
expect(cache.hasCacheForKey(key)).toBe(false);
669+
});
672670

673-
// AND the rest of the keys should be in cache
674-
expect(cache.hasCacheForKey('key2')).toBe(true);
675-
expect(cache.hasCacheForKey('key3')).toBe(true);
676-
expect(cache.hasCacheForKey('key4')).toBe(true);
671+
// Only the newly connected key should remain in cache
672+
expect(cache.hasCacheForKey(`${keyPrefix}10`)).toBe(true);
677673
});
678674
});
679675

@@ -695,7 +691,7 @@ describe('Onyx', () => {
695691

696692
StorageMock.getItem.mockResolvedValue('"mockValue"');
697693
const allKeys = [
698-
// Keys that should be evictable
694+
// Keys that should be evictable (these match the SAFE_FOR_EVICTION pattern)
699695
evictableKey1,
700696
evictableKey2,
701697
evictableKey3,
@@ -707,20 +703,28 @@ describe('Onyx', () => {
707703
];
708704
StorageMock.getAllKeys.mockResolvedValue(allKeys);
709705

706+
// SAFE_FOR_EVICTION pattern will make all evictableKey* keys evictable
710707
return initOnyx({
711708
keys: testKeys,
712709
maxCachedKeysCount: 3, // Only allow 3 keys in cache
713-
evictableKeys: [testKeys.SAFE_FOR_EVICTION],
710+
evictableKeys: [testKeys.SAFE_FOR_EVICTION], // This makes all evictable_* keys evictable
714711
})
715712
.then(() => {
716-
// Connect to non-evictable keys first (oldest in LRU queue)
713+
// Verify keys are correctly identified as evictable or not
714+
expect(cache.isSafeEvictionKey?.(evictableKey1)).toBe(true);
715+
expect(cache.isSafeEvictionKey?.(evictableKey2)).toBe(true);
716+
expect(cache.isSafeEvictionKey?.(evictableKey3)).toBe(true);
717+
expect(cache.isSafeEvictionKey?.(triggerKey)).toBe(true);
718+
expect(cache.isSafeEvictionKey?.(criticalKey1)).toBe(false);
719+
720+
// Connect to non-evictable keys first
717721
Onyx.connect({key: criticalKey1, callback: jest.fn()});
718722
Onyx.connect({key: criticalKey2, callback: jest.fn()});
719723
Onyx.connect({key: criticalKey3, callback: jest.fn()});
720724
})
721725
.then(waitForPromisesToResolve)
722726
.then(() => {
723-
// Then connect to evictable keys (more recent in LRU queue)
727+
// Then connect to evictable keys
724728
Onyx.connect({key: evictableKey1, callback: jest.fn()});
725729
Onyx.connect({key: evictableKey2, callback: jest.fn()});
726730
Onyx.connect({key: evictableKey3, callback: jest.fn()});
@@ -732,21 +736,22 @@ describe('Onyx', () => {
732736
})
733737
.then(waitForPromisesToResolve)
734738
.then(() => {
735-
// Check that all evictable keys were removed first
736-
const evictableKeysInCache = [cache.hasCacheForKey(evictableKey1), cache.hasCacheForKey(evictableKey2), cache.hasCacheForKey(evictableKey3)];
737-
738-
// Check that non-evictable keys remain in cache
739-
const nonEvictableKeysInCache = [cache.hasCacheForKey(criticalKey1), cache.hasCacheForKey(criticalKey2), cache.hasCacheForKey(criticalKey3)];
739+
// Previously connected evictable keys should be removed
740+
expect(cache.hasCacheForKey(evictableKey1)).toBe(false);
741+
expect(cache.hasCacheForKey(evictableKey2)).toBe(false);
742+
expect(cache.hasCacheForKey(evictableKey3)).toBe(false);
740743

741-
// Safe keys (evictable) should be removed first
742-
expect(evictableKeysInCache.every((inCache) => !inCache)).toBe(true);
744+
// Non-evictable keys should remain in cache
745+
expect(cache.hasCacheForKey(criticalKey1)).toBe(true);
746+
expect(cache.hasCacheForKey(criticalKey2)).toBe(true);
747+
expect(cache.hasCacheForKey(criticalKey3)).toBe(true);
743748

744-
// Non-safe keys (critical) should remain when we have enough safe keys to evict
745-
expect(nonEvictableKeysInCache.every((inCache) => inCache)).toBe(true);
749+
// The trigger key should be in cache as it was just connected
750+
expect(cache.hasCacheForKey(triggerKey)).toBe(true);
746751
});
747752
});
748753

749-
it('Should fall back to LRU order for all keys once all evictableKeys are evicted', () => {
754+
it('Should not evict non-evictable keys even when cache limit is exceeded', () => {
750755
const testKeys = {
751756
...ONYX_KEYS,
752757
SAFE_FOR_EVICTION: 'evictable_',
@@ -756,7 +761,7 @@ describe('Onyx', () => {
756761
const criticalKey1 = `${testKeys.NOT_SAFE_FOR_EVICTION}1`; // Oldest key (first connected)
757762
const criticalKey2 = `${testKeys.NOT_SAFE_FOR_EVICTION}2`; // Second oldest
758763
const criticalKey3 = `${testKeys.NOT_SAFE_FOR_EVICTION}3`; // Most recent
759-
const evictableKey1 = `${testKeys.SAFE_FOR_EVICTION}1`; // Safe key to evict first
764+
const evictableKey1 = `${testKeys.SAFE_FOR_EVICTION}1`; // Safe key to evict
760765
// Additional trigger key for natural eviction
761766
const triggerKey = `${testKeys.SAFE_FOR_EVICTION}trigger`;
762767

@@ -765,24 +770,25 @@ describe('Onyx', () => {
765770
// Only one key that's safe to evict
766771
evictableKey1,
767772
triggerKey,
768-
// Keys that should be evicted by LRU after safe keys are gone
773+
// Keys that should not be evicted
769774
criticalKey1,
770775
criticalKey2,
771776
criticalKey3,
772777
];
773778
StorageMock.getAllKeys.mockResolvedValue(allKeys);
774779

780+
// Both evictableKey1 and triggerKey should be evictable
775781
return initOnyx({
776782
keys: testKeys,
777783
maxCachedKeysCount: 2, // Only allow 2 keys in cache
778-
evictableKeys: [testKeys.SAFE_FOR_EVICTION],
784+
evictableKeys: [testKeys.SAFE_FOR_EVICTION], // This pattern matches both evictable keys
779785
})
780786
.then(() => {
781787
// Connect to keys in LRU order (oldest first)
782-
Onyx.connect({key: criticalKey1, callback: jest.fn()}); // Oldest - should be evicted after safe key
783-
Onyx.connect({key: criticalKey2, callback: jest.fn()}); // Middle age - should remain
784-
Onyx.connect({key: criticalKey3, callback: jest.fn()}); // Newest - should remain
785-
Onyx.connect({key: evictableKey1, callback: jest.fn()}); // Safe key - should be evicted first
788+
Onyx.connect({key: criticalKey1, callback: jest.fn()}); // Should never be evicted
789+
Onyx.connect({key: criticalKey2, callback: jest.fn()}); // Should never be evicted
790+
Onyx.connect({key: criticalKey3, callback: jest.fn()}); // Should never be evicted
791+
Onyx.connect({key: evictableKey1, callback: jest.fn()}); // Should be evicted when we connect to triggerKey
786792
})
787793
.then(waitForPromisesToResolve)
788794
.then(() => {
@@ -791,15 +797,21 @@ describe('Onyx', () => {
791797
})
792798
.then(waitForPromisesToResolve)
793799
.then(() => {
794-
// Safe key should be evicted first
800+
// evictableKey1 should be evicted since it's an evictable key
801+
// and triggerKey is the most recently connected one
795802
expect(cache.hasCacheForKey(evictableKey1)).toBe(false);
796803

797-
// Oldest non-safe key should be evicted next (LRU order)
798-
expect(cache.hasCacheForKey(criticalKey1)).toBe(false);
799-
800-
// More recent non-safe keys should remain in cache
804+
// Non-evictable keys should remain in cache
805+
expect(cache.hasCacheForKey(criticalKey1)).toBe(true);
801806
expect(cache.hasCacheForKey(criticalKey2)).toBe(true);
802807
expect(cache.hasCacheForKey(criticalKey3)).toBe(true);
808+
809+
// The trigger key should be in cache as it was just connected
810+
expect(cache.hasCacheForKey(triggerKey)).toBe(true);
811+
812+
// The cache size should exceed the configured limit (2) since
813+
// we can't remove non-evictable keys
814+
expect(cache.getRecentKeysSize()).toBeGreaterThan(2);
803815
});
804816
});
805817
});

0 commit comments

Comments
 (0)