UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

299 lines (298 loc) 7.51 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../monitoring/logger.js"; class LRUQueryCache { cache = /* @__PURE__ */ new Map(); maxSize; ttlMs; enableMetrics; // Metrics metrics = { hits: 0, misses: 0, evictions: 0, totalQueries: 0 }; constructor(options = {}) { this.maxSize = options.maxSize ?? 1e3; this.ttlMs = options.ttlMs ?? 3e5; this.enableMetrics = options.enableMetrics ?? true; logger.info("Query cache initialized", { maxSize: this.maxSize, ttlMs: this.ttlMs, enableMetrics: this.enableMetrics }); } /** * Get a value from cache */ get(key) { if (this.enableMetrics) { this.metrics.totalQueries++; } const entry = this.cache.get(key); if (!entry) { if (this.enableMetrics) { this.metrics.misses++; } return void 0; } const now = Date.now(); if (now - entry.createdAt > this.ttlMs) { this.cache.delete(key); if (this.enableMetrics) { this.metrics.misses++; this.metrics.evictions++; } logger.debug("Cache entry expired", { key, age: now - entry.createdAt }); return void 0; } entry.accessCount++; entry.lastAccessed = now; this.cache.delete(key); this.cache.set(key, entry); if (this.enableMetrics) { this.metrics.hits++; } logger.debug("Cache hit", { key, accessCount: entry.accessCount }); return entry.value; } /** * Set a value in cache */ set(key, value) { const now = Date.now(); if (this.cache.has(key)) { this.cache.delete(key); } while (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey !== void 0) { this.cache.delete(firstKey); if (this.enableMetrics) { this.metrics.evictions++; } logger.debug("Evicted LRU entry", { key: firstKey }); } else { break; } } const entry = { value, createdAt: now, accessCount: 0, lastAccessed: now }; this.cache.set(key, entry); logger.debug("Cache set", { key, size: this.cache.size }); } /** * Delete a specific key */ delete(key) { const deleted = this.cache.delete(key); if (deleted) { logger.debug("Cache delete", { key }); } return deleted; } /** * Clear all cached entries */ clear() { const size = this.cache.size; this.cache.clear(); logger.info("Cache cleared", { previousSize: size }); } /** * Invalidate entries matching a pattern */ invalidatePattern(pattern) { let count = 0; for (const key of this.cache.keys()) { if (pattern.test(key)) { this.cache.delete(key); count++; } } logger.info("Pattern invalidation", { pattern: pattern.source, invalidated: count }); return count; } /** * Get cache metrics */ getMetrics() { return { ...this.metrics, hitRate: this.metrics.totalQueries > 0 ? this.metrics.hits / this.metrics.totalQueries : 0, size: this.cache.size, maxSize: this.maxSize }; } /** * Get cache contents for debugging */ debug() { return Array.from(this.cache.entries()).map(([key, entry]) => ({ key, entry })); } /** * Cleanup expired entries */ cleanup() { const now = Date.now(); let removed = 0; for (const [key, entry] of this.cache.entries()) { if (now - entry.createdAt > this.ttlMs) { this.cache.delete(key); removed++; if (this.enableMetrics) { this.metrics.evictions++; } } } if (removed > 0) { logger.info("Cache cleanup completed", { removed, remaining: this.cache.size }); } return removed; } } class StackMemoryQueryCache { frameCache = new LRUQueryCache({ maxSize: 500, ttlMs: 3e5 }); // 5 min eventCache = new LRUQueryCache({ maxSize: 1e3, ttlMs: 18e4 }); // 3 min anchorCache = new LRUQueryCache({ maxSize: 200, ttlMs: 6e5 }); // 10 min digestCache = new LRUQueryCache({ maxSize: 100, ttlMs: 9e5 }); // 15 min /** * Cache frame data */ cacheFrame(frameId, data) { this.frameCache.set(`frame:${frameId}`, data); } getFrame(frameId) { return this.frameCache.get(`frame:${frameId}`); } /** * Cache frame context assemblies (expensive operations) */ cacheFrameContext(frameId, context) { this.frameCache.set(`context:${frameId}`, context); } getFrameContext(frameId) { return this.frameCache.get(`context:${frameId}`); } /** * Cache events for a frame */ cacheFrameEvents(frameId, events) { this.eventCache.set(`events:${frameId}`, events); } getFrameEvents(frameId) { return this.eventCache.get(`events:${frameId}`); } /** * Cache anchors */ cacheAnchors(frameId, anchors) { this.anchorCache.set(`anchors:${frameId}`, anchors); } getAnchors(frameId) { return this.anchorCache.get(`anchors:${frameId}`); } /** * Cache digest data */ cacheDigest(frameId, digest) { this.digestCache.set(`digest:${frameId}`, digest); } getDigest(frameId) { return this.digestCache.get(`digest:${frameId}`); } /** * Invalidate caches for a specific frame */ invalidateFrame(frameId) { this.frameCache.delete(`frame:${frameId}`); this.frameCache.delete(`context:${frameId}`); this.eventCache.delete(`events:${frameId}`); this.anchorCache.delete(`anchors:${frameId}`); this.digestCache.delete(`digest:${frameId}`); logger.info("Invalidated frame caches", { frameId }); } /** * Invalidate all caches for a project */ invalidateProject(projectId) { const pattern = new RegExp(`^(frame|context|events|anchors|digest):.+`); let total = 0; total += this.frameCache.invalidatePattern(pattern); total += this.eventCache.invalidatePattern(pattern); total += this.anchorCache.invalidatePattern(pattern); total += this.digestCache.invalidatePattern(pattern); logger.info("Invalidated project caches", { projectId, totalInvalidated: total }); } /** * Get comprehensive cache metrics */ getMetrics() { return { frame: this.frameCache.getMetrics(), event: this.eventCache.getMetrics(), anchor: this.anchorCache.getMetrics(), digest: this.digestCache.getMetrics() }; } /** * Cleanup all caches */ cleanup() { this.frameCache.cleanup(); this.eventCache.cleanup(); this.anchorCache.cleanup(); this.digestCache.cleanup(); } /** * Clear all caches */ clear() { this.frameCache.clear(); this.eventCache.clear(); this.anchorCache.clear(); this.digestCache.clear(); logger.info("All StackMemory caches cleared"); } } let globalCache = null; function getQueryCache() { if (!globalCache) { globalCache = new StackMemoryQueryCache(); } return globalCache; } function createCacheKey(queryName, params) { const paramsStr = params.map((p) => typeof p === "object" ? JSON.stringify(p) : String(p)).join(":"); return `${queryName}:${paramsStr}`; } export { LRUQueryCache, StackMemoryQueryCache, createCacheKey, getQueryCache }; //# sourceMappingURL=query-cache.js.map