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
JavaScript
"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();