From e88beff2b227244194089246df37590b802f7877 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Tue, 12 Dec 2023 08:47:57 +0100 Subject: [PATCH] feat: do not cache old search and filter results (#5600) --- .../useFeatureSearch.test.tsx | 67 +++++++++++++++++++ .../useFeatureSearch/useFeatureSearch.ts | 20 +++++- 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx new file mode 100644 index 0000000000..98a82cbb68 --- /dev/null +++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.test.tsx @@ -0,0 +1,67 @@ +import React, { FC } from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; +import { useFeatureSearch } from './useFeatureSearch'; +import { useSWRConfig } from 'swr'; + +const server = testServerSetup(); + +const TestComponent: FC<{ params: { project: string } }> = ({ params }) => { + const { loading, error, features, total } = useFeatureSearch(params); + const { cache } = useSWRConfig(); + + if (loading) { + return
Loading...
; + } + + if (error) { + return
Error: {error}
; + } + + return ( +
+
Features: {features.map((f) => f.name).join(', ')}
+
Total: {total}
+
Cache: {[...cache.keys()]}
+
+ ); +}; + +describe('useFeatureSearch', () => { + test('should return features after loading', async () => { + testServerRoute(server, '/api/admin/search/features?project=project1', { + features: [{ name: 'Feature1' }, { name: 'Feature2' }], + total: 2, + }); + + render(); + + await screen.findByText(/Loading.../); + + await screen.findByText(/Features:/); + await screen.findByText(/Total:/); + }); + + test('should keep only latest cache entry', async () => { + testServerRoute(server, '/api/admin/search/features?project=project1', { + features: [{ name: 'Feature1' }], + total: 1, + }); + render(); + await screen.findByText(/Features:/); + await screen.findByText( + 'Cache: api/admin/search/features?project=project1', + ); + + testServerRoute(server, '/api/admin/search/features?project=project2', { + features: [{ name: 'Feature2' }], + total: 1, + }); + render(); + await screen.findByText(/Features:/); + await screen.findByText( + 'Cache: api/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 1f0f976417..a9e56b4eba 100644 --- a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts +++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts @@ -1,4 +1,4 @@ -import useSWR, { SWRConfiguration } from 'swr'; +import useSWR, { SWRConfiguration, useSWRConfig } from 'swr'; import { useCallback, useEffect } from 'react'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; @@ -24,6 +24,21 @@ const fallbackData: SearchFeaturesSchema = { total: 0, }; +const PREFIX_KEY = 'api/admin/search/features?'; + +/** + 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` + */ +const useClearSWRCache = (currentKey: string, clearPrefix: string) => { + const { cache } = useSWRConfig(); + const keys = [...cache.keys()]; + keys.filter((key) => key !== currentKey && key.startsWith(clearPrefix)).map( + (key) => cache.delete(key), + ); +}; + const createFeatureSearch = () => { const internalCache: InternalCache = {}; @@ -54,6 +69,7 @@ const createFeatureSearch = () => { ): UseFeatureSearchOutput => { const { KEY, fetcher } = getFeatureSearchFetcher(params); const cacheId = params.project || ''; + useClearSWRCache(KEY, PREFIX_KEY); useEffect(() => { initCache(params.project || ''); @@ -103,7 +119,7 @@ const getFeatureSearchFetcher = (params: SearchFeaturesParams) => { .map(([key, value]) => [key, value.toString()]), // TODO: parsing non-string parameters ), ).toString(); - const KEY = `api/admin/search/features?${urlSearchParams}`; + const KEY = `${PREFIX_KEY}${urlSearchParams}`; const fetcher = () => { const path = formatApiPath(KEY); return fetch(path, {