context-engine-mcp
Version:
Context engine MCP server for comprehensive project analysis and multi-file editing
267 lines • 8.21 kB
JavaScript
import { configManager } from '../config/index.js';
import logger, { logMemoryUsage } from '../utils/logger.js';
import { ContextEngineError, ErrorCodes } from '../utils/errors.js';
export class CacheManager {
cache = new Map();
accessOrder = new Map(); // LRU tracking
cleanupInterval = null;
accessCounter = 0;
constructor() {
this.startCleanupTimer();
}
/**
* Set a value in the cache with optional expiration
*/
set(key, value, ttlMs) {
try {
const now = Date.now();
const entry = {
data: value,
timestamp: now
};
// Add expiration if TTL provided
if (ttlMs && ttlMs > 0) {
entry.expiresAt = now + ttlMs;
}
// Remove oldest entries if cache is full
this.enforceMaxSize();
this.cache.set(key, entry);
this.accessOrder.set(key, ++this.accessCounter);
logger.debug(`Cache set: ${key}`, { size: this.cache.size });
}
catch (error) {
throw new ContextEngineError(ErrorCodes.CACHE_ERROR, `Failed to set cache entry: ${key}`, { key, error: error instanceof Error ? error.message : String(error) });
}
}
/**
* Get a value from the cache
*/
get(key) {
try {
const entry = this.cache.get(key);
if (!entry) {
logger.debug(`Cache miss: ${key}`);
return undefined;
}
// Check if entry has expired
if (entry.expiresAt && Date.now() > entry.expiresAt) {
this.delete(key);
logger.debug(`Cache expired: ${key}`);
return undefined;
}
// Update access order for LRU
this.accessOrder.set(key, ++this.accessCounter);
logger.debug(`Cache hit: ${key}`);
return entry.data;
}
catch (error) {
logger.error(`Error getting cache entry: ${key}`, { error });
return undefined;
}
}
/**
* Check if a key exists in cache and is not expired
*/
has(key) {
const entry = this.cache.get(key);
if (!entry)
return false;
// Check expiration
if (entry.expiresAt && Date.now() > entry.expiresAt) {
this.delete(key);
return false;
}
return true;
}
/**
* Delete a cache entry
*/
delete(key) {
const deleted = this.cache.delete(key);
this.accessOrder.delete(key);
if (deleted) {
logger.debug(`Cache deleted: ${key}`, { size: this.cache.size });
}
return deleted;
}
/**
* Get cache entry with metadata
*/
getWithMetadata(key) {
const entry = this.cache.get(key);
if (!entry)
return undefined;
// Check expiration
if (entry.expiresAt && Date.now() > entry.expiresAt) {
this.delete(key);
return undefined;
}
return { ...entry, key };
}
/**
* Get all cache keys
*/
keys() {
return Array.from(this.cache.keys());
}
/**
* Get cache size
*/
size() {
return this.cache.size;
}
/**
* Clear all cache entries
*/
clear() {
const size = this.cache.size;
this.cache.clear();
this.accessOrder.clear();
this.accessCounter = 0;
logger.info(`Cache cleared`, { previousSize: size });
}
/**
* Get cache statistics
*/
getStats() {
const entries = Array.from(this.cache.entries());
let oldestEntry;
let newestEntry;
let oldestTime = Infinity;
let newestTime = 0;
for (const [key, entry] of entries) {
if (entry.timestamp < oldestTime) {
oldestTime = entry.timestamp;
oldestEntry = key;
}
if (entry.timestamp > newestTime) {
newestTime = entry.timestamp;
newestEntry = key;
}
}
// Rough memory usage estimation
const memoryUsage = this.estimateMemoryUsage();
const result = {
size: this.cache.size,
maxSize: configManager.get('maxCacheSize'),
memoryUsage: `${Math.round(memoryUsage / 1024)}KB`
};
if (oldestEntry !== undefined) {
result.oldestEntry = oldestEntry;
}
if (newestEntry !== undefined) {
result.newestEntry = newestEntry;
}
return result;
}
/**
* Cleanup expired entries
*/
cleanup() {
const now = Date.now();
let removed = 0;
for (const [key, entry] of this.cache.entries()) {
if (entry.expiresAt && now > entry.expiresAt) {
this.delete(key);
removed++;
}
}
if (removed > 0) {
logger.info(`Cache cleanup completed`, { removedEntries: removed, remainingSize: this.cache.size });
logMemoryUsage('cache cleanup');
}
return removed;
}
/**
* Start automatic cleanup timer
*/
startCleanupTimer() {
const interval = configManager.get('cacheCleanupInterval');
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, interval);
logger.debug(`Cache cleanup timer started`, { intervalMs: interval });
}
/**
* Stop automatic cleanup timer
*/
stopCleanupTimer() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
logger.debug('Cache cleanup timer stopped');
}
}
/**
* Enforce maximum cache size using LRU eviction
*/
enforceMaxSize() {
const maxSize = configManager.get('maxCacheSize');
if (this.cache.size >= maxSize) {
// Sort by access order and remove least recently used
const sortedByAccess = Array.from(this.accessOrder.entries())
.sort(([, a], [, b]) => a - b);
const toRemove = Math.floor(maxSize * 0.1) || 1; // Remove 10% or at least 1
for (let i = 0; i < toRemove && i < sortedByAccess.length; i++) {
const [key] = sortedByAccess[i];
this.delete(key);
}
logger.info(`Cache size enforced`, {
removedEntries: toRemove,
currentSize: this.cache.size,
maxSize
});
}
}
/**
* Estimate memory usage (rough calculation)
*/
estimateMemoryUsage() {
let totalSize = 0;
for (const [key, entry] of this.cache.entries()) {
// Rough estimation: key size + JSON string size of data
totalSize += key.length * 2; // UTF-16 characters
try {
const dataStr = JSON.stringify(entry.data);
totalSize += dataStr.length * 2;
}
catch {
// If data can't be serialized, use a rough estimate
totalSize += 1000; // 1KB estimate
}
totalSize += 64; // Rough overhead for entry metadata
}
return totalSize;
}
/**
* Destroy the cache manager
*/
destroy() {
this.stopCleanupTimer();
this.clear();
logger.info('Cache manager destroyed');
}
}
// Global cache instances for different data types
export const fileCache = new CacheManager();
export const projectCache = new CacheManager();
export const analysisCache = new CacheManager();
// Cleanup on process exit
process.on('exit', () => {
fileCache.destroy();
projectCache.destroy();
analysisCache.destroy();
});
process.on('SIGINT', () => {
fileCache.destroy();
projectCache.destroy();
analysisCache.destroy();
process.exit(0);
});
process.on('SIGTERM', () => {
fileCache.destroy();
projectCache.destroy();
analysisCache.destroy();
process.exit(0);
});
//# sourceMappingURL=cache-manager.js.map