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.

481 lines (480 loc) • 17.1 kB
"use strict"; /** * @fileoverview In-Memory Event Bus Implementation - Infrastructure Layer * @version 1.0.0 * @since 2025-07-29 * @lastUpdated 2025-07-29 * @module InMemoryEventBus Infrastructure Implementation * @description High-performance in-memory event bus implementation for Clean Architecture. * Provides synchronous and asynchronous event processing with retry logic, * middleware support, and comprehensive error handling. * @contributors Claude Code Agent * @dependencies Domain events, event handlers, event bus interface * @requirements REQ-ARCH-001 (Clean Architecture Infrastructure Layer) * @testCoverage Unit tests for event publishing, subscription, and error handling */ Object.defineProperty(exports, "__esModule", { value: true }); exports.InMemoryDeadLetterQueue = exports.InMemoryEventBus = void 0; /** * In-Memory Event Bus Implementation * * @description High-performance, event bus implementation * that runs entirely in memory. Suitable for single-process applications * and development environments. Provides comprehensive event processing * features including retry logic, middleware, and dead letter queue. * * @example * ```typescript * const eventBus = new InMemoryEventBus({ * maxRetries: 3, * retryDelayMs: 1000, * enableDeadLetterQueue: true * }); * * // Start the event bus * await eventBus.start(); * * // Subscribe to events * eventBus.subscribe(QuizCreatedEvent, new QuizAnalyticsHandler(), { * priority: 10, * parallel: true * }); * * // Publish events * await eventBus.publish(new QuizCreatedEvent(quizId, title)); * ``` * * @since 2025-07-29 * @author Claude Code Agent * @requirements REQ-ARCH-001 (Clean Architecture Infrastructure Layer) */ class InMemoryEventBus { constructor(config = {}) { this.config = config; this.subscriptions = new Map(); this.subscriptionIndex = new Map(); this.middleware = []; this.deadLetterQueue = []; this.isActive = false; this.processingQueue = []; this.processing = false; this.stats = { totalEventsPublished: 0, totalEventsProcessed: 0, totalEventsFailed: 0, activeSubscriptions: 0, averageProcessingTime: 0, }; // Set default configuration this.config = { maxRetries: 3, retryDelayMs: 1000, enableDeadLetterQueue: true, maxConcurrency: 10, enablePersistence: false, eventTimeoutMs: 30000, ...config, }; } /** * Start the event bus */ async start() { if (this.isActive) { return; } this.isActive = true; this.startProcessingLoop(); } /** * Stop the event bus gracefully */ async stop() { this.isActive = false; await this.flush(); } /** * Check if the event bus is running */ isRunning() { return this.isActive; } /** * Publish a domain event */ async publish(event, options = {}) { if (!this.isActive) { throw new Error('Event bus is not running. Call start() first.'); } this.stats.totalEventsPublished++; this.stats.lastEventTimestamp = new Date(); // Apply middleware preprocessing let processedEvent = event; for (const middleware of this.middleware) { if (middleware.beforePublish) { processedEvent = (await middleware.beforePublish(processedEvent, options)); } } if (options.waitForCompletion) { // Process immediately and wait await this.processEvent(processedEvent, options); } else { // Queue for background processing this.processingQueue.push({ event: processedEvent, options }); this.processQueueAsync(); } } /** * Publish multiple events */ async publishMany(events, options = {}) { if (options.waitForCompletion) { // Process all events in parallel if requested await Promise.all(events.map(event => this.publish(event, options))); } else { // Add all to queue for (const event of events) { await this.publish(event, options); } } } /** * Subscribe to events */ subscribe(eventType, handler, options = {}) { const eventTypeName = eventType.name; const subscriptionId = `${eventTypeName}_${Date.now()}_${Math.random()}`; const subscription = { id: subscriptionId, eventType: eventTypeName, handler, options: { priority: 0, parallel: true, ...options, }, createdAt: new Date(), totalEvents: 0, }; // Add to subscriptions map if (!this.subscriptions.has(eventTypeName)) { this.subscriptions.set(eventTypeName, []); } this.subscriptions.get(eventTypeName).push(subscription); // Sort by priority (higher priority first) this.subscriptions .get(eventTypeName) .sort((a, b) => (b.options.priority || 0) - (a.options.priority || 0)); // Add to index for quick lookup this.subscriptionIndex.set(subscriptionId, subscription); this.stats.activeSubscriptions++; return subscriptionId; } /** * Subscribe to multiple event types */ subscribeToMany(eventTypes, handler, options = {}) { return eventTypes.map(eventType => this.subscribe(eventType, handler, options)); } /** * Unsubscribe from events */ unsubscribe(subscriptionId) { const subscription = this.subscriptionIndex.get(subscriptionId); if (!subscription) { return; } // Remove from subscriptions map const eventSubscriptions = this.subscriptions.get(subscription.eventType); if (eventSubscriptions) { const index = eventSubscriptions.findIndex(s => s.id === subscriptionId); if (index >= 0) { eventSubscriptions.splice(index, 1); if (eventSubscriptions.length === 0) { this.subscriptions.delete(subscription.eventType); } } } // Remove from index this.subscriptionIndex.delete(subscriptionId); this.stats.activeSubscriptions--; } /** * Unsubscribe all handlers for event type */ unsubscribeAll(eventType) { const eventTypeName = eventType.name; const subscriptions = this.subscriptions.get(eventTypeName) || []; // Remove from index for (const subscription of subscriptions) { this.subscriptionIndex.delete(subscription.id); this.stats.activeSubscriptions--; } // Remove from subscriptions map this.subscriptions.delete(eventTypeName); } /** * Clear all subscriptions */ clear() { this.subscriptions.clear(); this.subscriptionIndex.clear(); this.stats.activeSubscriptions = 0; } /** * Get event bus statistics */ getStats() { return { ...this.stats }; } /** * Wait for all pending events to be processed */ async flush() { while (this.processingQueue.length > 0 || this.processing) { await new Promise(resolve => setTimeout(resolve, 10)); } } /** * Add middleware */ addMiddleware(middleware) { this.middleware.push(middleware); } /** * Remove middleware */ removeMiddleware(middleware) { const index = this.middleware.indexOf(middleware); if (index >= 0) { this.middleware.splice(index, 1); } } /** * Get dead letter queue */ getDeadLetterQueue() { return [...this.deadLetterQueue]; } /** * Clear dead letter queue */ clearDeadLetterQueue() { this.deadLetterQueue = []; } /** * Retry failed event from dead letter queue */ async retryFailedEvent(eventIndex) { if (eventIndex < 0 || eventIndex >= this.deadLetterQueue.length) { throw new Error('Invalid event index'); } const failedEvent = this.deadLetterQueue[eventIndex]; this.deadLetterQueue.splice(eventIndex, 1); // Retry the event await this.publish(failedEvent.event); } /** * Private Methods */ async processEvent(event, options = {}) { const eventTypeName = event.constructor.name; const subscriptions = this.subscriptions.get(eventTypeName) || []; if (subscriptions.length === 0) { return; // No handlers for this event type } const startTime = Date.now(); const results = []; // Filter subscriptions const activeSubscriptions = subscriptions.filter(subscription => { if (subscription.options.filter) { return subscription.options.filter(event); } return true; }); // Group by parallel/sequential processing const parallelHandlers = activeSubscriptions.filter(s => s.options.parallel); const sequentialHandlers = activeSubscriptions.filter(s => !s.options.parallel); try { // Process parallel handlers if (parallelHandlers.length > 0) { const parallelResults = await Promise.allSettled(parallelHandlers.map(subscription => this.executeHandler(event, subscription))); results.push(...parallelResults.map((result, index) => ({ success: result.status === 'fulfilled', error: result.status === 'rejected' ? result.reason : undefined, duration: Date.now() - startTime, handlerName: parallelHandlers[index].handler.constructor.name, }))); } // Process sequential handlers for (const subscription of sequentialHandlers) { try { await this.executeHandler(event, subscription); results.push({ success: true, duration: Date.now() - startTime, handlerName: subscription.handler.constructor.name, }); } catch (error) { results.push({ success: false, error: error instanceof Error ? error : new Error(String(error)), duration: Date.now() - startTime, handlerName: subscription.handler.constructor.name, }); if (!options.continueOnError) { break; } } } // Update statistics const successful = results.filter(r => r.success).length; const failed = results.filter(r => !r.success).length; this.stats.totalEventsProcessed += successful; this.stats.totalEventsFailed += failed; const totalDuration = results.reduce((sum, r) => sum + r.duration, 0); this.stats.averageProcessingTime = (this.stats.averageProcessingTime + totalDuration / results.length) / 2; } catch (error) { this.stats.totalEventsFailed++; throw error; } } async executeHandler(event, subscription) { var _a, _b, _c, _d; const maxRetries = (_b = (_a = subscription.options.retryConfig) === null || _a === void 0 ? void 0 : _a.maxRetries) !== null && _b !== void 0 ? _b : this.config.maxRetries; const retryDelay = (_d = (_c = subscription.options.retryConfig) === null || _c === void 0 ? void 0 : _c.delayMs) !== null && _d !== void 0 ? _d : this.config.retryDelayMs; let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { // Apply middleware preprocessing for (const middleware of this.middleware) { if (middleware.beforeHandle) { await middleware.beforeHandle(event, subscription.handler); } } // Execute handler with timeout const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Handler timeout')), this.config.eventTimeoutMs); }); const handlerPromise = subscription.handler.handle(event); await Promise.race([handlerPromise, timeoutPromise]); // Apply middleware postprocessing for (const middleware of this.middleware) { if (middleware.afterHandle) { await middleware.afterHandle(event, subscription.handler); } } // Update subscription statistics subscription.totalEvents++; subscription.lastEventAt = new Date(); return; // Success } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Apply middleware error handling for (const middleware of this.middleware) { if (middleware.onError) { await middleware.onError(event, subscription.handler, lastError); } } // Try handler's own error handling if (subscription.handler.onError) { try { await subscription.handler.onError(event, lastError); } catch (handlerError) { // Handler error handling failed, continue with retry logic } } // Wait before retry (except on last attempt) if (attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, retryDelay)); } } } // All retries failed if (lastError && this.config.enableDeadLetterQueue) { this.deadLetterQueue.push({ event, error: lastError, attempts: maxRetries + 1, failedAt: new Date(), subscriptionId: subscription.id, }); } throw lastError || new Error('Handler execution failed'); } startProcessingLoop() { // Start background processing of queued events this.processQueueAsync(); } async processQueueAsync() { if (this.processing || !this.isActive) { return; } this.processing = true; try { while (this.processingQueue.length > 0 && this.isActive) { const { event, options } = this.processingQueue.shift(); try { await this.processEvent(event, options); } catch (error) { // Event processing failed, but continue with queue console.error('Event processing failed:', error); } } } finally { this.processing = false; } } } exports.InMemoryEventBus = InMemoryEventBus; /** * Simple Dead Letter Queue Implementation */ class InMemoryDeadLetterQueue { constructor() { this.failedEvents = []; } async add(event, error, attempts) { this.failedEvents.push({ event, error, attempts, failedAt: new Date(), }); } async getFailedEvents(limit) { if (limit) { return this.failedEvents.slice(0, limit); } return [...this.failedEvents]; } async retry(eventId) { // Implementation would need event ID tracking throw new Error('Retry by ID not implemented in basic version'); } async clear() { this.failedEvents = []; } async getStats() { const events = this.failedEvents; return { totalEvents: events.length, oldestEvent: events.length > 0 ? events.reduce((oldest, current) => current.failedAt < oldest.failedAt ? current : oldest).failedAt : undefined, newestEvent: events.length > 0 ? events.reduce((newest, current) => current.failedAt > newest.failedAt ? current : newest).failedAt : undefined, }; } } exports.InMemoryDeadLetterQueue = InMemoryDeadLetterQueue;