interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
accessCount: number;
lastAccessed: number;
}
interface CacheStats {
hits: number;
misses: number;
sets: number;
deletes: number;
evictions: number;
totalRequests: number;
hitRate: number;
}
interface CacheConfig {
maxSize: number;
defaultTtl: number;
cleanupInterval: number;
enableStats: boolean;
}
export class CacheManager {
private cache = new Map<string, CacheEntry<any>>();
private stats: CacheStats;
private config: CacheConfig;
private cleanupTimer: NodeJS.Timeout | null = null;
constructor(config: Partial<CacheConfig> = {}) {
this.config = {
maxSize: config.maxSize || 1000,
defaultTtl: config.defaultTtl || 5 * 60 * 1000, // 5 minutes
cleanupInterval: config.cleanupInterval || 60 * 1000, // 1 minute
enableStats: config.enableStats !== false,
};
this.stats = {
hits: 0,
misses: 0,
sets: 0,
deletes: 0,
evictions: 0,
totalRequests: 0,
hitRate: 0,
};
this.startCleanupTimer();
}
/**
* Get value from cache
*/
get<T>(key: string): T | null {
this.updateStats('request');
const entry = this.cache.get(key);
if (!entry) {
this.updateStats('miss');
return null;
}
// Check if expired
if (this.isExpired(entry)) {
this.cache.delete(key);
this.updateStats('miss');
return null;
}
// Update access statistics
entry.accessCount++;
entry.lastAccessed = Date.now();
this.updateStats('hit');
return entry.data;
}
/**
* Set value in cache
*/
set<T>(key: string, value: T, ttl?: number): void {
const currentTtl = ttl || this.config.defaultTtl;
// Enforce cache size limit
if (this.cache.size >= this.config.maxSize && !this.cache.has(key)) {
this.evictLeastRecentlyUsed();
}
const entry: CacheEntry<T> = {
data: value,
timestamp: Date.now(),
ttl: currentTtl,
accessCount: 0,
lastAccessed: Date.now(),
};
this.cache.set(key, entry);
this.updateStats('set');
}
/**
* Delete value from cache
*/
delete(key: string): boolean {
const deleted = this.cache.delete(key);
if (deleted) {
this.updateStats('delete');
}
return deleted;
}
/**
* Check if key exists and is not expired
*/
has(key: string): boolean {
const entry = this.cache.get(key);
if (!entry) return false;
if (this.isExpired(entry)) {
this.cache.delete(key);
return false;
}
return true;
}
/**
* Clear all cache entries
*/
clear(): void {
const size = this.cache.size;
this.cache.clear();
this.stats.deletes += size;
}
/**
* Get cache statistics
*/
getStats(): CacheStats {
const totalRequests = this.stats.hits + this.stats.misses;
return {
...this.stats,
totalRequests,
hitRate: totalRequests > 0 ? Math.round((this.stats.hits / totalRequests) * 100 * 100) / 100 : 0,
};
}
/**
* Get cache info
*/
getInfo() {
const now = Date.now();
const entries = Array.from(this.cache.entries()).map(([key, entry]) => ({
key,
size: JSON.stringify(entry.data).length,
age: now - entry.timestamp,
ttl: entry.ttl,
timeToExpiry: entry.ttl - (now - entry.timestamp),
accessCount: entry.accessCount,
expired: this.isExpired(entry),
}));
return {
size: this.cache.size,
maxSize: this.config.maxSize,
config: this.config,
stats: this.getStats(),
entries: entries.sort((a, b) => b.accessCount - a.accessCount),
};
}
/**
* Cleanup expired entries
*/
cleanup(): number {
const sizeBefore = this.cache.size;
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (this.isExpired(entry)) {
this.cache.delete(key);
}
}
const removed = sizeBefore - this.cache.size;
this.stats.evictions += removed;
return removed;
}
/**
* Get or set pattern - fetch if not in cache
*/
async getOrSet<T>(
key: string,
fetcher: () => Promise<T>,
ttl?: number
): Promise<T> {
const cached = this.get<T>(key);
if (cached !== null) {
return cached;
}
const value = await fetcher();
this.set(key, value, ttl);
return value;
}
/**
* Invalidate cache entries by pattern
*/
invalidatePattern(pattern: RegExp): number {
let deleted = 0;
for (const key of this.cache.keys()) {
if (pattern.test(key)) {
this.cache.delete(key);
deleted++;
}
}
this.stats.deletes += deleted;
return deleted;
}
/**
* Warm cache with data
*/
warm<T>(entries: Array<{ key: string; value: T; ttl?: number }>): void {
entries.forEach(({ key, value, ttl }) => {
this.set(key, value, ttl);
});
}
/**
* Shutdown cache manager
*/
shutdown(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
this.clear();
}
private isExpired(entry: CacheEntry<any>): boolean {
return Date.now() - entry.timestamp > entry.ttl;
}
private updateStats(operation: 'hit' | 'miss' | 'set' | 'delete' | 'request'): void {
if (!this.config.enableStats) return;
switch (operation) {
case 'hit':
this.stats.hits++;
break;
case 'miss':
this.stats.misses++;
break;
case 'set':
this.stats.sets++;
break;
case 'delete':
this.stats.deletes++;
break;
case 'request':
// This is handled by calculating totalRequests
break;
}
}
private evictLeastRecentlyUsed(): void {
if (this.cache.size === 0) return;
let lruKey: string | null = null;
let lruTime = Infinity;
for (const [key, entry] of this.cache.entries()) {
if (entry.lastAccessed < lruTime) {
lruTime = entry.lastAccessed;
lruKey = key;
}
}
if (lruKey) {
this.cache.delete(lruKey);
this.stats.evictions++;
}
}
private startCleanupTimer(): void {
this.cleanupTimer = setInterval(() => {
this.cleanup();
}, this.config.cleanupInterval);
}
}