@noanswer/context-compose
Version:
Orchestrate complex AI interactions with Context Compose. A powerful CLI and server for building, validating, and managing context for large language models using the Model Context Protocol (MCP).
265 lines • 7.27 kB
JavaScript
import { createHash } from 'node:crypto';
import { readFile, stat } from 'node:fs/promises';
/**
* High-performance in-memory cache with TTL and file change detection
*/
export class MemoryCache {
cache = new Map();
accessOrder = new Map();
accessCounter = 0;
config;
constructor(config = {}) {
this.config = {
maxSize: config.maxSize ?? 1000,
defaultTtl: config.defaultTtl ?? 5 * 60 * 1000, // 5 minutes
};
}
/**
* Get cached value with file change detection
*/
async get(key, filePath) {
const entry = this.cache.get(key);
if (!entry)
return undefined;
// Check TTL
if (entry.ttl && Date.now() > entry.timestamp + entry.ttl) {
this.cache.delete(key);
this.accessOrder.delete(key);
return undefined;
}
// Check file changes if filePath provided
if (filePath) {
try {
const currentHash = await this.getFileHash(filePath);
if (currentHash !== entry.hash) {
this.cache.delete(key);
this.accessOrder.delete(key);
return undefined;
}
}
catch {
// File doesn't exist or can't be read, invalidate cache
this.cache.delete(key);
this.accessOrder.delete(key);
return undefined;
}
}
// Update access order for LRU
this.accessOrder.set(key, ++this.accessCounter);
return entry.data;
}
/**
* Set cached value
*/
async set(key, value, filePath, ttl) {
// Evict if cache is full
if (this.cache.size >= this.config.maxSize) {
this.evictLRU();
}
const hash = filePath ? await this.getFileHash(filePath) : '';
const entry = {
data: value,
timestamp: Date.now(),
hash,
ttl: ttl ?? this.config.defaultTtl,
};
this.cache.set(key, entry);
this.accessOrder.set(key, ++this.accessCounter);
}
/**
* Check if key exists in cache
*/
has(key) {
return this.cache.has(key);
}
/**
* Delete cached value
*/
delete(key) {
this.accessOrder.delete(key);
return this.cache.delete(key);
}
/**
* Clear all cached values
*/
clear() {
this.cache.clear();
this.accessOrder.clear();
this.accessCounter = 0;
}
/**
* Get cache statistics
*/
getStats() {
return {
size: this.cache.size,
maxSize: this.config.maxSize,
hitRate: 0, // Would need hit/miss tracking
memoryUsage: this.estimateMemoryUsage(),
};
}
/**
* Get file hash for change detection
*/
async getFileHash(filePath) {
try {
const stats = await stat(filePath);
const content = await readFile(filePath, 'utf8');
return createHash('sha256')
.update(`${stats.mtime.getTime()}-${content.length}`)
.digest('hex');
}
catch {
return '';
}
}
/**
* Evict least recently used item
*/
evictLRU() {
let oldestKey = '';
let oldestAccess = Number.MAX_SAFE_INTEGER;
for (const [key, access] of this.accessOrder) {
if (access < oldestAccess) {
oldestAccess = access;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
this.accessOrder.delete(oldestKey);
}
}
/**
* Estimate memory usage (rough calculation)
*/
estimateMemoryUsage() {
let size = 0;
for (const [key, entry] of this.cache) {
size += key.length * 2; // UTF-16 characters
size += JSON.stringify(entry.data).length * 2;
size += 64; // Overhead for timestamps, etc.
}
return size;
}
}
/**
* Global cache instances
*/
export const yamlCache = new MemoryCache({
maxSize: 500,
defaultTtl: 10 * 60 * 1000, // 10 minutes for YAML files
});
export const fileCache = new MemoryCache({
maxSize: 200,
defaultTtl: 5 * 60 * 1000, // 5 minutes for file content
});
/**
* Memoization decorator for functions
*/
export function memoize(fn, keyGenerator) {
const cache = new Map();
return (...args) => {
const key = keyGenerator ? keyGenerator(...args) : JSON.stringify(args);
if (cache.has(key)) {
const cached = cache.get(key);
if (cached !== undefined) {
return cached;
}
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
/**
* Async memoization decorator
*/
export function memoizeAsync(fn, keyGenerator, ttl = 5 * 60 * 1000) {
const cache = new Map();
return async (...args) => {
const key = keyGenerator ? keyGenerator(...args) : JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.result;
}
const result = await fn(...args);
cache.set(key, { result, timestamp: Date.now() });
return result;
};
}
/**
* Performance monitoring utilities
*/
const timers = new Map();
const metrics = new Map();
/**
* Start timing an operation
*/
export function startTimer(operation) {
timers.set(operation, performance.now());
}
/**
* End timing an operation and record metrics
*/
export function endTimer(operation) {
const startTime = timers.get(operation);
if (!startTime) {
throw new Error(`Timer for operation '${operation}' was not started`);
}
const duration = performance.now() - startTime;
timers.delete(operation);
// Update metrics
const existing = metrics.get(operation) || {
count: 0,
totalTime: 0,
avgTime: 0,
};
existing.count++;
existing.totalTime += duration;
existing.avgTime = existing.totalTime / existing.count;
metrics.set(operation, existing);
return duration;
}
/**
* Get performance metrics for an operation
*/
export function getMetrics(operation) {
return metrics.get(operation);
}
/**
* Get all performance metrics
*/
export function getAllMetrics() {
return Object.fromEntries(metrics);
}
/**
* Clear all metrics
*/
export function clearMetrics() {
timers.clear();
metrics.clear();
}
/**
* Decorator for timing function execution
*/
export function timeFunction(fn, operationName) {
const name = operationName || fn.name || 'anonymous';
return (...args) => {
startTimer(name);
try {
const result = fn(...args);
// Handle async functions
if (result instanceof Promise) {
return result.finally(() => endTimer(name));
}
endTimer(name);
return result;
}
catch (error) {
endTimer(name);
throw error;
}
};
}
//# sourceMappingURL=cache.js.map