diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx index 62a4bc0c7d..7a168377df 100644 --- a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx +++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx @@ -43,7 +43,7 @@ describe('useFeatureSearch', () => { await screen.findByText(/Total:/); }); - test('should keep only latest cache entry', async () => { + test('should keep at least latest cache entry', async () => { testServerRoute(server, '/api/admin/search/features?project=project1', { features: [{ name: 'Feature1' }], total: 1, @@ -61,7 +61,7 @@ describe('useFeatureSearch', () => { render(); await screen.findByText(/Features:/); await screen.findByText( - 'Cache: api/admin/search/features?project=project2', + 'Cache: api/admin/search/features?project=project1api/admin/search/features?project=project2', ); }); }); diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts index eee25a3358..2fc97150fd 100644 --- a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts +++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts @@ -25,6 +25,7 @@ const fallbackData: SearchFeaturesSchema = { total: 0, }; +const SWR_CACHE_SIZE = 10; const PREFIX_KEY = 'api/admin/search/features?'; const createFeatureSearch = () => { @@ -57,7 +58,7 @@ const createFeatureSearch = () => { ): UseFeatureSearchOutput => { const { KEY, fetcher } = getFeatureSearchFetcher(params); const cacheId = params.project || ''; - useClearSWRCache(KEY, PREFIX_KEY); + useClearSWRCache(KEY, PREFIX_KEY, SWR_CACHE_SIZE); useEffect(() => { initCache(params.project || ''); @@ -97,8 +98,6 @@ const createFeatureSearch = () => { export const DEFAULT_PAGE_LIMIT = 25; -export const useFeatureSearch = createFeatureSearch(); - const getFeatureSearchFetcher = (params: SearchFeaturesParams) => { const urlSearchParams = new URLSearchParams( Array.from( @@ -122,3 +121,5 @@ const getFeatureSearchFetcher = (params: SearchFeaturesParams) => { KEY, }; }; + +export const useFeatureSearch = createFeatureSearch(); diff --git a/frontend/src/hooks/useClearSWRCache.test.ts b/frontend/src/hooks/useClearSWRCache.test.ts new file mode 100644 index 0000000000..1a3498a01e --- /dev/null +++ b/frontend/src/hooks/useClearSWRCache.test.ts @@ -0,0 +1,53 @@ +import { clearCacheEntries } from './useClearSWRCache'; + +describe('manageCacheEntries', () => { + it('should clear old cache entries and keep the current one when SWR_CACHE_SIZE is not provided', () => { + const cacheMock = new Map(); + cacheMock.set('prefix-1', {}); + cacheMock.set('prefix-2', {}); + cacheMock.set('prefix-3', {}); + + clearCacheEntries(cacheMock, 'prefix-3', 'prefix-'); + + expect(cacheMock.has('prefix-1')).toBe(false); + expect(cacheMock.has('prefix-2')).toBe(false); + expect(cacheMock.has('prefix-3')).toBe(true); + }); + + it('should keep the SWR_CACHE_SIZE entries and delete the rest', () => { + const cacheMock = new Map(); + cacheMock.set('prefix-1', {}); + cacheMock.set('prefix-2', {}); + cacheMock.set('prefix-3', {}); + cacheMock.set('prefix-4', {}); + + clearCacheEntries(cacheMock, 'prefix-4', 'prefix-', 2); + + expect(cacheMock.has('prefix-4')).toBe(true); + expect([...cacheMock.keys()].length).toBe(2); + }); + + it('should handle case when SWR_CACHE_SIZE is larger than number of entries', () => { + const cacheMock = new Map(); + cacheMock.set('prefix-1', {}); + cacheMock.set('prefix-2', {}); + + clearCacheEntries(cacheMock, 'prefix-2', 'prefix-', 5); + + expect(cacheMock.has('prefix-1')).toBe(true); + expect(cacheMock.has('prefix-2')).toBe(true); + }); + + it('should not delete entries that do not match the prefix', () => { + const cacheMock = new Map(); + cacheMock.set('prefix-1', {}); + cacheMock.set('other-2', {}); + cacheMock.set('prefix-3', {}); + + clearCacheEntries(cacheMock, 'prefix-3', 'prefix-', 2); + + expect(cacheMock.has('prefix-1')).toBe(true); + expect(cacheMock.has('other-2')).toBe(true); + expect(cacheMock.has('prefix-3')).toBe(true); + }); +}); diff --git a/frontend/src/hooks/useClearSWRCache.ts b/frontend/src/hooks/useClearSWRCache.ts index 359ac102c0..32db4c6ce7 100644 --- a/frontend/src/hooks/useClearSWRCache.ts +++ b/frontend/src/hooks/useClearSWRCache.ts @@ -1,14 +1,33 @@ import { useSWRConfig } from 'swr'; +type Cache = ReturnType['cache']; + +export const clearCacheEntries = ( + cache: Cache, + currentKey: string, + clearPrefix: string, + SWR_CACHE_SIZE = 1, +) => { + const keys = [...cache.keys()]; + + const filteredKeys = keys.filter( + (key) => key.startsWith(clearPrefix) && key !== currentKey, + ); + const keysToDelete = filteredKeys.slice(SWR_CACHE_SIZE - 1); + + keysToDelete.forEach((key) => cache.delete(key)); +}; + /** With dynamic search and filter parameters we want to prevent cache from growing extensively. We only keep the latest cache key `currentKey` and remove all other entries identified by the `clearPrefix` */ -export const useClearSWRCache = (currentKey: string, clearPrefix: string) => { +export const useClearSWRCache = ( + currentKey: string, + clearPrefix: string, + SWR_CACHE_SIZE = 1, +) => { const { cache } = useSWRConfig(); - const keys = [...cache.keys()]; - keys.filter((key) => key !== currentKey && key.startsWith(clearPrefix)).map( - (key) => cache.delete(key), - ); + clearCacheEntries(cache, currentKey, clearPrefix, SWR_CACHE_SIZE); };