UNPKG

@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
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