Skip to main content
Glama
SearchResultCache.ts8.98 kB
/** * Cache system for search results to improve performance for repeated queries */ /** * Cache entry with TTL */ interface CacheEntry<T> { // The cached data data: T; // Expiration timestamp expiration: number; // When the entry was created created: number; // Size of the entry in bytes (approximate) size: number; } /** * Cache configuration */ export interface SearchCacheConfig { // Maximum cache size in bytes maxSize?: number; // Default TTL in milliseconds defaultTtl?: number; // Enable cache statistics enableStats?: boolean; } /** * Cache statistics */ export interface CacheStats { // Total number of cache hits hits: number; // Total number of cache misses misses: number; // Hit rate (0-1) hitRate: number; // Current size in bytes currentSize: number; // Maximum size in bytes maxSize: number; // Current number of entries entryCount: number; // Number of evictions evictions: number; // Average lookup time (ms) averageLookupTime: number; } /** * A memory-efficient cache for search results */ export class SearchResultCache<T> { private cache: Map<string, CacheEntry<T>> = new Map(); private maxSize: number; private currentSize: number = 0; private defaultTtl: number; private enableStats: boolean; // Statistics private hits: number = 0; private misses: number = 0; private evictions: number = 0; private totalLookupTime: number = 0; private totalLookups: number = 0; /** * Create a new SearchResultCache * @param config Configuration options */ constructor(config?: SearchCacheConfig) { // Default to 100MB max size this.maxSize = config?.maxSize || 100 * 1024 * 1024; // Default to 5 minute TTL this.defaultTtl = config?.defaultTtl || 5 * 60 * 1000; // Enable stats by default this.enableStats = config?.enableStats !== false; } /** * Estimate the size of an object in bytes * @param obj The object to measure * @returns Approximate size in bytes */ // eslint-disable-next-line @typescript-eslint/no-explicit-any private estimateSize(obj: any): number { if (obj === null || obj === undefined) { return 0; } // For arrays of numbers (vectors), use more precise calculation if (Array.isArray(obj) && obj.length > 0 && typeof obj[0] === 'number') { return obj.length * 8; // 8 bytes per number (Float64) } // For strings, use character count * 2 (for UTF-16) if (typeof obj === 'string') { return obj.length * 2; } // For simple objects with a 'data' property containing a string if (obj && typeof obj === 'object' && typeof obj.data === 'string') { return obj.data.length * 2 + 100; // String length + overhead } // Use JSON stringification as an approximation for complex objects // Add a small overhead to account for object structure try { const json = JSON.stringify(obj); return json ? json.length * 2 + 100 : 100; // UTF-16 characters + overhead } catch { // If stringification fails, use a reasonable default return 1024; // 1KB default } } /** * Generate a cache key from a query and parameters * @param query Original query string * @param params Optional parameters that affect the query * @returns A cache key string */ private generateKey(query: string, params?: Record<string, unknown>): string { if (!params) { return query; } // Sort keys for consistent key generation regardless of parameter order const sortedParams = Object.keys(params) .sort() .map((key) => `${key}:${JSON.stringify(params[key])}`) .join(','); return `${query}|${sortedParams}`; } /** * Evict the oldest or least valuable entries to free up space * @param requiredSpace The amount of space needed */ private evictEntries(requiredSpace: number): void { // If cache is empty, nothing to evict if (this.cache.size === 0) { return; } // Create an array of entries sorted by "value" // Value is determined by how recently they were created const entries = Array.from(this.cache.entries()).sort((a, b) => a[1].created - b[1].created); let freedSpace = 0; let evictionCount = 0; // Evict entries until we have enough space for (const [key, entry] of entries) { if (freedSpace >= requiredSpace && evictionCount > 0) { break; } this.cache.delete(key); freedSpace += entry.size; this.currentSize -= entry.size; evictionCount++; // Only evict the oldest entry and then check if we have enough space if (evictionCount === 1 && freedSpace >= requiredSpace) { break; } } // Update statistics if (this.enableStats) { this.evictions += evictionCount; } } /** * Set a cache entry * @param query The original query * @param params Optional parameters that affect the results * @param data The data to cache * @param ttl Optional time-to-live in milliseconds */ set(query: string, data: T, params?: Record<string, unknown>, ttl?: number): void { // Clean expired entries this.removeExpired(); // Generate cache key const key = this.generateKey(query, params); // Estimate data size const size = this.estimateSize(data); // If item is too large for the cache, don't cache it if (size > this.maxSize) { return; } // Calculate expiration time const now = Date.now(); const expiration = now + (ttl || this.defaultTtl); // Create cache entry const entry: CacheEntry<T> = { data, expiration, created: now, size, }; // Check if we need to make space if (this.currentSize + size > this.maxSize) { this.evictEntries(size); } // Add to cache this.cache.set(key, entry); this.currentSize += size; } /** * Get a value from the cache * @param query The original query * @param params Optional parameters that affect the results * @returns The cached data or undefined if not found or expired */ get(query: string, params?: Record<string, unknown>): T | undefined { const startTime = this.enableStats ? performance.now() : 0; // Generate cache key const key = this.generateKey(query, params); // Get entry const entry = this.cache.get(key); // Track lookup time if (this.enableStats) { const endTime = performance.now(); this.totalLookupTime += endTime - startTime; this.totalLookups++; } // If entry doesn't exist, return undefined if (!entry) { if (this.enableStats) { this.misses++; } return undefined; } // Check if expired if (entry.expiration < Date.now()) { // Remove expired entry this.cache.delete(key); this.currentSize -= entry.size; if (this.enableStats) { this.misses++; } return undefined; } // Valid cache hit if (this.enableStats) { this.hits++; } return entry.data; } /** * Remove all expired entries from the cache */ removeExpired(): void { const now = Date.now(); let removedSize = 0; for (const [key, entry] of this.cache.entries()) { if (entry.expiration < now) { this.cache.delete(key); removedSize += entry.size; } } this.currentSize -= removedSize; } /** * Clear the entire cache */ clear(): void { this.cache.clear(); this.currentSize = 0; } /** * Get cache statistics * @returns Cache statistics */ getStats(): CacheStats { // Calculate hit rate const totalRequests = this.hits + this.misses; const hitRate = totalRequests > 0 ? this.hits / totalRequests : 0; // Calculate average lookup time const averageLookupTime = this.totalLookups > 0 ? this.totalLookupTime / this.totalLookups : 0; return { hits: this.hits, misses: this.misses, hitRate, currentSize: this.currentSize, maxSize: this.maxSize, entryCount: this.cache.size, evictions: this.evictions, averageLookupTime, }; } /** * Get the current number of entries in the cache * @returns Number of entries */ size(): number { return this.cache.size; } /** * Check if the cache contains a specific key * @param query The original query * @param params Optional parameters that affect the results * @returns True if the key exists and is not expired */ has(query: string, params?: Record<string, unknown>): boolean { const key = this.generateKey(query, params); const entry = this.cache.get(key); if (!entry) { return false; } if (entry.expiration < Date.now()) { // Remove expired entry this.cache.delete(key); this.currentSize -= entry.size; return false; } return true; } }

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/gannonh/memento-mcp'

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