UNPKG

@codai/memorai

Version:

Universal Database & Storage Service for CODAI Ecosystem - CBD Backend

292 lines 9.74 kB
/** * Cache Service - Production Implementation * Supports in-memory caching with optional Redis backend */ import { EventEmitter } from 'events'; export class CacheService extends EventEmitter { constructor(config) { super(); this.config = config; this.cache = new Map(); this.isInitialized = false; this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0, expired: 0 }; } async initialize() { try { // Initialize Redis connection if configured if (this.config.provider === 'redis' && this.config.url) { await this.initializeRedis(); } // Start cleanup interval for expired items this.startCleanupInterval(); this.isInitialized = true; this.emit('initialized'); console.log('💾 Cache Service initialized'); } catch (error) { console.error('Failed to initialize cache service:', error); this.emit('error', error); throw error; } } async shutdown() { if (this.isInitialized) { // Stop cleanup interval if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } // Clean up connections this.isInitialized = false; this.cache.clear(); this.emit('shutdown'); console.log('💾 Cache Service shutdown'); } } async get(key) { if (!this.isInitialized) { throw new Error('Cache service not initialized'); } try { const item = this.cache.get(key); if (!item) { this.stats.misses++; this.emit('cache:miss', { key }); return null; } // Check expiration if (item.expiresAt < new Date()) { this.cache.delete(key); this.stats.expired++; this.stats.misses++; this.emit('cache:expired', { key }); return null; } // Update hit statistics item.hits++; this.stats.hits++; this.emit('cache:hit', { key, hits: item.hits }); return item.value; } catch (error) { console.error('Cache get error:', error); this.emit('cache:error', { operation: 'get', key, error }); return null; } } async set(key, value, options) { if (!this.isInitialized) { throw new Error('Cache service not initialized'); } try { // Check cache size limit if (this.cache.size >= this.config.maxSize) { await this.evictLRU(); } const ttl = options?.ttl || this.config.ttl; const expiresAt = new Date(Date.now() + ttl * 1000); const tags = options?.tags || []; const metadata = options?.metadata; const item = { value, expiresAt, createdAt: new Date(), tags, hits: 0, metadata }; this.cache.set(key, item); this.stats.sets++; this.emit('cache:set', { key, ttl, tags }); } catch (error) { console.error('Cache set error:', error); this.emit('cache:error', { operation: 'set', key, error }); throw error; } } async delete(key) { if (!this.isInitialized) { throw new Error('Cache service not initialized'); } try { const deleted = this.cache.delete(key); if (deleted) { this.stats.deletes++; this.emit('cache:delete', { key }); } return deleted; } catch (error) { console.error('Cache delete error:', error); this.emit('cache:error', { operation: 'delete', key, error }); return false; } } async clear(pattern) { if (!this.isInitialized) { throw new Error('Cache service not initialized'); } try { if (!pattern) { const size = this.cache.size; this.cache.clear(); this.emit('cache:clear', { count: size }); return size; } // Pattern matching with support for wildcards let cleared = 0; const regex = new RegExp(pattern.replace(/\*/g, '.*')); for (const key of this.cache.keys()) { if (regex.test(key)) { this.cache.delete(key); cleared++; } } this.emit('cache:clear', { count: cleared, pattern }); return cleared; } catch (error) { console.error('Cache clear error:', error); this.emit('cache:error', { operation: 'clear', pattern, error }); return 0; } } async clearByTag(tag) { if (!this.isInitialized) { throw new Error('Cache service not initialized'); } let cleared = 0; for (const [key, item] of this.cache.entries()) { if (item.tags.includes(tag)) { this.cache.delete(key); cleared++; } } this.emit('cache:clear_by_tag', { tag, count: cleared }); return cleared; } async has(key) { if (!this.isInitialized) { throw new Error('Cache service not initialized'); } const item = this.cache.get(key); if (!item) return false; // Check expiration if (item.expiresAt < new Date()) { this.cache.delete(key); this.stats.expired++; return false; } return true; } async keys(pattern) { if (!this.isInitialized) { throw new Error('Cache service not initialized'); } const keys = Array.from(this.cache.keys()); if (!pattern) return keys; const regex = new RegExp(pattern.replace(/\*/g, '.*')); return keys.filter(key => regex.test(key)); } async getStats() { const totalRequests = this.stats.hits + this.stats.misses; return { totalKeys: this.cache.size, memoryUsage: this.calculateMemoryUsage(), hitRate: totalRequests > 0 ? this.stats.hits / totalRequests : 0, missRate: totalRequests > 0 ? this.stats.misses / totalRequests : 0, evictionCount: this.stats.evictions, expiredCount: this.stats.expired }; } async getHealth() { if (!this.isInitialized) { return { status: 'unhealthy', details: { initialized: false } }; } try { const stats = await this.getStats(); return { status: 'healthy', details: { initialized: true, provider: this.config.provider, stats, maxSize: this.config.maxSize, currentSize: this.cache.size } }; } catch (error) { return { status: 'unhealthy', details: { error: error instanceof Error ? error.message : 'Unknown error' } }; } } // ==================== PRIVATE METHODS ==================== async initializeRedis() { // TODO: Initialize Redis connection // This would use ioredis or similar library console.log('Redis cache backend not yet implemented, using in-memory cache'); } startCleanupInterval() { // Clean up expired items every minute this.cleanupInterval = setInterval(() => { this.cleanupExpired(); }, 60 * 1000); } cleanupExpired() { const now = new Date(); let expired = 0; for (const [key, item] of this.cache.entries()) { if (item.expiresAt < now) { this.cache.delete(key); expired++; } } if (expired > 0) { this.stats.expired += expired; this.emit('cache:cleanup', { expired }); } } async evictLRU() { // Find least recently used item (lowest hits, oldest creation) let lruKey = null; let lruScore = Infinity; for (const [key, item] of this.cache.entries()) { // Score based on hits and age (lower is better for eviction) const age = Date.now() - item.createdAt.getTime(); const score = item.hits - (age / 1000 / 60 / 60); // Hits minus hours of age if (score < lruScore) { lruScore = score; lruKey = key; } } if (lruKey) { this.cache.delete(lruKey); this.stats.evictions++; this.emit('cache:evict', { key: lruKey, score: lruScore }); } } calculateMemoryUsage() { // Rough estimate of memory usage let size = 0; for (const [key, item] of this.cache.entries()) { size += key.length * 2; // String is 2 bytes per character size += JSON.stringify(item).length * 2; // Rough estimate of object size } return size; } } //# sourceMappingURL=CacheService.js.map