tasq/node_modules/@claude-flow/memory/dist/cache-manager.js

407 lines
11 KiB
JavaScript

/**
* 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';
/**
* 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 extends EventEmitter {
config;
cache = new Map();
head = null;
tail = null;
currentMemory = 0;
// Statistics
stats = {
hits: 0,
misses: 0,
evictions: 0,
expirations: 0,
writes: 0,
};
// Cleanup timer
cleanupInterval = null;
constructor(config = {}) {
super();
this.config = this.mergeConfig(config);
this.startCleanupTimer();
}
/**
* Get a value from the cache
*/
get(key) {
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, data, ttl) {
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 = {
data,
cachedAt: now,
expiresAt: now + entryTtl,
lastAccessedAt: now,
accessCount: 0,
};
const node = {
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) {
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) {
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() {
this.cache.clear();
this.head = null;
this.tail = null;
this.currentMemory = 0;
this.emit('cache:cleared', { previousSize: this.cache.size });
}
/**
* Get cache statistics
*/
getStats() {
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() {
return Array.from(this.cache.keys());
}
/**
* Get the size of the cache
*/
get size() {
return this.cache.size;
}
/**
* Prefetch multiple keys in a single batch
*/
async prefetch(keys, loader, ttl) {
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, loader, ttl) {
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) {
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) {
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() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.clear();
this.emit('cache:shutdown');
}
// ===== Private Methods =====
mergeConfig(config) {
return {
maxSize: config.maxSize || 10000,
ttl: config.ttl || 300000, // 5 minutes default
lruEnabled: config.lruEnabled !== false,
maxMemory: config.maxMemory,
writeThrough: config.writeThrough || false,
};
}
isExpired(entry) {
return Date.now() > entry.expiresAt;
}
estimateSize(data) {
try {
return JSON.stringify(data).length * 2; // Rough UTF-16 estimate
}
catch {
return 1000; // Default for non-serializable objects
}
}
addToFront(node) {
node.prev = null;
node.next = this.head;
if (this.head) {
this.head.prev = node;
}
this.head = node;
if (!this.tail) {
this.tail = node;
}
}
removeNode(node) {
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;
}
}
moveToFront(node) {
if (node === this.head)
return;
this.removeNode(node);
this.addToFront(node);
}
evictLRU() {
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 });
}
startCleanupTimer() {
// Clean up expired entries every minute
this.cleanupInterval = setInterval(() => {
this.cleanupExpired();
}, 60000);
}
cleanupExpired() {
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 extends EventEmitter {
l1Cache;
l2Loader = null;
l2Writer = null;
constructor(l1Config = {}, l2Options) {
super();
this.l1Cache = new CacheManager(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) {
// 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, value, ttl) {
// 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) {
return this.l1Cache.delete(key);
}
/**
* Get L1 cache statistics
*/
getStats() {
return this.l1Cache.getStats();
}
/**
* Clear L1 cache
*/
clear() {
this.l1Cache.clear();
}
/**
* Shutdown tiered cache
*/
shutdown() {
this.l1Cache.shutdown();
}
}
export default CacheManager;
//# sourceMappingURL=cache-manager.js.map