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