UNPKG

@codai/memorai-core

Version:

Simplified advanced memory engine - no tiers, just powerful semantic search with persistence

273 lines (272 loc) 8.37 kB
/** * High-Performance Memory Cache with TTL and LRU Eviction * Significantly improves MCP recall performance */ import { logger } from '../utils/logger.js'; export class HighPerformanceCache { constructor(config = {}) { this.cache = new Map(); this.accessOrder = []; // For LRU tracking this.stats = { hits: 0, misses: 0, hitRate: 0, size: 0, memoryUsage: 0, }; this.config = { maxSize: 10000, // 10k entries defaultTtl: 300, // 5 minutes enableCompression: true, enableStatistics: true, ...config, }; // Periodic cleanup setInterval(() => this.cleanup(), 60000); // Every minute } /** * Get value from cache with automatic TTL and LRU handling */ get(key) { const entry = this.cache.get(key); if (!entry) { this.recordMiss(); return null; } // Check TTL const now = Date.now(); if (now - entry.timestamp > entry.ttl * 1000) { this.cache.delete(key); this.removeFromAccessOrder(key); this.recordMiss(); return null; } // Update access tracking entry.accessCount++; entry.lastAccess = now; this.updateAccessOrder(key); this.recordHit(); return this.decompress(entry); } /** * Set value in cache with optional TTL */ set(key, value, ttl) { // Evict if at capacity if (this.cache.size >= this.config.maxSize) { this.evictLRU(); } const now = Date.now(); const entry = { value: this.compress(value), timestamp: now, ttl: ttl || this.config.defaultTtl, accessCount: 0, lastAccess: now, compressed: this.config.enableCompression, }; this.cache.set(key, entry); this.updateAccessOrder(key); this.updateStats(); } /** * Intelligent cache key generation for memory queries */ static generateMemoryQueryKey(query, tenantId, agentId, options) { const keyParts = [query, tenantId]; if (agentId) keyParts.push(agentId); if (options) keyParts.push(JSON.stringify(options)); // Use hash for consistent, shorter keys const crypto = require('crypto'); return crypto.createHash('md5').update(keyParts.join('|')).digest('hex'); } /** * Cache memory search results with smart invalidation */ cacheMemoryResults(query, tenantId, results, agentId, options) { const key = HighPerformanceCache.generateMemoryQueryKey(query, tenantId, agentId, options); // Cache with shorter TTL for dynamic results const ttl = results.length > 100 ? 60 : this.config.defaultTtl; // 1 min for large results this.set(key, results, ttl); logger.debug(`Cached ${results.length} memory results for query: ${query.substring(0, 50)}...`); } /** * Get cached memory results */ getCachedMemoryResults(query, tenantId, agentId, options) { const key = HighPerformanceCache.generateMemoryQueryKey(query, tenantId, agentId, options); const results = this.get(key); if (results) { logger.debug(`Cache hit for memory query: ${query.substring(0, 50)}...`); } return results; } /** * Invalidate cache entries for a tenant (when memories are added/removed) */ invalidateTenant(tenantId) { const keysToDelete = []; for (const [key] of this.cache.entries()) { // Check if the key contains the tenantId (simple approach) if (key.includes(tenantId)) { keysToDelete.push(key); } } keysToDelete.forEach(key => { this.cache.delete(key); this.removeFromAccessOrder(key); }); logger.debug(`Invalidated ${keysToDelete.length} cache entries for tenant: ${tenantId}`); this.updateStats(); } /** * Bulk cache operations for better performance */ setMultiple(entries) { entries.forEach(({ key, value, ttl }) => { this.set(key, value, ttl); }); } getMultiple(keys) { const results = new Map(); keys.forEach(key => { results.set(key, this.get(key)); }); return results; } /** * Get cache statistics */ getStats() { this.updateStats(); return { ...this.stats }; } /** * Clear all cache entries */ clear() { this.cache.clear(); this.accessOrder = []; this.resetStats(); } /** * Get cache size info */ getSizeInfo() { return { entries: this.cache.size, memoryUsage: this.calculateMemoryUsage(), maxSize: this.config.maxSize, }; } // Private methods compress(value) { if (!this.config.enableCompression) return value; // For now, return as-is. In production, implement actual compression // using libraries like pako or node-lz4 for large objects return value; } decompress(entry) { if (!entry.compressed) return entry.value; // For now, return as-is. In production, implement actual decompression return entry.value; } evictLRU() { if (this.accessOrder.length === 0) return; // Find least recently used entry const lruKey = this.accessOrder[0]; this.cache.delete(lruKey); this.removeFromAccessOrder(lruKey); logger.debug(`Evicted LRU cache entry: ${lruKey}`); } updateAccessOrder(key) { this.removeFromAccessOrder(key); this.accessOrder.push(key); } removeFromAccessOrder(key) { const index = this.accessOrder.indexOf(key); if (index > -1) { this.accessOrder.splice(index, 1); } } cleanup() { const now = Date.now(); const keysToDelete = []; for (const [key, entry] of this.cache.entries()) { if (now - entry.timestamp > entry.ttl * 1000) { keysToDelete.push(key); } } keysToDelete.forEach(key => { this.cache.delete(key); this.removeFromAccessOrder(key); }); if (keysToDelete.length > 0) { logger.debug(`Cleaned up ${keysToDelete.length} expired cache entries`); this.updateStats(); } } recordHit() { if (!this.config.enableStatistics) return; this.stats.hits++; this.updateHitRate(); } recordMiss() { if (!this.config.enableStatistics) return; this.stats.misses++; this.updateHitRate(); } updateHitRate() { const total = this.stats.hits + this.stats.misses; this.stats.hitRate = total > 0 ? this.stats.hits / total : 0; } updateStats() { if (!this.config.enableStatistics) return; this.stats.size = this.cache.size; this.stats.memoryUsage = this.calculateMemoryUsage(); } resetStats() { this.stats = { hits: 0, misses: 0, hitRate: 0, size: 0, memoryUsage: 0, }; } calculateMemoryUsage() { // Rough estimation of memory usage let totalSize = 0; for (const [key, entry] of this.cache.entries()) { totalSize += key.length * 2; // UTF-16 characters totalSize += JSON.stringify(entry).length * 2; // Rough estimate } return totalSize; } } /** * Global cache instance for memory operations */ export const memoryCache = new HighPerformanceCache({ maxSize: 5000, defaultTtl: 300, // 5 minutes enableCompression: true, enableStatistics: true, }); /** * Context cache for frequently accessed context data */ export const contextCache = new HighPerformanceCache({ maxSize: 1000, defaultTtl: 600, // 10 minutes enableCompression: false, // Context is usually small enableStatistics: true, });