CacheManager.ts•4.72 kB
import { CacheEntry, CacheStats, CacheConfig } from './types.js';
export class CacheManager {
  private cache: Map<string, CacheEntry>;
  private stats: CacheStats;
  private config: Required<CacheConfig>;
  private cleanupInterval: ReturnType<typeof setInterval>;
  private statsUpdateInterval: ReturnType<typeof setInterval>;
  constructor(config: CacheConfig = {}) {
    this.cache = new Map();
    this.stats = {
      totalEntries: 0,
      memoryUsage: 0,
      hits: 0,
      misses: 0,
      hitRate: 0,
      avgAccessTime: 0
    };
    
    // Set default configuration
    this.config = {
      maxEntries: config.maxEntries ?? 1000,
      maxMemory: config.maxMemory ?? 100 * 1024 * 1024, // 100MB default
      defaultTTL: config.defaultTTL ?? 3600, // 1 hour default
      checkInterval: config.checkInterval ?? 60 * 1000, // 1 minute default
      statsInterval: config.statsInterval ?? 30 * 1000 // 30 seconds default
    };
    // Start maintenance intervals
    this.cleanupInterval = setInterval(() => this.evictStale(), this.config.checkInterval);
    this.statsUpdateInterval = setInterval(() => this.updateStats(), this.config.statsInterval);
  }
  set(key: string, value: any, ttl?: number): void {
    const startTime = performance.now();
    
    // Calculate approximate size in bytes
    const size = this.calculateSize(value);
    
    // Check if adding this entry would exceed memory limit
    if (this.stats.memoryUsage + size > this.config.maxMemory) {
      this.enforceMemoryLimit(size);
    }
    const entry: CacheEntry = {
      value,
      created: Date.now(),
      lastAccessed: Date.now(),
      ttl: ttl ?? this.config.defaultTTL,
      size
    };
    this.cache.set(key, entry);
    this.stats.totalEntries = this.cache.size;
    this.stats.memoryUsage += size;
    const endTime = performance.now();
    this.updateAccessTime(endTime - startTime);
  }
  get(key: string): any {
    const startTime = performance.now();
    const entry = this.cache.get(key);
    if (!entry) {
      this.stats.misses++;
      this.updateHitRate();
      return undefined;
    }
    // Check if entry has expired
    if (this.isExpired(entry)) {
      this.delete(key);
      this.stats.misses++;
      this.updateHitRate();
      return undefined;
    }
    // Update last accessed time
    entry.lastAccessed = Date.now();
    this.stats.hits++;
    this.updateHitRate();
    const endTime = performance.now();
    this.updateAccessTime(endTime - startTime);
    return entry.value;
  }
  delete(key: string): boolean {
    const entry = this.cache.get(key);
    if (entry) {
      this.stats.memoryUsage -= entry.size;
      this.cache.delete(key);
      this.stats.totalEntries = this.cache.size;
      return true;
    }
    return false;
  }
  clear(): void {
    this.cache.clear();
    this.resetStats();
  }
  getStats(): CacheStats {
    return { ...this.stats };
  }
  private isExpired(entry: CacheEntry): boolean {
    return Date.now() > entry.created + (entry.ttl ?? this.config.defaultTTL) * 1000;
  }
  private evictStale(): void {
    for (const [key, entry] of this.cache.entries()) {
      if (this.isExpired(entry)) {
        this.delete(key);
      }
    }
  }
  private enforceMemoryLimit(requiredSize: number): void {
    // Use LRU strategy to remove entries until we have enough space
    const entries = Array.from(this.cache.entries())
      .sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed);
    while (this.stats.memoryUsage + requiredSize > this.config.maxMemory && entries.length > 0) {
      const [key] = entries.shift()!;
      this.delete(key);
    }
  }
  private calculateSize(value: any): number {
    // Rough estimation of memory usage in bytes
    const str = JSON.stringify(value);
    return str.length * 2; // Approximate UTF-16 encoding size
  }
  private updateHitRate(): void {
    const total = this.stats.hits + this.stats.misses;
    this.stats.hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
  }
  private updateAccessTime(duration: number): void {
    const total = this.stats.hits + this.stats.misses;
    this.stats.avgAccessTime = 
      ((this.stats.avgAccessTime * (total - 1)) + duration) / total;
  }
  private resetStats(): void {
    this.stats = {
      totalEntries: 0,
      memoryUsage: 0,
      hits: 0,
      misses: 0,
      hitRate: 0,
      avgAccessTime: 0
    };
  }
  private updateStats(): void {
    // Additional periodic stats updates could be added here
    this.updateHitRate();
  }
  destroy(): void {
    if (this.cleanupInterval) clearInterval(this.cleanupInterval);
    if (this.statsUpdateInterval) clearInterval(this.statsUpdateInterval);
    this.clear();
  }
}