import { getLogger } from '@stock-bot/logger'; import { CacheProvider, CacheOptions, CacheStats } from '../types'; interface CacheEntry { value: T; expiry: number; accessed: number; } /** * In-memory cache provider with LRU eviction and comprehensive metrics */ export class MemoryCache implements CacheProvider { private store = new Map>(); private logger = getLogger('memory-cache'); private defaultTTL: number; private keyPrefix: string; private maxItems: number; private enableMetrics: boolean; private startTime = Date.now(); private stats: CacheStats = { hits: 0, misses: 0, errors: 0, hitRate: 0, total: 0, uptime: 0 }; constructor(options: CacheOptions = {}) { this.defaultTTL = options.ttl ?? 3600; // 1 hour default this.keyPrefix = options.keyPrefix ?? 'cache:'; this.maxItems = options.maxMemoryItems ?? 1000; this.enableMetrics = options.enableMetrics ?? true; this.logger.info('Memory cache initialized', { maxItems: this.maxItems, defaultTTL: this.defaultTTL, enableMetrics: this.enableMetrics }); // Cleanup expired entries every 5 minutes setInterval(() => this.cleanup(), 5 * 60 * 1000); } private getKey(key: string): string { return `${this.keyPrefix}${key}`; } private updateStats(hit: boolean, error = false): void { if (!this.enableMetrics) return; if (error) { this.stats.errors++; } else if (hit) { this.stats.hits++; } else { this.stats.misses++; } this.stats.total = this.stats.hits + this.stats.misses; this.stats.hitRate = this.stats.total > 0 ? this.stats.hits / this.stats.total : 0; this.stats.uptime = Date.now() - this.startTime; } private cleanup(): void { const now = Date.now(); let cleaned = 0; for (const [key, entry] of this.store.entries()) { if (entry.expiry < now) { this.store.delete(key); cleaned++; } } if (cleaned > 0) { this.logger.debug('Cleaned expired entries', { cleaned, remaining: this.store.size }); } } private evictLRU(): void { if (this.store.size <= this.maxItems) return; // Find least recently accessed item let oldestKey = ''; let oldestAccess = Date.now(); for (const [key, entry] of this.store.entries()) { if (entry.accessed < oldestAccess) { oldestAccess = entry.accessed; oldestKey = key; } } if (oldestKey) { this.store.delete(oldestKey); this.logger.debug('Evicted LRU entry', { key: oldestKey }); } } async get(key: string): Promise { try { const fullKey = this.getKey(key); const entry = this.store.get(fullKey); if (!entry) { this.updateStats(false); this.logger.debug('Cache miss', { key }); return null; } const now = Date.now(); if (entry.expiry < now) { this.store.delete(fullKey); this.updateStats(false); this.logger.debug('Cache miss (expired)', { key }); return null; } // Update access time for LRU entry.accessed = now; this.updateStats(true); this.logger.debug('Cache hit', { key, hitRate: this.stats.hitRate }); return entry.value; } catch (error) { this.updateStats(false, true); this.logger.error('Cache get error', { key, error: error instanceof Error ? error.message : String(error) }); return null; } } async set(key: string, value: T, ttl?: number): Promise { try { const fullKey = this.getKey(key); const now = Date.now(); const expiry = now + 1000 * (ttl ?? this.defaultTTL); // Evict if necessary this.evictLRU(); this.store.set(fullKey, { value, expiry, accessed: now }); this.logger.debug('Cache set', { key, ttl: ttl ?? this.defaultTTL }); } catch (error) { this.updateStats(false, true); this.logger.error('Cache set error', { key, error: error instanceof Error ? error.message : String(error) }); } } async del(key: string): Promise { try { const fullKey = this.getKey(key); const deleted = this.store.delete(fullKey); this.logger.debug('Cache delete', { key, deleted }); } catch (error) { this.updateStats(false, true); this.logger.error('Cache delete error', { key, error: error instanceof Error ? error.message : String(error) }); } } async exists(key: string): Promise { try { const fullKey = this.getKey(key); const entry = this.store.get(fullKey); if (!entry) return false; // Check if expired if (entry.expiry < Date.now()) { this.store.delete(fullKey); return false; } return true; } catch (error) { this.updateStats(false, true); this.logger.error('Cache exists error', { key, error: error instanceof Error ? error.message : String(error) }); return false; } } async clear(): Promise { try { const size = this.store.size; this.store.clear(); this.logger.info('Cache cleared', { entriesDeleted: size }); } catch (error) { this.updateStats(false, true); this.logger.error('Cache clear error', { error: error instanceof Error ? error.message : String(error) }); } } async health(): Promise { try { // Simple health check - try to set and get a test value await this.set('__health_check__', 'ok', 1); const result = await this.get('__health_check__'); await this.del('__health_check__'); return result === 'ok'; } catch (error) { this.logger.error('Memory cache health check failed', error); return false; } } getStats(): CacheStats { return { ...this.stats, uptime: Date.now() - this.startTime }; } /** * Get additional memory cache specific stats */ getMemoryStats() { return { ...this.getStats(), entries: this.store.size, maxItems: this.maxItems, memoryUsage: this.estimateMemoryUsage() }; } private estimateMemoryUsage(): number { // Rough estimation of memory usage in bytes let bytes = 0; for (const [key, entry] of this.store.entries()) { bytes += key.length * 2; // UTF-16 characters bytes += JSON.stringify(entry.value).length * 2; bytes += 24; // Overhead for entry object } return bytes; } async waitForReady(timeout: number = 5000): Promise { // Memory cache is always ready immediately return Promise.resolve(); } isReady(): boolean { // Memory cache is always ready return true; } private getMemoryUsage(): number { // Rough estimation of memory usage in bytes let bytes = 0; for (const [key, entry] of this.store.entries()) { bytes += key.length * 2; // UTF-16 characters bytes += JSON.stringify(entry.value).length * 2; bytes += 24; // Overhead for entry object } return bytes; } }