UNPKG

@rhofkens/mcp-quotes-server-claude-code

Version:

Model Context Protocol (MCP) server for managing and serving quotes

235 lines 6.28 kB
/** * 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