Skip to main content
Glama

Open Search MCP

by flyanima
MIT License
2
  • Apple
  • Linux
cache-manager.ts9.77 kB
import { defaultLogger as logger } from './logger.js'; export interface CacheEntry<T = any> { key: string; value: T; timestamp: number; ttl: number; accessCount: number; lastAccessed: number; size: number; } export interface CacheStats { totalEntries: number; totalSize: number; hitRate: number; missRate: number; evictionCount: number; oldestEntry: number; newestEntry: number; averageAccessCount: number; } export interface CacheConfig { maxSize: number; // Maximum cache size in bytes maxEntries: number; // Maximum number of entries defaultTTL: number; // Default TTL in milliseconds cleanupInterval: number; // Cleanup interval in milliseconds evictionPolicy: 'lru' | 'lfu' | 'ttl'; // Eviction policy } /** * Advanced Cache Manager with multiple eviction policies and performance optimization */ export class CacheManager { private cache: Map<string, CacheEntry> = new Map(); private config: CacheConfig; private stats = { hits: 0, misses: 0, evictions: 0 }; private cleanupTimer?: NodeJS.Timeout; constructor(config?: Partial<CacheConfig>) { this.config = { maxSize: 100 * 1024 * 1024, // 100MB maxEntries: 10000, defaultTTL: 3600000, // 1 hour cleanupInterval: 300000, // 5 minutes evictionPolicy: 'lru', ...config }; this.startCleanupTimer(); } /** * Get value from cache */ public get<T>(key: string): T | null { const entry = this.cache.get(key); if (!entry) { this.stats.misses++; return null; } // Check if entry has expired if (this.isExpired(entry)) { this.cache.delete(key); this.stats.misses++; return null; } // Update access statistics entry.accessCount++; entry.lastAccessed = Date.now(); this.stats.hits++; return entry.value as T; } /** * Set value in cache */ public set<T>(key: string, value: T, ttl?: number): void { const now = Date.now(); const entryTTL = ttl || this.config.defaultTTL; const size = this.calculateSize(value); const entry: CacheEntry<T> = { key, value, timestamp: now, ttl: entryTTL, accessCount: 1, lastAccessed: now, size }; // Check if we need to evict entries this.ensureCapacity(size); this.cache.set(key, entry); logger.debug(`Cache set: ${key} (size: ${size} bytes, TTL: ${entryTTL}ms)`); } /** * Delete entry from cache */ public delete(key: string): boolean { const deleted = this.cache.delete(key); if (deleted) { logger.debug(`Cache delete: ${key}`); } return deleted; } /** * Check if key exists in cache */ public has(key: string): boolean { const entry = this.cache.get(key); return entry !== undefined && !this.isExpired(entry); } /** * Clear all cache entries */ public clear(): void { this.cache.clear(); this.stats = { hits: 0, misses: 0, evictions: 0 }; logger.info('Cache cleared'); } /** * Get cache statistics */ public getStats(): CacheStats { const entries = Array.from(this.cache.values()); const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0); const totalRequests = this.stats.hits + this.stats.misses; return { totalEntries: this.cache.size, totalSize, hitRate: totalRequests > 0 ? this.stats.hits / totalRequests : 0, missRate: totalRequests > 0 ? this.stats.misses / totalRequests : 0, evictionCount: this.stats.evictions, oldestEntry: entries.length > 0 ? Math.min(...entries.map(e => e.timestamp)) : 0, newestEntry: entries.length > 0 ? Math.max(...entries.map(e => e.timestamp)) : 0, averageAccessCount: entries.length > 0 ? entries.reduce((sum, e) => sum + e.accessCount, 0) / entries.length : 0 }; } /** * Get cache entries for debugging */ public getEntries(): CacheEntry[] { return Array.from(this.cache.values()); } /** * Optimize cache by removing expired entries and applying eviction policy */ public optimize(): void { const beforeSize = this.cache.size; // Remove expired entries this.removeExpiredEntries(); // Apply eviction policy if still over capacity this.applyEvictionPolicy(); const afterSize = this.cache.size; const removed = beforeSize - afterSize; if (removed > 0) { logger.info(`Cache optimized: removed ${removed} entries`); } } /** * Generate cache key from parameters */ public static generateKey(prefix: string, params: Record<string, any>): string { const sortedParams = Object.keys(params) .sort() .map(key => `${key}:${JSON.stringify(params[key])}`) .join('|'); return `${prefix}:${Buffer.from(sortedParams).toString('base64')}`; } /** * Check if entry has expired */ private isExpired(entry: CacheEntry): boolean { return Date.now() - entry.timestamp > entry.ttl; } /** * Calculate size of value in bytes */ private calculateSize(value: any): number { try { return Buffer.byteLength(JSON.stringify(value), 'utf8'); } catch { // Fallback for non-serializable values return 1024; // 1KB estimate } } /** * Ensure cache has capacity for new entry */ private ensureCapacity(newEntrySize: number): void { // Check entry count limit while (this.cache.size >= this.config.maxEntries) { this.evictOne(); } // Check size limit const currentSize = Array.from(this.cache.values()).reduce((sum, entry) => sum + entry.size, 0); while (currentSize + newEntrySize > this.config.maxSize && this.cache.size > 0) { this.evictOne(); } } /** * Evict one entry based on eviction policy */ private evictOne(): void { let keyToEvict: string | null = null; switch (this.config.evictionPolicy) { case 'lru': keyToEvict = this.findLRUKey(); break; case 'lfu': keyToEvict = this.findLFUKey(); break; case 'ttl': keyToEvict = this.findOldestKey(); break; } if (keyToEvict) { this.cache.delete(keyToEvict); this.stats.evictions++; logger.debug(`Cache evicted: ${keyToEvict} (policy: ${this.config.evictionPolicy})`); } } /** * Find least recently used key */ private findLRUKey(): string | null { let oldestKey: string | null = null; let oldestTime = Infinity; for (const [key, entry] of this.cache) { if (entry.lastAccessed < oldestTime) { oldestTime = entry.lastAccessed; oldestKey = key; } } return oldestKey; } /** * Find least frequently used key */ private findLFUKey(): string | null { let leastUsedKey: string | null = null; let leastCount = Infinity; for (const [key, entry] of this.cache) { if (entry.accessCount < leastCount) { leastCount = entry.accessCount; leastUsedKey = key; } } return leastUsedKey; } /** * Find oldest key by timestamp */ private findOldestKey(): string | null { let oldestKey: string | null = null; let oldestTime = Infinity; for (const [key, entry] of this.cache) { if (entry.timestamp < oldestTime) { oldestTime = entry.timestamp; oldestKey = key; } } return oldestKey; } /** * Remove expired entries */ private removeExpiredEntries(): void { const now = Date.now(); const expiredKeys: string[] = []; for (const [key, entry] of this.cache) { if (now - entry.timestamp > entry.ttl) { expiredKeys.push(key); } } expiredKeys.forEach(key => { this.cache.delete(key); logger.debug(`Cache expired: ${key}`); }); } /** * Apply eviction policy to maintain cache limits */ private applyEvictionPolicy(): void { // Apply size-based eviction const currentSize = Array.from(this.cache.values()).reduce((sum, entry) => sum + entry.size, 0); while (currentSize > this.config.maxSize && this.cache.size > 0) { this.evictOne(); } // Apply count-based eviction while (this.cache.size > this.config.maxEntries) { this.evictOne(); } } /** * Start cleanup timer */ private startCleanupTimer(): void { this.cleanupTimer = setInterval(() => { this.optimize(); }, this.config.cleanupInterval); } /** * Stop cleanup timer */ public destroy(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; } this.clear(); } /** * Get cache configuration */ public getConfig(): CacheConfig { return { ...this.config }; } /** * Update cache configuration */ public updateConfig(newConfig: Partial<CacheConfig>): void { this.config = { ...this.config, ...newConfig }; // Restart cleanup timer if interval changed if (newConfig.cleanupInterval && this.cleanupTimer) { clearInterval(this.cleanupTimer); this.startCleanupTimer(); } // Apply new limits immediately this.optimize(); logger.info('Cache configuration updated', newConfig); } } // Global cache manager instance export const cacheManager = new CacheManager({ maxSize: parseInt(process.env.CACHE_MAX_SIZE || '104857600'), // 100MB maxEntries: parseInt(process.env.CACHE_MAX_ENTRIES || '10000'), defaultTTL: parseInt(process.env.CACHE_TTL || '3600000'), // 1 hour cleanupInterval: parseInt(process.env.CACHE_CLEANUP_INTERVAL || '300000'), // 5 minutes evictionPolicy: (process.env.CACHE_EVICTION_POLICY as 'lru' | 'lfu' | 'ttl') || 'lru' });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/flyanima/open-search-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server