UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

372 lines (371 loc) • 12.4 kB
"use strict"; /** * @fileoverview Cache Service Interface and Implementation - Infrastructure Layer * @version 1.0.0 * @since 2025-07-29 * @lastUpdated 2025-07-29 * @module CacheService Infrastructure Implementation * @description High-performance caching service for Clean Architecture with multiple * backend support (in-memory, Redis, etc.) and advanced features like * TTL, cache invalidation, and performance monitoring. * @contributors Claude Code Agent * @dependencies Clean Architecture infrastructure contracts * @requirements REQ-ARCH-001 (Clean Architecture Infrastructure Layer) * @testCoverage Unit tests for cache operations and performance */ Object.defineProperty(exports, "__esModule", { value: true }); exports.cacheServiceFactory = exports.CacheServiceFactory = exports.InMemoryCacheService = void 0; /** * In-Memory Cache Service Implementation * * @description High-performance in-memory cache implementation suitable for * single-process applications and development environments. * Includes LRU eviction, TTL support, and comprehensive statistics. */ class InMemoryCacheService { constructor(config = {}) { this.cache = new Map(); this.timers = new Map(); this.config = { defaultTTL: 3600, maxKeys: 10000, keyPrefix: '', enableCompression: false, enableStats: true, connection: {}, ...config, }; this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0, totalKeys: 0, memoryUsage: 0, hitRate: 0, averageResponseTime: 0, }; } async start() { // Setup periodic cleanup for expired entries setInterval(() => { this.cleanupExpired(); }, 60000); // Run every minute } async stop() { // Clear all timers for (const timer of this.timers.values()) { clearTimeout(timer); } this.timers.clear(); this.cache.clear(); } async set(key, value, ttl) { const startTime = Date.now(); const fullKey = this.getFullKey(key); const expiresIn = ttl || this.config.defaultTTL; // Check if we need to evict entries if (this.cache.size >= this.config.maxKeys && !this.cache.has(fullKey)) { await this.evictLRU(); } // Clear existing timer if key exists const existingTimer = this.timers.get(fullKey); if (existingTimer) { clearTimeout(existingTimer); } // Create cache entry const entry = { value, key: fullKey, ttl: expiresIn, createdAt: new Date(), lastAccessed: new Date(), accessCount: 0, size: this.calculateSize(value), }; this.cache.set(fullKey, entry); // Set expiration timer const timer = setTimeout(() => { this.cache.delete(fullKey); this.timers.delete(fullKey); }, expiresIn * 1000); this.timers.set(fullKey, timer); // Update statistics if (this.config.enableStats) { this.stats.sets++; this.stats.totalKeys = this.cache.size; this.updateResponseTime(Date.now() - startTime); } } async get(key) { const startTime = Date.now(); const fullKey = this.getFullKey(key); const entry = this.cache.get(fullKey); if (!entry) { if (this.config.enableStats) { this.stats.misses++; this.updateHitRate(); this.updateResponseTime(Date.now() - startTime); } return null; } // Update access information entry.lastAccessed = new Date(); entry.accessCount++; if (this.config.enableStats) { this.stats.hits++; this.updateHitRate(); this.updateResponseTime(Date.now() - startTime); } return entry.value; } async has(key) { const fullKey = this.getFullKey(key); return this.cache.has(fullKey); } async delete(key) { const fullKey = this.getFullKey(key); const existed = this.cache.has(fullKey); if (existed) { // Clear timer const timer = this.timers.get(fullKey); if (timer) { clearTimeout(timer); this.timers.delete(fullKey); } this.cache.delete(fullKey); if (this.config.enableStats) { this.stats.deletes++; this.stats.totalKeys = this.cache.size; } } return existed; } async clear() { // Clear all timers for (const timer of this.timers.values()) { clearTimeout(timer); } this.timers.clear(); this.cache.clear(); if (this.config.enableStats) { this.stats.totalKeys = 0; } } async setMany(entries) { await Promise.all(entries.map(entry => this.set(entry.key, entry.value, entry.ttl))); } async getMany(keys) { const result = new Map(); await Promise.all(keys.map(async (key) => { const value = await this.get(key); if (value !== null) { result.set(key, value); } })); return result; } async deleteMany(keys) { let deleted = 0; await Promise.all(keys.map(async (key) => { const wasDeleted = await this.delete(key); if (wasDeleted) { deleted++; } })); return deleted; } async keys(pattern) { const allKeys = Array.from(this.cache.keys()); if (!pattern) { return allKeys.map(key => this.removeKeyPrefix(key)); } // Simple pattern matching (supports * wildcard) const regex = new RegExp(pattern.replace(/\*/g, '.*')); return allKeys .filter(key => regex.test(this.removeKeyPrefix(key))) .map(key => this.removeKeyPrefix(key)); } async increment(key, by = 1, ttl) { const currentValue = (await this.get(key)) || 0; const newValue = currentValue + by; await this.set(key, newValue, ttl); return newValue; } async decrement(key, by = 1, ttl) { return this.increment(key, -by, ttl); } async expire(key, ttl) { const fullKey = this.getFullKey(key); const entry = this.cache.get(fullKey); if (!entry) { return false; } // Clear existing timer const existingTimer = this.timers.get(fullKey); if (existingTimer) { clearTimeout(existingTimer); } // Set new timer entry.ttl = ttl; const timer = setTimeout(() => { this.cache.delete(fullKey); this.timers.delete(fullKey); }, ttl * 1000); this.timers.set(fullKey, timer); return true; } async ttl(key) { const fullKey = this.getFullKey(key); const entry = this.cache.get(fullKey); if (!entry) { return -2; // Key doesn't exist } const timer = this.timers.get(fullKey); if (!timer) { return -1; // Key exists but has no TTL } // Calculate remaining time (approximation) const elapsed = Date.now() - entry.createdAt.getTime(); const remaining = Math.max(0, entry.ttl * 1000 - elapsed); return Math.ceil(remaining / 1000); } async getStats() { const memoryUsage = Array.from(this.cache.values()).reduce((total, entry) => total + entry.size, 0); return { ...this.stats, totalKeys: this.cache.size, memoryUsage, }; } async getEntry(key) { const fullKey = this.getFullKey(key); const entry = this.cache.get(fullKey); if (!entry) { return null; } return { ...entry }; } async flush() { let flushed = 0; const now = Date.now(); for (const [key, entry] of this.cache.entries()) { const elapsed = now - entry.createdAt.getTime(); if (elapsed >= entry.ttl * 1000) { this.cache.delete(key); const timer = this.timers.get(key); if (timer) { clearTimeout(timer); this.timers.delete(key); } flushed++; } } if (this.config.enableStats) { this.stats.totalKeys = this.cache.size; } return flushed; } /** * Private helper methods */ getFullKey(key) { return this.config.keyPrefix ? `${this.config.keyPrefix}:${key}` : key; } removeKeyPrefix(fullKey) { if (!this.config.keyPrefix) { return fullKey; } const prefix = `${this.config.keyPrefix}:`; return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey; } calculateSize(value) { // Simple size calculation (could be enhanced with proper serialization) try { return JSON.stringify(value).length * 2; // Rough estimate for UTF-16 } catch (_a) { return 100; // Default size for non-serializable objects } } async evictLRU() { let oldestEntry = null; let oldestKey = null; // Find least recently used entry for (const [key, entry] of this.cache.entries()) { if (!oldestEntry || entry.lastAccessed < oldestEntry.lastAccessed) { oldestEntry = entry; oldestKey = key; } } if (oldestKey) { await this.delete(this.removeKeyPrefix(oldestKey)); if (this.config.enableStats) { this.stats.evictions++; } } } cleanupExpired() { const now = Date.now(); const expiredKeys = []; for (const [key, entry] of this.cache.entries()) { const elapsed = now - entry.createdAt.getTime(); if (elapsed >= entry.ttl * 1000) { expiredKeys.push(key); } } for (const key of expiredKeys) { this.cache.delete(key); const timer = this.timers.get(key); if (timer) { clearTimeout(timer); this.timers.delete(key); } } if (this.config.enableStats && expiredKeys.length > 0) { this.stats.totalKeys = this.cache.size; } } updateHitRate() { const totalRequests = this.stats.hits + this.stats.misses; this.stats.hitRate = totalRequests > 0 ? this.stats.hits / totalRequests : 0; } updateResponseTime(time) { this.stats.averageResponseTime = (this.stats.averageResponseTime + time) / 2; } } exports.InMemoryCacheService = InMemoryCacheService; /** * Cache Service Factory */ class CacheServiceFactory { static getInstance() { if (!CacheServiceFactory.instance) { CacheServiceFactory.instance = new CacheServiceFactory(); } return CacheServiceFactory.instance; } createInMemory(config) { return new InMemoryCacheService(config); } createFromEnvironment() { const cacheType = process.env.CACHE_TYPE || 'in-memory'; switch (cacheType.toLowerCase()) { case 'in-memory': return this.createInMemory({ defaultTTL: parseInt(process.env.CACHE_DEFAULT_TTL || '3600'), maxKeys: parseInt(process.env.CACHE_MAX_KEYS || '10000'), keyPrefix: process.env.CACHE_KEY_PREFIX || '', enableStats: process.env.CACHE_ENABLE_STATS !== 'false', }); default: throw new Error(`Unsupported cache type: ${cacheType}`); } } } exports.CacheServiceFactory = CacheServiceFactory; /** * Export factory instance */ exports.cacheServiceFactory = CacheServiceFactory.getInstance();