@rhofkens/mcp-quotes-server-claude-code
Version:
Model Context Protocol (MCP) server for managing and serving quotes
235 lines • 6.28 kB
JavaScript
/**
* Cache Layer for Quote Responses
*
* Provides in-memory caching with TTL support for API responses
* to enable fallback mechanisms and reduce API calls
*/
import { logger } from './logger.js';
/**
* Generic in-memory cache with TTL and LRU eviction
*/
export class Cache {
cache;
maxSize;
defaultTTL;
stats;
enableStats;
constructor(config = {}) {
this.cache = new Map();
this.maxSize = config.maxSize || 1000;
this.defaultTTL = config.defaultTTL || 5 * 60 * 1000; // 5 minutes default
this.enableStats = config.enableStats !== false;
this.stats = {
hits: 0,
misses: 0,
evictions: 0,
size: 0,
};
}
/**
* Get an item from cache
*/
get(key) {
const entry = this.cache.get(key);
if (!entry) {
if (this.enableStats) {
this.stats.misses++;
}
return null;
}
// Check if expired
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
if (this.enableStats) {
this.stats.misses++;
this.stats.size--;
}
return null;
}
// Update hit count and stats
entry.hits++;
if (this.enableStats) {
this.stats.hits++;
}
return entry.data;
}
/**
* Set an item in cache with optional TTL
*/
set(key, data, ttl) {
const expiresAt = Date.now() + (ttl || this.defaultTTL);
// Evict least recently used if at max size
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
this.evictLRU();
}
this.cache.set(key, {
data,
timestamp: Date.now(),
hits: 0,
expiresAt,
});
if (this.enableStats) {
this.stats.size = this.cache.size;
}
}
/**
* Check if a key exists and is not expired
*/
has(key) {
const entry = this.cache.get(key);
if (!entry) {
return false;
}
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
if (this.enableStats) {
this.stats.size--;
}
return false;
}
return true;
}
/**
* Delete an item from cache
*/
delete(key) {
const result = this.cache.delete(key);
if (result && this.enableStats) {
this.stats.size--;
}
return result;
}
/**
* Clear all cache entries
*/
clear() {
this.cache.clear();
if (this.enableStats) {
this.stats.size = 0;
}
}
/**
* Get cache statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Reset cache statistics
*/
resetStats() {
this.stats = {
hits: 0,
misses: 0,
evictions: 0,
size: this.cache.size,
};
}
/**
* Get all keys in cache
*/
keys() {
return Array.from(this.cache.keys());
}
/**
* Evict least recently used entry
*/
evictLRU() {
let oldestKey = null;
let oldestTime = Infinity;
for (const [key, entry] of this.cache.entries()) {
if (entry.timestamp < oldestTime) {
oldestTime = entry.timestamp;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
if (this.enableStats) {
this.stats.evictions++;
this.stats.size--;
}
logger.debug('Cache eviction', { key: oldestKey });
}
}
/**
* Clean up expired entries
*/
cleanup() {
const now = Date.now();
const keysToDelete = [];
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
keysToDelete.push(key);
}
}
for (const key of keysToDelete) {
this.cache.delete(key);
if (this.enableStats) {
this.stats.size--;
}
}
if (keysToDelete.length > 0) {
logger.debug('Cache cleanup', { removed: keysToDelete.length });
}
}
}
/**
* Specialized cache for quote responses
*/
export class QuoteCache extends Cache {
constructor() {
super({
maxSize: 500,
defaultTTL: 10 * 60 * 1000, // 10 minutes for quotes
enableStats: true,
});
// Set up periodic cleanup
setInterval(() => this.cleanup(), 60 * 1000); // Every minute
}
/**
* Generate cache key for quote requests
*/
static generateKey(person, topic, numberOfQuotes) {
const parts = ['quotes', person.toLowerCase()];
if (topic) {
parts.push(topic.toLowerCase());
}
if (numberOfQuotes) {
parts.push(`n${numberOfQuotes}`);
}
return parts.join(':');
}
/**
* Get quotes from cache with fallback to stale data
*/
getWithFallback(key) {
const fresh = this.get(key);
if (fresh) {
return { data: fresh, stale: false };
}
// Check for stale data (expired but still in memory)
const entry = this.cache.get(key);
if (entry) {
logger.warn('Returning stale cache data', { key, expiredAt: new Date(entry.expiresAt) });
return { data: entry.data, stale: true };
}
return { data: null, stale: false };
}
/**
* Warm up cache with common searches
*/
async warmup(commonSearches) {
logger.info('Warming up quote cache', { searches: commonSearches.length });
// This would be called during startup to pre-populate cache
// Implementation would depend on having access to the search function
}
}
// Export singleton instance
export const quoteCache = new QuoteCache();
// Export default cache for general use
export const defaultCache = new Cache({
maxSize: 1000,
defaultTTL: 5 * 60 * 1000,
});
//# sourceMappingURL=cache.js.map