UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

517 lines (438 loc) 11.4 kB
/** * V3 Cache Manager * * High-performance LRU cache with TTL support, memory pressure handling, * and write-through caching for the unified memory system. * * @module v3/memory/cache-manager */ import { EventEmitter } from 'node:events'; import { CacheConfig, CacheStats, CachedEntry, MemoryEntry, MemoryEvent, } from './types.js'; /** * Doubly-linked list node for LRU implementation */ interface LRUNode<T> { key: string; value: CachedEntry<T>; prev: LRUNode<T> | null; next: LRUNode<T> | null; } /** * High-performance LRU Cache with TTL support * * Features: * - O(1) get, set, delete operations * - LRU eviction policy * - TTL-based expiration * - Memory pressure handling * - Write-through caching support * - Performance statistics */ export class CacheManager<T = MemoryEntry> extends EventEmitter { private config: CacheConfig; private cache: Map<string, LRUNode<T>> = new Map(); private head: LRUNode<T> | null = null; private tail: LRUNode<T> | null = null; private currentMemory: number = 0; // Statistics private stats: { hits: number; misses: number; evictions: number; expirations: number; writes: number; } = { hits: 0, misses: 0, evictions: 0, expirations: 0, writes: 0, }; // Cleanup timer private cleanupInterval: NodeJS.Timeout | null = null; constructor(config: Partial<CacheConfig> = {}) { super(); this.config = this.mergeConfig(config); this.startCleanupTimer(); } /** * Get a value from the cache */ get(key: string): T | null { const node = this.cache.get(key); if (!node) { this.stats.misses++; this.emit('cache:miss', { key }); return null; } // Check if expired if (this.isExpired(node.value)) { this.delete(key); this.stats.misses++; this.stats.expirations++; this.emit('cache:expired', { key }); return null; } // Update access time and count node.value.lastAccessedAt = Date.now(); node.value.accessCount++; // Move to front (most recently used) this.moveToFront(node); this.stats.hits++; this.emit('cache:hit', { key }); return node.value.data; } /** * Set a value in the cache */ set(key: string, data: T, ttl?: number): void { const now = Date.now(); const entryTtl = ttl || this.config.ttl; // Check if key already exists const existingNode = this.cache.get(key); if (existingNode) { // Update existing entry existingNode.value.data = data; existingNode.value.cachedAt = now; existingNode.value.expiresAt = now + entryTtl; existingNode.value.lastAccessedAt = now; this.moveToFront(existingNode); this.stats.writes++; return; } // Calculate memory for new entry const entryMemory = this.estimateSize(data); // Evict entries if needed for memory pressure if (this.config.maxMemory) { while ( this.currentMemory + entryMemory > this.config.maxMemory && this.cache.size > 0 ) { this.evictLRU(); } } // Evict entries if at capacity while (this.cache.size >= this.config.maxSize) { this.evictLRU(); } // Create new node const cachedEntry: CachedEntry<T> = { data, cachedAt: now, expiresAt: now + entryTtl, lastAccessedAt: now, accessCount: 0, }; const node: LRUNode<T> = { key, value: cachedEntry, prev: null, next: null, }; // Add to cache this.cache.set(key, node); this.addToFront(node); this.currentMemory += entryMemory; this.stats.writes++; this.emit('cache:set', { key, ttl: entryTtl }); } /** * Delete a value from the cache */ delete(key: string): boolean { const node = this.cache.get(key); if (!node) { return false; } this.removeNode(node); this.cache.delete(key); this.currentMemory -= this.estimateSize(node.value.data); this.emit('cache:delete', { key }); return true; } /** * Check if a key exists in the cache (without affecting LRU order) */ has(key: string): boolean { const node = this.cache.get(key); if (!node) return false; if (this.isExpired(node.value)) { this.delete(key); return false; } return true; } /** * Clear all entries from the cache */ clear(): void { this.cache.clear(); this.head = null; this.tail = null; this.currentMemory = 0; this.emit('cache:cleared', { previousSize: this.cache.size }); } /** * Get cache statistics */ getStats(): CacheStats { const total = this.stats.hits + this.stats.misses; return { size: this.cache.size, hitRate: total > 0 ? this.stats.hits / total : 0, hits: this.stats.hits, misses: this.stats.misses, evictions: this.stats.evictions, memoryUsage: this.currentMemory, }; } /** * Get all keys in the cache */ keys(): string[] { return Array.from(this.cache.keys()); } /** * Get the size of the cache */ get size(): number { return this.cache.size; } /** * Prefetch multiple keys in a single batch */ async prefetch( keys: string[], loader: (keys: string[]) => Promise<Map<string, T>>, ttl?: number ): Promise<void> { const missing = keys.filter((key) => !this.has(key)); if (missing.length === 0) { return; } const data = await loader(missing); for (const [key, value] of data) { this.set(key, value, ttl); } this.emit('cache:prefetched', { keys: missing.length }); } /** * Get or set pattern - get from cache or load and cache */ async getOrSet( key: string, loader: () => Promise<T>, ttl?: number ): Promise<T> { const cached = this.get(key); if (cached !== null) { return cached; } const data = await loader(); this.set(key, data, ttl); return data; } /** * Warm the cache with initial data */ warmUp(entries: Array<{ key: string; data: T; ttl?: number }>): void { for (const entry of entries) { this.set(entry.key, entry.data, entry.ttl); } this.emit('cache:warmedUp', { count: entries.length }); } /** * Invalidate entries matching a pattern */ invalidatePattern(pattern: string | RegExp): number { const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern; let invalidated = 0; for (const key of this.cache.keys()) { if (regex.test(key)) { this.delete(key); invalidated++; } } this.emit('cache:invalidated', { pattern: pattern.toString(), count: invalidated }); return invalidated; } /** * Shutdown the cache manager */ shutdown(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.clear(); this.emit('cache:shutdown'); } // ===== Private Methods ===== private mergeConfig(config: Partial<CacheConfig>): CacheConfig { return { maxSize: config.maxSize || 10000, ttl: config.ttl || 300000, // 5 minutes default lruEnabled: config.lruEnabled !== false, maxMemory: config.maxMemory, writeThrough: config.writeThrough || false, }; } private isExpired(entry: CachedEntry<T>): boolean { return Date.now() > entry.expiresAt; } private estimateSize(data: T): number { try { return JSON.stringify(data).length * 2; // Rough UTF-16 estimate } catch { return 1000; // Default for non-serializable objects } } private addToFront(node: LRUNode<T>): void { node.prev = null; node.next = this.head; if (this.head) { this.head.prev = node; } this.head = node; if (!this.tail) { this.tail = node; } } private removeNode(node: LRUNode<T>): void { if (node.prev) { node.prev.next = node.next; } else { this.head = node.next; } if (node.next) { node.next.prev = node.prev; } else { this.tail = node.prev; } } private moveToFront(node: LRUNode<T>): void { if (node === this.head) return; this.removeNode(node); this.addToFront(node); } private evictLRU(): void { if (!this.tail) return; const evictedKey = this.tail.key; const evictedSize = this.estimateSize(this.tail.value.data); this.removeNode(this.tail); this.cache.delete(evictedKey); this.currentMemory -= evictedSize; this.stats.evictions++; this.emit('cache:eviction', { key: evictedKey }); } private startCleanupTimer(): void { // Clean up expired entries every minute this.cleanupInterval = setInterval(() => { this.cleanupExpired(); }, 60000); } private cleanupExpired(): void { const now = Date.now(); let cleaned = 0; for (const [key, node] of this.cache) { if (node.value.expiresAt < now) { this.delete(key); cleaned++; } } if (cleaned > 0) { this.emit('cache:cleanup', { expired: cleaned }); } } } /** * Multi-layer cache with L1 (memory) and L2 (storage) tiers */ export class TieredCacheManager<T = MemoryEntry> extends EventEmitter { private l1Cache: CacheManager<T>; private l2Loader: ((key: string) => Promise<T | null>) | null = null; private l2Writer: ((key: string, value: T) => Promise<void>) | null = null; constructor( l1Config: Partial<CacheConfig> = {}, l2Options?: { loader: (key: string) => Promise<T | null>; writer?: (key: string, value: T) => Promise<void>; } ) { super(); this.l1Cache = new CacheManager<T>(l1Config); if (l2Options) { this.l2Loader = l2Options.loader; this.l2Writer = l2Options.writer ?? null; } // Forward L1 events this.l1Cache.on('cache:hit', (data) => this.emit('l1:hit', data)); this.l1Cache.on('cache:miss', (data) => this.emit('l1:miss', data)); this.l1Cache.on('cache:eviction', (data) => this.emit('l1:eviction', data)); } /** * Get from tiered cache */ async get(key: string): Promise<T | null> { // Try L1 first const l1Result = this.l1Cache.get(key); if (l1Result !== null) { return l1Result; } // Try L2 if available if (this.l2Loader) { const l2Result = await this.l2Loader(key); if (l2Result !== null) { // Promote to L1 this.l1Cache.set(key, l2Result); this.emit('l2:hit', { key }); return l2Result; } this.emit('l2:miss', { key }); } return null; } /** * Set in tiered cache */ async set(key: string, value: T, ttl?: number): Promise<void> { // Write to L1 this.l1Cache.set(key, value, ttl); // Write-through to L2 if configured if (this.l2Writer) { await this.l2Writer(key, value); this.emit('l2:write', { key }); } } /** * Delete from tiered cache */ delete(key: string): boolean { return this.l1Cache.delete(key); } /** * Get L1 cache statistics */ getStats(): CacheStats { return this.l1Cache.getStats(); } /** * Clear L1 cache */ clear(): void { this.l1Cache.clear(); } /** * Shutdown tiered cache */ shutdown(): void { this.l1Cache.shutdown(); } } export default CacheManager;