import { getHeaders } from 'h3'; import type { H3Event } from 'h3'; interface CacheEntry { data: T; expiresAt: number; } // Хранилище закэшированных ответов (ключ — URL + язык, значение — данные + время истечения) const cache = new Map>(); // Хранилище текущих незавершённых запросов для дедупликации параллельных вызовов const inflight = new Map>(); /** * Формирует ключ кэша с учётом языка из заголовка X-Language. */ function getCacheKey(url: string, event: H3Event): string { const lang = event.node.req.headers['x-language'] || 'en'; return `${lang}::${url}`; } /** * Выполняет GET-запрос к API с кэшированием и дедупликацией. * @param url — адрес API-эндпоинта * @param event — H3-событие текущего запроса (нужно для ServerApiHelper) * @param maxAge — время жизни кэша в секундах (по умолчанию 600 = 10 минут) */ export async function cachedApiFetch(url: string, event: H3Event, maxAge = 600): Promise { const cacheKey = getCacheKey(url, event); // Если данные есть в кэше и ещё не протухли — возвращаем сразу const cached = cache.get(cacheKey); if (cached && cached.expiresAt > Date.now()) { return cached.data as T; } // Если запрос по этому ключу уже летит — не делаем дубль, а ждём тот же промис const existing = inflight.get(cacheKey); if (existing) { return existing as Promise; } // Запускаем реальный запрос к API const promise = apiFetch(url, { method: 'GET', headers: getHeaders(event) as HeadersInit, }) .then((rawData: any) => { const data = (rawData && typeof rawData === 'object' && 'data' in rawData ? rawData.data : rawData) as T; if (cache.size > 1000) { const now = Date.now(); for (const [k, v] of cache) { if (v.expiresAt < now) cache.delete(k); if (cache.size <= 800) break; } } cache.set(cacheKey, { data, expiresAt: Date.now() + maxAge * 1000 }); return data; }) .finally(() => { // Убираем промис из inflight — запрос завершён (успешно или с ошибкой) inflight.delete(cacheKey); }); // Регистрируем промис как «в полёте», чтобы параллельные вызовы подхватили его inflight.set(cacheKey, promise); return promise; }