Skip to content

Commit

Permalink
feat: introduce large cache for swr (#7470)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sjaanus authored Jun 28, 2024
1 parent 95359ec commit 82a53fa
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -61,7 +61,7 @@ describe('useFeatureSearch', () => {
render(<TestComponent params={{ project: 'project2' }} />);
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',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const fallbackData: SearchFeaturesSchema = {
total: 0,
};

const SWR_CACHE_SIZE = 10;
const PREFIX_KEY = 'api/admin/search/features?';

const createFeatureSearch = () => {
Expand Down Expand Up @@ -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 || '');
Expand Down Expand Up @@ -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(
Expand All @@ -122,3 +121,5 @@ const getFeatureSearchFetcher = (params: SearchFeaturesParams) => {
KEY,
};
};

export const useFeatureSearch = createFeatureSearch();
53 changes: 53 additions & 0 deletions frontend/src/hooks/useClearSWRCache.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
29 changes: 24 additions & 5 deletions frontend/src/hooks/useClearSWRCache.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import { useSWRConfig } from 'swr';

type Cache = ReturnType<typeof useSWRConfig>['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);
};

0 comments on commit 82a53fa

Please sign in to comment.