UNPKG

@codai/memorai

Version:

Universal Database & Storage Service for CODAI Ecosystem - CBD Backend

363 lines (293 loc) 9.23 kB
/** * Cache Service - Production Implementation * Supports in-memory caching with optional Redis backend */ import { EventEmitter } from 'events' import { createHash } from 'crypto' import type { CacheOptions, CacheStats, MemoraiConfig } from '../types' interface CacheItem<T = any> { value: T expiresAt: Date createdAt: Date tags: string[] hits: number metadata?: Record<string, any> } export class CacheService extends EventEmitter { private cache = new Map<string, CacheItem>() private isInitialized = false private cleanupInterval?: NodeJS.Timeout private stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0, expired: 0 } constructor(private config: MemoraiConfig['cache']) { super() } async initialize(): Promise<void> { 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(): Promise<void> { 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<T = any>(key: string): Promise<T | null> { 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 as T } catch (error) { console.error('Cache get error:', error) this.emit('cache:error', { operation: 'get', key, error }) return null } } async set<T = any>(key: string, value: T, options?: CacheOptions): Promise<void> { 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: CacheItem<T> = { 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: string): Promise<boolean> { 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?: string): Promise<number> { 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: string): Promise<number> { 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: string): Promise<boolean> { 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?: string): Promise<string[]> { 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(): Promise<CacheStats> { 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(): Promise<{ status: string; details?: any }> { 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 ==================== private async initializeRedis(): Promise<void> { // TODO: Initialize Redis connection // This would use ioredis or similar library console.log('Redis cache backend not yet implemented, using in-memory cache') } private startCleanupInterval(): void { // Clean up expired items every minute this.cleanupInterval = setInterval(() => { this.cleanupExpired() }, 60 * 1000) } private cleanupExpired(): void { 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 }) } } private async evictLRU(): Promise<void> { // Find least recently used item (lowest hits, oldest creation) let lruKey: string | null = 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 }) } } private calculateMemoryUsage(): number { // 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 } }