import { Request, Response, NextFunction } from 'express'; interface CacheEntry { data: unknown; expiresAt: number; } const cache = new Map(); const DEFAULT_TTL = 60 * 1000; // 1 minute /** Never cache generic API error envelopes (avoids poisoning cache if status/body ever disagree). */ function looksLikeGenericErrorPayload(body: unknown): boolean { if (body == null || typeof body !== 'object' || Array.isArray(body)) return false; const o = body as Record; if (typeof o.error !== 'string') return false; // Success shapes we must not treat as errors if ('pools' in o || 'tokens' in o || 'data' in o || 'chains' in o || 'tree' in o || 'quote' in o) return false; if (o.status === 'healthy') return false; return true; } function buildCacheKey(req: Request): string { const base = `${req.method}:${req.originalUrl}`; if (req.method !== 'POST' && req.method !== 'PUT' && req.method !== 'PATCH') { return base; } try { const body = req.body; if (body == null) { return base; } const serialized = typeof body === 'string' ? body : JSON.stringify(body); if (!serialized || serialized === '{}' || serialized === '[]') { return base; } return `${base}:${serialized}`; } catch { return base; } } export function cacheMiddleware(ttl: number = DEFAULT_TTL) { return (req: Request, res: Response, next: NextFunction) => { const bypassCache = req.query.refresh === '1' || req.query.noCache === '1' || /\bno-cache\b|\bno-store\b/i.test(req.header('cache-control') || ''); const key = buildCacheKey(req); const cached = bypassCache ? undefined : cache.get(key); if (cached && cached.expiresAt > Date.now()) { res.setHeader('X-Token-Aggregation-Cache', 'hit'); return res.json(cached.data); } res.setHeader('X-Token-Aggregation-Cache', bypassCache ? 'bypass' : 'miss'); // Store original json method const originalJson = res.json.bind(res); // Override json method to cache response res.json = function (body: unknown) { // Only cache successful payloads. Otherwise a 500 body gets replayed on cache hit with HTTP 200. const okStatus = res.statusCode >= 200 && res.statusCode < 300; if (!bypassCache && okStatus && !looksLikeGenericErrorPayload(body)) { cache.set(key, { data: body, expiresAt: Date.now() + ttl, }); } return originalJson(body); }; next(); }; } export function clearCache(): void { cache.clear(); } export function clearCacheForPattern(pattern: string): void { for (const key of cache.keys()) { if (key.includes(pattern)) { cache.delete(key); } } }