// Task 6: In-memory cache with TTL + Rate Limiting const responseCache = new Map(); function cacheGet(key) { const entry = responseCache.get(key); if (!entry) return null; if (Date.now() > entry.expires) { responseCache.delete(key); return null; } return entry.data; } function cacheSet(key, data, ttlMs) { responseCache.set(key, { data, expires: Date.now() + ttlMs }); // Evict old entries periodically (keep cache under 500 entries) if (responseCache.size > 500) { const now = Date.now(); for (const [k, v] of responseCache) { if (now > v.expires) responseCache.delete(k); } } } const CACHE_TTL_LOOKUP = 5 * 60 * 1000; // 5 minutes const CACHE_TTL_LOOKUP_DEGRADED = 90 * 1000; // matches the existing 90s guard for suspicious /api/validate zero-counts const CACHE_TTL_DEFAULT = 5 * 60 * 1000; // 5 minutes // RDAP Cache — prevents 429 flooding on LACNIC/AFRINIC/APNIC/ARIN const rdapCache = new Map(); // key: asn string, value: { data, ts } const RDAP_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours function rdapCacheGet(asn) { const e = rdapCache.get(String(asn)); if (e && (Date.now() - e.ts) < RDAP_CACHE_TTL) return e.data; return undefined; // undefined = not cached, null = cached miss } function rdapCacheSet(asn, data) { if (rdapCache.size > 5000) { rdapCache.delete(rdapCache.keys().next().value); } rdapCache.set(String(asn), { data, ts: Date.now() }); } // WHOIS Cache — 24h TTL, prevents repeated RDAP hammering const whoisCache = new Map(); // key: asn string, value: { data, ts, ttl } const WHOIS_CACHE_TTL = 24 * 60 * 60 * 1000; // A "not found" result after all 5 RIR sources failed is far more often a shared // network blip (DNS, outbound connectivity, simultaneous rate-limit) than a real // ASN existing nowhere across RIPE+APNIC+ARIN+LACNIC+AFRINIC -- fetchJSON can't // tell timeout/network-error apart from a genuine 404, so cache that specific // conclusion much more briefly than a confirmed, parsed WHOIS record. const WHOIS_NOT_FOUND_CACHE_TTL = 10 * 60 * 1000; function whoisCacheGet(asn) { const e = whoisCache.get(String(asn)); if (e && (Date.now() - e.ts) < (e.ttl || WHOIS_CACHE_TTL)) return e.data; return undefined; } function whoisCacheSet(asn, data, ttl) { if (whoisCache.size > 5000) whoisCache.delete(whoisCache.keys().next().value); whoisCache.set(String(asn), { data, ts: Date.now(), ttl: ttl || WHOIS_CACHE_TTL }); } // Quick-IX Cache — 1h TTL, for Peering Recommendations const quickIxCache = new Map(); // key: asn string, value: { data, ts } const QUICK_IX_CACHE_TTL = 60 * 60 * 1000; function quickIxCacheGet(asn) { const e = quickIxCache.get(String(asn)); if (e && (Date.now() - e.ts) < QUICK_IX_CACHE_TTL) return e.data; return undefined; } function quickIxCacheSet(asn, data) { if (quickIxCache.size > 2000) quickIxCache.delete(quickIxCache.keys().next().value); quickIxCache.set(String(asn), { data, ts: Date.now() }); } // bgproutes + ASPA result caches — 15min TTL, prevent re-hitting slow APIs const bgproutesResultCache = new Map(); const aspaResultCache = new Map(); const validateResultCache = new Map(); const RESULT_CACHE_TTL = 15 * 60 * 1000; // 15 minutes function resultCacheGet(map, key) { const e = map.get(String(key)); const ttl = e && e.ttl ? e.ttl : RESULT_CACHE_TTL; if (e && (Date.now() - e.ts) < ttl) return e.data; return undefined; } function resultCacheSet(map, key, data, ttlMs) { if (map.size > 2000) map.delete(map.keys().next().value); map.set(String(key), { data, ts: Date.now(), ttl: ttlMs || RESULT_CACHE_TTL }); } module.exports = { responseCache, cacheGet, cacheSet, CACHE_TTL_LOOKUP, CACHE_TTL_LOOKUP_DEGRADED, CACHE_TTL_DEFAULT, rdapCacheGet, rdapCacheSet, whoisCacheGet, whoisCacheSet, WHOIS_NOT_FOUND_CACHE_TTL, quickIxCacheGet, quickIxCacheSet, bgproutesResultCache, aspaResultCache, validateResultCache, resultCacheGet, resultCacheSet, };