claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
297 lines (296 loc) • 9.31 kB
JavaScript
/**
* LRU Skill Cache
*
* Memory-aware LRU (Least Recently Used) cache for skill content.
* Tracks memory usage per skill and evicts LRU entries when budget exceeded.
*
* Features:
* - Memory budget enforcement (bytes, not just entry count)
* - LRU eviction policy
* - Cache statistics (hits, misses, evictions)
* - Thread-safe operations
* - TTL support (optional)
*
* @module skill-cache
*/ import { createLogger } from './logging.js';
import { StandardError } from './errors.js';
/**
* LRU Skill Cache with memory budget enforcement
*/ export class LRUSkillCache {
cache;
maxMemoryBytes;
maxEntries;
defaultTTLMs;
currentMemoryBytes = 0;
logger;
debug;
// Statistics
stats = {
hits: 0,
misses: 0,
evictions: 0
};
constructor(config){
this.cache = new Map();
this.maxMemoryBytes = config.maxMemoryBytes;
this.maxEntries = config.maxEntries ?? Number.MAX_SAFE_INTEGER;
this.defaultTTLMs = config.defaultTTLMs;
this.logger = config.logger ?? createLogger('lru-skill-cache');
this.debug = config.debug ?? false;
if (this.debug) {
this.logger.info('LRU cache initialized', {
maxMemoryBytes: this.maxMemoryBytes,
maxMemoryMB: (this.maxMemoryBytes / 1024 / 1024).toFixed(2),
maxEntries: this.maxEntries,
defaultTTLMs: this.defaultTTLMs
});
}
}
/**
* Get value from cache
*
* Updates last access time (LRU tracking).
* Returns undefined if not found or expired.
*/ get(key) {
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
return undefined;
}
// Check expiry
if (entry.expiresAt && entry.expiresAt < new Date()) {
if (this.debug) {
this.logger.debug('Cache entry expired', {
key
});
}
this.delete(key);
this.stats.misses++;
return undefined;
}
// Update last accessed (LRU tracking)
entry.lastAccessed = new Date();
this.stats.hits++;
if (this.debug) {
this.logger.debug('Cache hit', {
key,
sizeBytes: entry.sizeBytes,
age: Date.now() - entry.createdAt.getTime()
});
}
return entry.value;
}
/**
* Set value in cache
*
* Evicts LRU entries if memory budget would be exceeded.
*
* @param key - Cache key
* @param value - Value to cache
* @param sizeBytes - Size of value in bytes
* @param ttlMs - TTL in milliseconds (optional, overrides default)
*/ set(key, value, sizeBytes, ttlMs) {
// Check if entry already exists
const existing = this.cache.get(key);
if (existing) {
// Update existing entry
this.currentMemoryBytes -= existing.sizeBytes;
this.currentMemoryBytes += sizeBytes;
existing.value = value;
existing.sizeBytes = sizeBytes;
existing.lastAccessed = new Date();
existing.createdAt = new Date();
if (ttlMs !== undefined || this.defaultTTLMs !== undefined) {
const ttl = ttlMs ?? this.defaultTTLMs;
existing.expiresAt = new Date(Date.now() + ttl);
}
if (this.debug) {
this.logger.debug('Cache entry updated', {
key,
sizeBytes,
memoryUsageBytes: this.currentMemoryBytes
});
}
return;
}
// Evict entries if needed
this.evictIfNeeded(sizeBytes);
// Create new entry
const entry = {
key,
value,
sizeBytes,
lastAccessed: new Date(),
createdAt: new Date()
};
if (ttlMs !== undefined || this.defaultTTLMs !== undefined) {
const ttl = ttlMs ?? this.defaultTTLMs;
entry.expiresAt = new Date(Date.now() + ttl);
}
this.cache.set(key, entry);
this.currentMemoryBytes += sizeBytes;
if (this.debug) {
this.logger.debug('Cache entry added', {
key,
sizeBytes,
memoryUsageBytes: this.currentMemoryBytes,
memoryUtilization: (this.currentMemoryBytes / this.maxMemoryBytes).toFixed(2)
});
}
}
/**
* Delete entry from cache
*/ delete(key) {
const entry = this.cache.get(key);
if (!entry) {
return false;
}
this.cache.delete(key);
this.currentMemoryBytes -= entry.sizeBytes;
if (this.debug) {
this.logger.debug('Cache entry deleted', {
key,
sizeBytes: entry.sizeBytes,
memoryUsageBytes: this.currentMemoryBytes
});
}
return true;
}
/**
* Check if key exists in cache
*/ has(key) {
const entry = this.cache.get(key);
if (!entry) {
return false;
}
// Check expiry
if (entry.expiresAt && entry.expiresAt < new Date()) {
this.delete(key);
return false;
}
return true;
}
/**
* Clear all entries
*/ clear() {
this.cache.clear();
this.currentMemoryBytes = 0;
if (this.debug) {
this.logger.debug('Cache cleared');
}
}
/**
* Get cache size (number of entries)
*/ get size() {
return this.cache.size;
}
/**
* Get current memory usage (bytes)
*/ get memoryUsageBytes() {
return this.currentMemoryBytes;
}
/**
* Get cache statistics
*/ getStatistics() {
const totalOps = this.stats.hits + this.stats.misses;
const hitRate = totalOps > 0 ? this.stats.hits / totalOps : 0;
const evictionRate = totalOps > 0 ? this.stats.evictions / totalOps : 0;
const memoryUtilization = this.currentMemoryBytes / this.maxMemoryBytes;
return {
size: this.cache.size,
maxSize: this.maxEntries,
memoryUsageBytes: this.currentMemoryBytes,
maxMemoryBytes: this.maxMemoryBytes,
hits: this.stats.hits,
misses: this.stats.misses,
evictions: this.stats.evictions,
hitRate,
evictionRate,
memoryUtilization
};
}
/**
* Reset statistics
*/ resetStatistics() {
this.stats = {
hits: 0,
misses: 0,
evictions: 0
};
}
/**
* Get all cache keys
*/ keys() {
return Array.from(this.cache.keys());
}
/**
* Evict entries if needed to fit new entry
*
* Uses LRU (Least Recently Used) eviction policy.
*
* @param newEntrySizeBytes - Size of new entry to add
*/ evictIfNeeded(newEntrySizeBytes) {
// Check entry count limit
while(this.cache.size >= this.maxEntries){
this.evictLRU();
}
// Check memory budget
while(this.currentMemoryBytes + newEntrySizeBytes > this.maxMemoryBytes && this.cache.size > 0){
this.evictLRU();
}
// Final check: if single entry exceeds budget, throw error
if (newEntrySizeBytes > this.maxMemoryBytes) {
throw new StandardError('CACHE_ENTRY_TOO_LARGE', `Entry size (${newEntrySizeBytes} bytes) exceeds maximum memory budget (${this.maxMemoryBytes} bytes)`, {
newEntrySizeBytes,
maxMemoryBytes: this.maxMemoryBytes
});
}
}
/**
* Evict least recently used entry
*/ evictLRU() {
let oldestKey;
let oldestTime;
// Find LRU entry
for (const [key, entry] of this.cache.entries()){
if (!oldestTime || entry.lastAccessed < oldestTime) {
oldestKey = key;
oldestTime = entry.lastAccessed;
}
}
if (oldestKey) {
const entry = this.cache.get(oldestKey);
this.delete(oldestKey);
this.stats.evictions++;
if (this.debug) {
this.logger.debug('Evicted LRU entry', {
key: oldestKey,
sizeBytes: entry.sizeBytes,
age: Date.now() - entry.createdAt.getTime(),
lastAccessed: entry.lastAccessed.toISOString()
});
}
}
}
/**
* Remove expired entries
*
* @returns Number of expired entries removed
*/ cleanupExpired() {
const now = new Date();
let removed = 0;
for (const [key, entry] of this.cache.entries()){
if (entry.expiresAt && entry.expiresAt < now) {
this.delete(key);
removed++;
}
}
if (removed > 0 && this.debug) {
this.logger.debug('Cleaned up expired entries', {
count: removed
});
}
return removed;
}
}
//# sourceMappingURL=skill-cache.js.map