Checkpoint/src/utils/cache-utils.ts

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;
}
};