// ============================================================================= // CENTRALIZED CACHE CLEANUP UTILITY // ============================================================================= // Consolidates all cache cleanup logic to prevent duplication import { parseDuration } from './time.js'; export interface CacheEntry { readonly value: T; readonly timestamp: number; readonly ttl?: number; } export interface CacheOptions { readonly maxSize?: number; readonly defaultTTL?: number; readonly cleanupRatio?: number; // What percentage to clean when over limit (0.0-1.0) } export interface CacheCleanupResult { readonly expired: number; readonly overflow: number; readonly total: number; } export interface TTLCacheEntry { readonly data: unknown; readonly expires: number; } /** * Generic TTL-based cache cleaner */ export class TTLCacheCleaner { /** * Cleans expired entries from a Map-based cache with TTL entries * @param cache - Map cache to clean * @param now - Current timestamp (defaults to Date.now()) * @returns Number of entries removed */ static cleanExpired( cache: Map, now: number = Date.now() ): number { let cleaned = 0; for (const [key, entry] of cache.entries()) { if (now >= entry.expires) { cache.delete(key); cleaned++; } } return cleaned; } /** * Cleans cache by removing oldest entries when over size limit * @param cache - Map cache to clean * @param maxSize - Maximum allowed size * @param cleanupRatio - What percentage to remove (default 0.25 = 25%) * @returns Number of entries removed */ static cleanOverflow( cache: Map, maxSize: number, cleanupRatio: number = 0.25 ): number { if (cache.size <= maxSize) { return 0; } const targetSize = Math.floor(maxSize * (1 - cleanupRatio)); const toRemove = cache.size - targetSize; // Remove oldest entries (based on Map insertion order) let removed = 0; for (const key of cache.keys()) { if (removed >= toRemove) break; cache.delete(key); removed++; } return removed; } /** * Comprehensive cache cleanup (expired + overflow) */ static cleanup( cache: Map, options: CacheOptions = {} ): CacheCleanupResult { const { maxSize = 10000, cleanupRatio = 0.25 } = options; const now = Date.now(); const expired = this.cleanExpired(cache, now); const overflow = this.cleanOverflow(cache, maxSize, cleanupRatio); return { expired, overflow, total: expired + overflow }; } } /** * Generic timestamped cache cleaner (for caches with timestamp fields) */ export class TimestampCacheCleaner { /** * Cleans expired entries from cache with custom timestamp/TTL logic */ static cleanExpired( cache: Map, ttlMs: number, timestampField: 'timestamp' | 'lastReset' = 'timestamp', now: number = Date.now() ): number { let cleaned = 0; for (const [key, entry] of cache.entries()) { const entryTime = entry[timestampField]; if (!entryTime || (now - entryTime) > ttlMs) { cache.delete(key); cleaned++; } } return cleaned; } /** * Cleans cache entries with custom expiration logic */ static cleanWithCustomLogic( cache: Map, shouldExpire: (key: K, value: T, now: number) => boolean, now: number = Date.now() ): number { let cleaned = 0; for (const [key, entry] of cache.entries()) { if (shouldExpire(key, entry, now)) { cache.delete(key); cleaned++; } } return cleaned; } } /** * Specialized cleaner for rate limiting caches */ export class RateLimitCacheCleaner { static cleanExpiredRateLimits( cache: Map, windowMs: number, now: number = Date.now() ): number { return TimestampCacheCleaner.cleanExpired(cache, windowMs, 'lastReset', now); } } /** * Specialized cleaner for reputation caches */ export class ReputationCacheCleaner { static cleanExpiredReputation( cache: Map, ttlMs: number, now: number = Date.now() ): number { return TimestampCacheCleaner.cleanExpired(cache, ttlMs, 'timestamp', now); } } /** * High-level cache manager for common patterns */ export class CacheManager { private cleanupTimers: Map = new Map(); /** * Sets up automatic cleanup for a cache */ setupPeriodicCleanup( cacheName: string, cache: Map, options: CacheOptions & { interval?: string } = {} ): void { const { interval = '5m', maxSize = 10000 } = options; const intervalMs = parseDuration(interval); const timer = setInterval(() => { const result = TTLCacheCleaner.cleanup(cache, { maxSize }); if (result.total > 0) { console.log(`Cache ${cacheName}: cleaned ${result.expired} expired + ${result.overflow} overflow entries`); } }, intervalMs); // Store timer so it can be cleared later this.cleanupTimers.set(cacheName, timer); } /** * Stops periodic cleanup for a cache */ stopPeriodicCleanup(cacheName: string): void { const timer = this.cleanupTimers.get(cacheName); if (timer) { clearInterval(timer); this.cleanupTimers.delete(cacheName); } } /** * Stops all periodic cleanups */ stopAllCleanups(): void { for (const [_name, timer] of this.cleanupTimers.entries()) { clearInterval(timer); } this.cleanupTimers.clear(); } } // Export singleton cache manager export const cacheManager = new CacheManager(); /** * Utility functions for common cache operations */ export const CacheUtils = { /** * Creates a TTL cache entry */ createTTLEntry(value: T, ttlMs: number): TTLCacheEntry { return { data: value, expires: Date.now() + ttlMs }; }, /** * Checks if TTL entry is expired */ isExpired(entry: TTLCacheEntry, now: number = Date.now()): boolean { return now >= entry.expires; }, /** * Gets remaining TTL for an entry */ getRemainingTTL(entry: TTLCacheEntry, now: number = Date.now()): number { return Math.max(0, entry.expires - now); }, /** * Safely gets cache entry, returning null if expired */ safeGet(cache: Map, key: string): T | null { const entry = cache.get(key); if (!entry) { return null; } if (this.isExpired(entry)) { cache.delete(key); return null; } return entry.data as T; } };