@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
301 lines • 8.96 kB
JavaScript
/**
* In-Memory Cache Implementation
*
* L1 cache layer with LRU eviction policy and size limits.
* Provides ultra-fast access for frequently used queries.
*/
import { getLogger } from '../../../logging/Logger.js';
export class InMemoryCache {
logger = getLogger();
cache = new Map();
accessOrder = []; // For LRU tracking
// Configuration
maxSizeBytes;
maxEntries;
defaultTTL;
checkInterval;
// Statistics
stats = {
hits: 0,
misses: 0,
evictions: 0,
expirations: 0,
};
currentSizeBytes = 0;
cleanupTimer;
constructor(options = {}) {
this.maxSizeBytes = (options.maxSizeMB || 100) * 1024 * 1024; // Default 100MB
this.maxEntries = options.maxEntries || 10000;
this.defaultTTL = options.defaultTTL || 300000; // Default 5 minutes
this.checkInterval = options.checkInterval || 60000; // Default 1 minute
// Start cleanup timer
this.startCleanupTimer();
this.logger.info({
maxSizeMB: this.maxSizeBytes / 1024 / 1024,
maxEntries: this.maxEntries,
defaultTTL: this.defaultTTL,
}, 'InMemoryCache initialized');
}
/**
* Get value from cache
*/
async get(key) {
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
this.logger.debug({ key }, 'Cache miss');
return null;
}
// Check expiration
if (Date.now() > entry.expiresAt) {
this.stats.expirations++;
this.logger.debug({ key }, 'Cache entry expired');
this.delete(key);
return null;
}
// Update access tracking
this.updateAccessOrder(key);
entry.lastAccessed = Date.now();
entry.hits++;
this.stats.hits++;
this.logger.debug({
key,
hits: entry.hits,
ttlRemaining: entry.expiresAt - Date.now()
}, 'Cache hit');
// Return cached result with updated metadata
return {
data: entry.value,
metadata: {
...entry.metadata,
hits: entry.hits,
}
};
}
/**
* Set value in cache
*/
async set(key, value, ttl, metadata) {
const effectiveTTL = ttl || this.defaultTTL;
const size = this.estimateSize(value);
const now = Date.now();
// Check if we need to evict entries
if (this.needsEviction(size)) {
await this.evictEntries(size);
}
// Create cache entry
const entry = {
key,
value,
metadata: {
cached: true,
cachedAt: now,
ttl: effectiveTTL,
cacheKey: key,
source: 'memory',
hits: 0,
...metadata
},
expiresAt: now + effectiveTTL,
size,
lastAccessed: now,
hits: 0,
};
// Remove old entry if exists
if (this.cache.has(key)) {
this.delete(key);
}
// Add new entry
this.cache.set(key, entry);
this.accessOrder.push(key);
this.currentSizeBytes += size;
this.logger.debug({
key,
size,
ttl: effectiveTTL,
expiresAt: new Date(entry.expiresAt),
currentSizeMB: this.currentSizeBytes / 1024 / 1024,
}, 'Cache entry added');
}
/**
* Delete entry from cache
*/
delete(key) {
const entry = this.cache.get(key);
if (!entry)
return false;
this.cache.delete(key);
this.currentSizeBytes -= entry.size;
// Remove from access order
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
this.logger.debug({ key, size: entry.size }, 'Cache entry deleted');
return true;
}
/**
* Clear all entries
*/
clear() {
const entriesCleared = this.cache.size;
this.cache.clear();
this.accessOrder.length = 0;
this.currentSizeBytes = 0;
this.logger.info({ entriesCleared }, 'Cache cleared');
}
/**
* Invalidate entries matching pattern
*/
async invalidate(patterns) {
let invalidated = 0;
for (const pattern of patterns) {
const regex = this.patternToRegex(pattern);
for (const key of this.cache.keys()) {
if (regex.test(key)) {
this.delete(key);
invalidated++;
}
}
}
this.logger.info({ patterns, invalidated }, 'Cache entries invalidated');
return invalidated;
}
/**
* Get cache statistics
*/
getStats() {
const entries = this.cache.size;
const sizeBytes = this.currentSizeBytes;
const sizeMB = sizeBytes / 1024 / 1024;
const total = this.stats.hits + this.stats.misses;
const hitRate = total > 0 ? this.stats.hits / total : 0;
return {
entries,
sizeBytes,
sizeMB,
hits: this.stats.hits,
misses: this.stats.misses,
evictions: this.stats.evictions,
expirations: this.stats.expirations,
hitRate,
};
}
/**
* Get all keys (for debugging)
*/
keys() {
return Array.from(this.cache.keys());
}
/**
* Check if needs eviction
*/
needsEviction(additionalSize) {
return (this.currentSizeBytes + additionalSize > this.maxSizeBytes ||
this.cache.size >= this.maxEntries);
}
/**
* Evict entries using LRU policy
*/
async evictEntries(requiredSize) {
const targetSize = this.maxSizeBytes * 0.9; // Free up to 90% capacity
let evicted = 0;
while ((this.currentSizeBytes + requiredSize > targetSize ||
this.cache.size >= this.maxEntries) &&
this.accessOrder.length > 0) {
// Get least recently used key
const lruKey = this.accessOrder[0];
if (this.delete(lruKey)) {
evicted++;
this.stats.evictions++;
}
}
if (evicted > 0) {
this.logger.info({
evicted,
currentSizeMB: this.currentSizeBytes / 1024 / 1024
}, 'Evicted LRU entries');
}
}
/**
* Update access order for LRU tracking
*/
updateAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
// Move to end (most recently used)
this.accessOrder.splice(index, 1);
this.accessOrder.push(key);
}
}
/**
* Estimate size of value in bytes
*/
estimateSize(value) {
if (value === null || value === undefined)
return 8;
switch (typeof value) {
case 'boolean':
return 4;
case 'number':
return 8;
case 'string':
return value.length * 2; // UTF-16
case 'object':
if (Buffer.isBuffer(value)) {
return value.length;
}
// Rough estimate for objects
const json = JSON.stringify(value);
return json.length * 2;
default:
return 64; // Default estimate
}
}
/**
* Convert wildcard pattern to regex
*/
patternToRegex(pattern) {
// Escape special regex characters except *
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
// Replace * with .*
const regexPattern = escaped.replace(/\*/g, '.*');
return new RegExp(`^${regexPattern}$`);
}
/**
* Start cleanup timer for expired entries
*/
startCleanupTimer() {
this.cleanupTimer = setInterval(() => {
this.cleanupExpired();
}, this.checkInterval);
}
/**
* Clean up expired entries
*/
cleanupExpired() {
const now = Date.now();
let cleaned = 0;
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.delete(key);
cleaned++;
this.stats.expirations++;
}
}
if (cleaned > 0) {
this.logger.debug({ cleaned }, 'Cleaned expired entries');
}
}
/**
* Shutdown cache and stop timers
*/
shutdown() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
this.clear();
this.logger.info('InMemoryCache shutdown');
}
}
//# sourceMappingURL=InMemoryCache.js.map