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