Skip to main content
Glama
CacheManager.ts9.88 kB
/** * Intelligent caching system for WordPress MCP Server * Implements multi-layer caching with TTL, LRU eviction, and site-specific keys */ import { ConfigHelpers } from "@/config/Config.js"; export interface CacheEntry<T = unknown> { value: T; timestamp: number; ttl: number; etag?: string | undefined; lastModified?: string | undefined; accessCount: number; lastAccessed: number; } export interface CacheStats { hits: number; misses: number; evictions: number; totalSize: number; hitRate: number; } export interface CacheConfig { maxSize: number; defaultTTL: number; enableLRU: boolean; enableStats: boolean; sitePrefix?: string; } /** * High-performance in-memory cache with TTL and LRU eviction */ export class CacheManager { private cache: Map<string, CacheEntry> = new Map(); private accessOrder: string[] = []; private cleanupInterval: NodeJS.Timeout | null = null; private stats: CacheStats = { hits: 0, misses: 0, evictions: 0, totalSize: 0, hitRate: 0, }; constructor(private config: CacheConfig) { // Start cleanup interval (skip in test environment to avoid timer issues) if (!ConfigHelpers.isTest()) { this.startCleanupInterval(); } } /** * Generate cache key with site prefix and parameter hash * Uses fast non-cryptographic hash (FNV-1a) for 3-5x better performance */ generateKey(siteId: string, endpoint: string, params?: Record<string, unknown>): string { const baseKey = `${siteId}:${endpoint}`; if (!params || Object.keys(params).length === 0) { return baseKey; } // Use fast non-cryptographic hash (FNV-1a) for cache keys // Normalized params ensure consistent ordering const paramString = JSON.stringify(this.normalizeParams(params)); const paramHash = this.fastHash(paramString); return `${baseKey}:${paramHash}`; } /** * Fast non-cryptographic hash function (FNV-1a variant) * 3-5x faster than MD5 for cache key generation * Returns base36 string for compact keys with low collision risk */ private fastHash(str: string): string { let hash = 2166136261; // FNV offset basis (32-bit) for (let i = 0; i < str.length; i++) { hash ^= str.charCodeAt(i); // FNV prime: 16777619 hash = Math.imul(hash, 16777619); } // Convert to unsigned 32-bit integer and encode as base36 // Base36 (0-9, a-z) gives ~6 characters for 32-bit hash (more compact than hex) return (hash >>> 0).toString(36); } /** * Get value from cache if not expired */ get<T>(key: string): T | null { const entry = this.cache.get(key); if (!entry) { this.stats.misses++; this.updateHitRate(); return null; } // Check if expired if (this.isExpired(entry)) { this.cache.delete(key); this.removeFromAccessOrder(key); this.stats.misses++; this.updateHitRate(); return null; } // Update access tracking entry.accessCount++; entry.lastAccessed = Date.now(); this.updateAccessOrder(key); this.stats.hits++; this.updateHitRate(); return entry.value as T; } /** * Set value in cache with TTL */ set<T>(key: string, value: T, ttl: number = this.config.defaultTTL, etag?: string, lastModified?: string): void { // Check if we need to evict entries if (this.cache.size >= this.config.maxSize && !this.cache.has(key)) { this.evictLRU(); } const entry: CacheEntry<T> = { value, timestamp: Date.now(), ttl, etag, lastModified, accessCount: 1, lastAccessed: Date.now(), }; // If updating existing entry, remove from access order first if (this.cache.has(key)) { this.removeFromAccessOrder(key); } this.cache.set(key, entry); this.accessOrder.push(key); this.stats.totalSize = this.cache.size; } /** * Check if cache entry exists and is valid */ has(key: string): boolean { const entry = this.cache.get(key); return entry !== undefined && !this.isExpired(entry); } /** * Delete specific cache entry */ delete(key: string): boolean { const existed = this.cache.delete(key); if (existed) { this.removeFromAccessOrder(key); this.stats.totalSize = this.cache.size; } return existed; } /** * Clear all cache entries */ clear(): void { this.cache.clear(); this.accessOrder = []; this.stats.totalSize = 0; } /** * Clear cache entries for specific site */ clearSite(siteId: string): number { let cleared = 0; const keysToDelete: string[] = []; for (const key of this.cache.keys()) { if (key.startsWith(`${siteId}:`)) { keysToDelete.push(key); } } for (const key of keysToDelete) { this.delete(key); cleared++; } return cleared; } /** * Clear cache entries matching pattern */ clearPattern(pattern: RegExp): number { let cleared = 0; const keysToDelete: string[] = []; for (const key of this.cache.keys()) { if (pattern.test(key)) { keysToDelete.push(key); } } for (const key of keysToDelete) { this.delete(key); cleared++; } return cleared; } /** * Get cache statistics */ getStats(): CacheStats { return { ...this.stats }; } /** * Get cache entry with metadata */ getEntry(key: string): CacheEntry | null { const entry = this.cache.get(key); if (!entry || this.isExpired(entry)) { return null; } return { ...entry }; } /** * Check if entry supports conditional requests */ supportsConditionalRequest(key: string): boolean { const entry = this.cache.get(key); return !!(entry && (entry.etag || entry.lastModified)); } /** * Get conditional request headers */ getConditionalHeaders(key: string): Record<string, string> { const entry = this.cache.get(key); if (!entry) return {}; const headers: Record<string, string> = {}; if (entry.etag) { headers["If-None-Match"] = entry.etag; } if (entry.lastModified) { headers["If-Modified-Since"] = entry.lastModified; } return headers; } /** * Check if entry is expired */ private isExpired(entry: CacheEntry): boolean { return Date.now() - entry.timestamp > entry.ttl; } /** * Evict least recently used entry */ private evictLRU(): void { if (this.accessOrder.length === 0) return; const lruKey = this.accessOrder[0]; this.cache.delete(lruKey); this.accessOrder.shift(); this.stats.evictions++; this.stats.totalSize = this.cache.size; } /** * Update access order for LRU tracking */ private updateAccessOrder(key: string): void { if (!this.config.enableLRU) return; const index = this.accessOrder.indexOf(key); if (index > -1) { this.accessOrder.splice(index, 1); } this.accessOrder.push(key); } /** * Remove key from access order */ private removeFromAccessOrder(key: string): void { const index = this.accessOrder.indexOf(key); if (index > -1) { this.accessOrder.splice(index, 1); } } /** * Normalize parameters for consistent hashing */ private normalizeParams(params: unknown): unknown { if (typeof params !== "object" || params === null) { return params; } if (Array.isArray(params)) { // Recursively normalize each item in the array return params.map((item) => this.normalizeParams(item)); } // Sort object keys for consistent hashing const normalized: Record<string, unknown> = {}; const keys = Object.keys(params).sort(); for (const key of keys) { normalized[key] = this.normalizeParams((params as Record<string, unknown>)[key]); } return normalized; } /** * Update hit rate calculation */ private updateHitRate(): void { const total = this.stats.hits + this.stats.misses; this.stats.hitRate = total > 0 ? this.stats.hits / total : 0; } /** * Start periodic cleanup of expired entries * Note: This uses setInterval and is not called in test environments to avoid Jest timer issues */ private startCleanupInterval(): void { this.cleanupInterval = setInterval(() => { this.cleanupExpired(); }, 60000); // Cleanup every minute } /** * Stop the cleanup interval and clean up resources */ destroy(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } /** * Remove expired entries */ private cleanupExpired(): void { const now = Date.now(); const keysToDelete: string[] = []; for (const [key, entry] of this.cache.entries()) { if (now - entry.timestamp > entry.ttl) { keysToDelete.push(key); } } for (const key of keysToDelete) { this.cache.delete(key); this.removeFromAccessOrder(key); } this.stats.totalSize = this.cache.size; } } /** * Cache configuration presets for different data types */ export const CachePresets = { // Static data: site settings, user roles STATIC: { ttl: 4 * 60 * 60 * 1000, // 4 hours cacheControl: "public, max-age=14400", }, // Semi-static: categories, tags, user profiles SEMI_STATIC: { ttl: 2 * 60 * 60 * 1000, // 2 hours cacheControl: "public, max-age=7200", }, // Dynamic: posts, pages, comments DYNAMIC: { ttl: 15 * 60 * 1000, // 15 minutes cacheControl: "public, max-age=900", }, // Session: authentication, current user SESSION: { ttl: 30 * 60 * 1000, // 30 minutes cacheControl: "private, max-age=1800", }, // Fast changing: real-time data REALTIME: { ttl: 60 * 1000, // 1 minute cacheControl: "public, max-age=60", }, };

Implementation Reference

Latest Blog Posts

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/docdyhr/mcp-wordpress'

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