278 lines
No EOL
6.7 KiB
TypeScript
278 lines
No EOL
6.7 KiB
TypeScript
// =============================================================================
|
|
// CENTRALIZED CACHE CLEANUP UTILITY
|
|
// =============================================================================
|
|
// Consolidates all cache cleanup logic to prevent duplication
|
|
|
|
import { parseDuration } from './time.js';
|
|
|
|
export interface CacheEntry<T = unknown> {
|
|
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<K>(
|
|
cache: Map<K, TTLCacheEntry>,
|
|
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<K>(
|
|
cache: Map<K, TTLCacheEntry>,
|
|
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<K>(
|
|
cache: Map<K, TTLCacheEntry>,
|
|
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<K, T extends { timestamp?: number; lastReset?: number }>(
|
|
cache: Map<K, T>,
|
|
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<K, T>(
|
|
cache: Map<K, T>,
|
|
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<K>(
|
|
cache: Map<K, { count: number; lastReset: number }>,
|
|
windowMs: number,
|
|
now: number = Date.now()
|
|
): number {
|
|
return TimestampCacheCleaner.cleanExpired(cache, windowMs, 'lastReset', now);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Specialized cleaner for reputation caches
|
|
*/
|
|
export class ReputationCacheCleaner {
|
|
static cleanExpiredReputation<K>(
|
|
cache: Map<K, { reputation: unknown; timestamp: number }>,
|
|
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<string, NodeJS.Timeout> = new Map();
|
|
|
|
/**
|
|
* Sets up automatic cleanup for a cache
|
|
*/
|
|
setupPeriodicCleanup<K>(
|
|
cacheName: string,
|
|
cache: Map<K, TTLCacheEntry>,
|
|
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<T>(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<T>(cache: Map<string, TTLCacheEntry>, 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;
|
|
}
|
|
};
|