From 82a53fa9b3540f04b6a0f60c1d0f33da271bf79e Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Fri, 28 Jun 2024 10:44:35 +0300 Subject: [PATCH] feat: introduce large cache for swr (#7470) Previously, clearing the SWR cache cleared all entries. Now you can configure the cache size. 1. This makes the search more fluid. Previously, if you went back and forth on pages, you were always sent to the loading state. 2. This also solves the issue where the command bar search cleared the cache for all other searches. 3. Additionally, it addresses the problem where the global search cleared the cache for project search. --- .../useFeatureSearch.test.tsx | 4 +- .../useFeatureSearch/useFeatureSearch.ts | 7 +-- frontend/src/hooks/useClearSWRCache.test.ts | 53 +++++++++++++++++++ frontend/src/hooks/useClearSWRCache.ts | 29 ++++++++-- 4 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 frontend/src/hooks/useClearSWRCache.test.ts 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); };