@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.
274 lines (273 loc) • 7.09 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 { EventEmitter } from "events";
import { logger } from "../monitoring/logger.js";
class ContextCache extends EventEmitter {
cache = /* @__PURE__ */ new Map();
accessOrder = [];
options;
stats;
currentSize = 0;
constructor(options = {}) {
super();
this.options = {
maxSize: options.maxSize || 100 * 1024 * 1024,
// 100MB default
maxItems: options.maxItems || 1e4,
defaultTTL: options.defaultTTL || 36e5,
// 1 hour default
enableStats: options.enableStats ?? true,
onEvict: options.onEvict || (() => {
})
};
this.stats = {
hits: 0,
misses: 0,
evictions: 0,
size: 0,
itemCount: 0,
hitRate: 0,
avgAccessTime: 0
};
}
/**
* Get item from cache
*/
get(key) {
const startTime = Date.now();
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
this.updateHitRate();
return void 0;
}
if (entry.ttl && Date.now() - entry.createdAt > entry.ttl) {
this.delete(key);
this.stats.misses++;
this.updateHitRate();
return void 0;
}
entry.hits++;
entry.lastAccessed = Date.now();
this.updateAccessOrder(key);
this.stats.hits++;
this.updateHitRate();
this.updateAvgAccessTime(Date.now() - startTime);
return entry.value;
}
/**
* Set item in cache
*/
set(key, value, options = {}) {
const size = options.size || this.estimateSize(value);
const ttl = options.ttl ?? this.options.defaultTTL;
if (this.cache.size >= this.options.maxItems || this.currentSize + size > this.options.maxSize) {
this.evict(size);
}
if (this.cache.has(key)) {
this.delete(key);
}
const entry = {
value,
size,
hits: 0,
createdAt: Date.now(),
lastAccessed: Date.now(),
ttl
};
this.cache.set(key, entry);
this.accessOrder.push(key);
this.currentSize += size;
this.stats.size = this.currentSize;
this.stats.itemCount = this.cache.size;
this.emit("set", key, value);
}
/**
* Delete item from cache
*/
delete(key) {
const entry = this.cache.get(key);
if (!entry) return false;
this.cache.delete(key);
this.currentSize -= entry.size;
this.accessOrder = this.accessOrder.filter((k) => k !== key);
this.stats.size = this.currentSize;
this.stats.itemCount = this.cache.size;
this.emit("delete", key);
return true;
}
/**
* Clear entire cache
*/
clear() {
const oldSize = this.cache.size;
this.cache.clear();
this.accessOrder = [];
this.currentSize = 0;
this.stats.size = 0;
this.stats.itemCount = 0;
this.stats.evictions += oldSize;
this.emit("clear");
}
/**
* Check if key exists and is valid
*/
has(key) {
const entry = this.cache.get(key);
if (!entry) return false;
if (entry.ttl && Date.now() - entry.createdAt > entry.ttl) {
this.delete(key);
return false;
}
return true;
}
/**
* Get cache statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Get cache size info
*/
getSize() {
return {
bytes: this.currentSize,
items: this.cache.size,
utilization: this.currentSize / this.options.maxSize
};
}
/**
* Preload multiple items
*/
preload(entries) {
const startTime = Date.now();
for (const entry of entries) {
this.set(entry.key, entry.value, {
ttl: entry.ttl,
size: entry.size
});
}
logger.debug("Cache preload complete", {
items: entries.length,
duration: Date.now() - startTime,
cacheSize: this.currentSize
});
}
/**
* Get multiple items efficiently
*/
getMany(keys) {
const results = /* @__PURE__ */ new Map();
for (const key of keys) {
const value = this.get(key);
if (value !== void 0) {
results.set(key, value);
}
}
return results;
}
/**
* Warm cache with computed values
*/
async warmUp(keys, compute, options = {}) {
const { parallel = true } = options;
if (parallel) {
const promises = keys.map(async (key) => {
if (!this.has(key)) {
const value = await compute(key);
this.set(key, value, { ttl: options.ttl });
}
});
await Promise.all(promises);
} else {
for (const key of keys) {
if (!this.has(key)) {
const value = await compute(key);
this.set(key, value, { ttl: options.ttl });
}
}
}
}
/**
* Get or compute value
*/
async getOrCompute(key, compute, options = {}) {
const cached = this.get(key);
if (cached !== void 0) {
return cached;
}
const value = await compute();
this.set(key, value, options);
return value;
}
// Private methods
evict(requiredSize) {
const startEvictions = this.stats.evictions;
while ((this.cache.size >= this.options.maxItems || this.currentSize + requiredSize > this.options.maxSize) && this.accessOrder.length > 0) {
const keyToEvict = this.accessOrder.shift();
const entry = this.cache.get(keyToEvict);
if (entry) {
this.cache.delete(keyToEvict);
this.currentSize -= entry.size;
this.stats.evictions++;
this.options.onEvict(keyToEvict, entry);
this.emit("evict", keyToEvict, entry);
}
}
if (this.stats.evictions > startEvictions) {
logger.debug("Cache eviction", {
evicted: this.stats.evictions - startEvictions,
currentSize: this.currentSize,
requiredSize
});
}
}
updateAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
this.accessOrder.push(key);
}
estimateSize(value) {
if (typeof value === "string") {
return value.length * 2;
}
if (typeof value === "object" && value !== null) {
return JSON.stringify(value).length * 2;
}
return 8;
}
updateHitRate() {
const total = this.stats.hits + this.stats.misses;
this.stats.hitRate = total > 0 ? this.stats.hits / total : 0;
}
updateAvgAccessTime(time) {
const alpha = 0.1;
this.stats.avgAccessTime = this.stats.avgAccessTime * (1 - alpha) + time * alpha;
}
/**
* Cleanup expired entries periodically
*/
startCleanup(intervalMs = 6e4) {
return setInterval(() => {
let cleaned = 0;
for (const [key, entry] of this.cache.entries()) {
if (entry.ttl && Date.now() - entry.createdAt > entry.ttl) {
this.delete(key);
cleaned++;
}
}
if (cleaned > 0) {
logger.debug("Cache cleanup", { cleaned, remaining: this.cache.size });
}
}, intervalMs);
}
}
export {
ContextCache
};
//# sourceMappingURL=context-cache.js.map