From bb98681ea6a45119c3cc71a2ede0dfb34e340374 Mon Sep 17 00:00:00 2001 From: Stysusss <158248053+stysus@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:14:49 +0700 Subject: [PATCH] perf: client-side API response cache (STY-96 step 1) Frontend (delegated to agent frontend): - fetchJsonCached helper with 60s TTL in-memory cache (Map) - getCategories & getNews use cache; only successful responses cached - Key per URL (unique per query/category) - clearCache(pattern?) for invalidation - Aim: subsequent visits render instantly instead of waiting for API Verified: svelte-check 0 errors, lint pass, build ok. --- frontend/src/lib/api.ts | 80 +++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0c8aa85..9108a7e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -3,6 +3,59 @@ import { mockCategories, type Category, type News } from './mockData'; export const BASE_URL = (env.PUBLIC_API_URL?.trim() || '/api').replace(/\/+$/, ''); +// --------------------------------------------------------------------------- +// Client-side API response cache (STY-96) +// --------------------------------------------------------------------------- +// Only successful responses (res.ok) are cached. Failed/fallback results are +// never stored, so the next call retries the network. + +const API_CACHE_TTL = 60_000; // 60 s — tunable + +interface CacheEntry { + data: unknown; + expiresAt: number; +} + +const apiCache = new Map(); + +/** Fetch JSON, returning cached data when still valid. */ +async function fetchJsonCached( + url: string, + f: typeof fetch, + signal?: AbortSignal +): Promise { + const now = Date.now(); + const cached = apiCache.get(url); + if (cached && now < cached.expiresAt) return cached.data as T; + + try { + const res = await f(url, { signal }); + if (res.ok) { + const data = await res.json(); + apiCache.set(url, { data, expiresAt: now + API_CACHE_TTL }); + return data; + } + } catch { + // network / timeout — fall through to return null + } + + return null; +} + +/** + * Clear the API cache. If `pattern` is provided, only keys that include the + * substring are removed; otherwise the entire cache is flushed. + */ +export function clearCache(pattern?: string) { + if (!pattern) { + apiCache.clear(); + return; + } + for (const key of [...apiCache.keys()]) { + if (key.includes(pattern)) apiCache.delete(key); + } +} + /** * Gets SvelteKit-compatible fetch or global fetch. */ @@ -15,15 +68,12 @@ function getFetch(customFetch?: typeof fetch): typeof fetch { */ export async function getCategories(customFetch?: typeof fetch): Promise { const f = getFetch(customFetch); - try { - const res = await f(`${BASE_URL}/categories`, { signal: AbortSignal.timeout(2000) }); - if (res.ok) { - const data = await res.json(); - return data && data.data && data.data.length > 0 ? data.data : mockCategories; - } - } catch (e) { - console.warn('Backend categories API unreachable, using mock categories.', e); - } + const data = await fetchJsonCached<{ data: Category[] }>( + `${BASE_URL}/categories`, + f, + AbortSignal.timeout(2000) + ); + if (data?.data && data.data.length > 0) return data.data; return mockCategories; } @@ -36,7 +86,6 @@ export async function getNews( searchQuery?: string ): Promise { const f = getFetch(customFetch); - let articles: News[] = []; // Build query string. Search uses the backend ?q= endpoint, optionally combined // with a category filter; otherwise fetch a large page to populate the feeds. @@ -48,15 +97,8 @@ export async function getNews( url += `&category=${encodeURIComponent(categorySlug)}`; } - try { - const res = await f(url, { signal: AbortSignal.timeout(2000) }); - if (res.ok) { - const result = await res.json(); - articles = result && result.data ? result.data : []; - } - } catch (e) { - console.warn('Backend news API unreachable.', e); - } + const result = await fetchJsonCached<{ data: News[] }>(url, f, AbortSignal.timeout(2000)); + const articles = result?.data ?? []; // Filter by published status (backend search already filters by category/query). return articles