Add multi-layer caching: 24h Redis TTL, stale-while-revalidate, frontend LRU cache
- Increase Redis cache TTL from 30 minutes to 24 hours - Add stale-while-revalidate: serve stale cache (>4h) immediately while repopulating in background with SETNX lock to prevent concurrent rebuilds - Add in-memory frontend LRU cache (5 entries) so repeat filter visits are instant without network requests - Invalidate frontend cache on listing refresh and task completion - Add unit tests for get_cache_age, is_cache_stale, acquire_repopulation_lock
This commit is contained in:
parent
04bda8c127
commit
1ae00b7cbf
5 changed files with 270 additions and 1 deletions
|
|
@ -18,6 +18,7 @@ import { Button } from './components/ui/button';
|
|||
import { Filter, Heart } from 'lucide-react';
|
||||
import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature, POI, POITravelFilter } from '@/types';
|
||||
import { refreshListings, streamListingGeoJSON, fetchUserPOIs, fetchBulkPOIDistances, type StreamingProgress } from '@/services';
|
||||
import { getCached, setCached, invalidateAll as invalidateListingCache } from '@/services/listingCache';
|
||||
import { setOnUnauthorized } from '@/services/apiClient';
|
||||
import { clearPasskeyUser } from './auth/passkeyService';
|
||||
import { poiMetricPropertyName, injectPoiMetricProperty } from '@/utils/poiUtils';
|
||||
|
|
@ -131,6 +132,18 @@ function App() {
|
|||
const loadListings = useCallback(async (parameters: ParameterValues) => {
|
||||
if (!user) return;
|
||||
|
||||
// Check in-memory cache first
|
||||
const cached = getCached(parameters);
|
||||
if (cached) {
|
||||
setQueryParameters(parameters);
|
||||
setMobileFilterOpen(false);
|
||||
accumulatedFeaturesRef.current = cached;
|
||||
setListingData({ type: 'FeatureCollection', features: [...cached] });
|
||||
setIsLoading(false);
|
||||
setStreamingProgress(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort any in-flight streaming request
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
|
|
@ -183,6 +196,8 @@ function App() {
|
|||
}
|
||||
// Final flush to ensure all data is rendered
|
||||
flushUpdate();
|
||||
// Store successful result in frontend cache
|
||||
setCached(parameters, accumulatedFeaturesRef.current);
|
||||
} catch (error) {
|
||||
// Silently ignore AbortError — it means we intentionally cancelled
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
|
|
@ -305,6 +320,7 @@ function App() {
|
|||
}, [user, loadListings]);
|
||||
|
||||
const handleTaskCompleted = useCallback(() => {
|
||||
invalidateListingCache();
|
||||
if (queryParameters) {
|
||||
loadListings(queryParameters);
|
||||
}
|
||||
|
|
@ -326,6 +342,7 @@ function App() {
|
|||
if (action === 'visualize') {
|
||||
loadListings(parameters);
|
||||
} else if (action === 'fetch-data') {
|
||||
invalidateListingCache();
|
||||
setQueryParameters(parameters);
|
||||
setMobileFilterOpen(false);
|
||||
setIsLoading(true);
|
||||
|
|
|
|||
45
frontend/src/services/listingCache.ts
Normal file
45
frontend/src/services/listingCache.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* In-memory LRU cache for streaming listing results.
|
||||
*
|
||||
* Keyed by a deterministic hash of query parameters so that repeat visits
|
||||
* to the same filter combination are instant (no network request).
|
||||
*/
|
||||
import type { PropertyFeature } from '@/types';
|
||||
import type { ParameterValues } from '@/components/FilterPanel';
|
||||
|
||||
interface CacheEntry {
|
||||
features: PropertyFeature[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const cache = new Map<string, CacheEntry>();
|
||||
const MAX_ENTRIES = 5;
|
||||
|
||||
export function makeCacheKey(params: ParameterValues): string {
|
||||
const sorted = Object.entries(params)
|
||||
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([k, v]) => `${k}=${v instanceof Date ? v.toISOString() : v}`);
|
||||
return sorted.join('&');
|
||||
}
|
||||
|
||||
export function getCached(params: ParameterValues): PropertyFeature[] | null {
|
||||
const key = makeCacheKey(params);
|
||||
const entry = cache.get(key);
|
||||
if (!entry) return null;
|
||||
return entry.features;
|
||||
}
|
||||
|
||||
export function setCached(params: ParameterValues, features: PropertyFeature[]): void {
|
||||
const key = makeCacheKey(params);
|
||||
if (cache.size >= MAX_ENTRIES && !cache.has(key)) {
|
||||
// Evict oldest entry (first inserted)
|
||||
const oldest = cache.keys().next().value;
|
||||
if (oldest) cache.delete(oldest);
|
||||
}
|
||||
cache.set(key, { features, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
export function invalidateAll(): void {
|
||||
cache.clear();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue