@codai/memorai-core
Version:
Simplified advanced memory engine - no tiers, just powerful semantic search with persistence
273 lines (272 loc) • 8.37 kB
JavaScript
/**
* 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,
});