UNPKG

converse-mcp-server

Version:

Converse MCP Server - Converse with other LLMs with chat and consensus tools

367 lines (322 loc) 9.47 kB
/** * EventBus System - Job Lifecycle Event Communication (Simplified for Single-User) * * Provides event system for broadcasting job lifecycle events throughout the async execution system. * Simplified version without session management for single-user local MCP server. * Uses Node.js EventEmitter pattern to decouple components and enable structured event handling. */ import { EventEmitter } from 'events'; import { debugLog, debugError } from '../utils/console.js'; /** * Event type constants for typed event system */ export const EVENT_TYPES = { JOB_CREATED: 'job.created', JOB_UPDATED: 'job.updated', JOB_COMPLETED: 'job.completed', JOB_FAILED: 'job.failed', JOB_CANCELLED: 'job.cancelled', JOB_STARTED: 'job.started', }; /** * Custom error class for EventBus operations */ export class EventBusError extends Error { constructor(message, code = 'EVENT_BUS_ERROR') { super(message); this.name = 'EventBusError'; this.code = code; } } /** * Simplified EventBus class for single-user local MCP server * No session management needed - just job lifecycle events */ export class EventBus extends EventEmitter { /** * Create a new EventBus instance * @param {object} options - Configuration options * @param {number} options.maxListeners - Maximum listeners per event type (default: 100) * @param {number} options.maxEventHistory - Maximum events to keep in history per job (default: 100) */ constructor(options = {}) { super(); // Configuration this.maxListeners = options.maxListeners || 100; this.maxEventHistory = options.maxEventHistory || 100; // Set max listeners to prevent memory warnings this.setMaxListeners(this.maxListeners); // Event history tracking this.eventHistory = new Map(); // jobId -> Array of events // Statistics tracking this.stats = { eventsEmitted: 0, listenersAdded: 0, listenersRemoved: 0, memoryUsage: 0, }; debugLog('EventBus: Initialized (simplified for single-user)'); } /** * Emit a job created event * @param {string} jobId - Job identifier * @param {object} data - Event data * @returns {boolean} True if event was emitted */ emitJobCreated(jobId, data = {}) { return this._emitJobEvent(EVENT_TYPES.JOB_CREATED, jobId, { tool: data.tool, options: data.options, timestamp: Date.now(), ...data }); } /** * Emit a job updated event * @param {string} jobId - Job identifier * @param {object} data - Update data * @returns {boolean} True if event was emitted */ emitJobUpdated(jobId, data = {}) { return this._emitJobEvent(EVENT_TYPES.JOB_UPDATED, jobId, { progress: data.progress, status: data.status, providers: data.providers, timestamp: Date.now(), ...data }); } /** * Emit a job completed event * @param {string} jobId - Job identifier * @param {*} result - Job result * @returns {boolean} True if event was emitted */ emitJobCompleted(jobId, result) { return this._emitJobEvent(EVENT_TYPES.JOB_COMPLETED, jobId, { result, timestamp: Date.now(), duration: this._calculateDuration(jobId) }); } /** * Emit a job failed event * @param {string} jobId - Job identifier * @param {Error|string} error - Error information * @returns {boolean} True if event was emitted */ emitJobFailed(jobId, error) { const errorData = error instanceof Error ? { message: error.message, code: error.code || 'UNKNOWN_ERROR', stack: error.stack } : { message: String(error) }; return this._emitJobEvent(EVENT_TYPES.JOB_FAILED, jobId, { error: errorData, timestamp: Date.now(), duration: this._calculateDuration(jobId) }); } /** * Emit a job cancelled event * @param {string} jobId - Job identifier * @param {object} data - Event data * @returns {boolean} True if event was emitted */ emitJobCancelled(jobId, data = {}) { return this._emitJobEvent(EVENT_TYPES.JOB_CANCELLED, jobId, { reason: data.reason || 'User cancelled', partial_result: data.partial_result, timestamp: Date.now(), ...data }); } /** * Emit a job started event * @param {string} jobId - Job identifier * @param {object} data - Event data * @returns {boolean} True if event was emitted */ emitJobStarted(jobId, data = {}) { return this._emitJobEvent(EVENT_TYPES.JOB_STARTED, jobId, { tool: data.tool, timestamp: Date.now(), ...data }); } /** * Register a listener for job events * @param {string} eventType - Event type to listen for * @param {Function} listener - Callback function * @returns {Function} Unsubscribe function */ onJobEvent(eventType, listener) { if (!Object.values(EVENT_TYPES).includes(eventType)) { throw new EventBusError( `Invalid event type: ${eventType}`, 'INVALID_EVENT_TYPE' ); } if (typeof listener !== 'function') { throw new EventBusError( 'Listener must be a function', 'INVALID_LISTENER' ); } this.on(eventType, listener); this.stats.listenersAdded++; // Return unsubscribe function return () => { this.off(eventType, listener); this.stats.listenersRemoved++; }; } /** * Get event history for a specific job * @param {string} jobId - Job identifier * @returns {Array} Array of events for the job */ getJobHistory(jobId) { return this.eventHistory.get(jobId) || []; } /** * Clear event history for a specific job * @param {string} jobId - Job identifier */ clearJobHistory(jobId) { this.eventHistory.delete(jobId); } /** * Get current statistics * @returns {object} Current EventBus statistics */ getStats() { return { ...this.stats, totalListeners: this.listenerCount(), eventHistorySize: this.eventHistory.size, memoryUsage: this._calculateMemoryUsage() }; } /** * Internal method to emit job events with validation and history tracking * @param {string} eventType - Event type constant * @param {string} jobId - Job identifier * @param {object} data - Event data * @returns {boolean} True if event was emitted * @private */ _emitJobEvent(eventType, jobId, data) { try { // Validate parameters if (!jobId || typeof jobId !== 'string') { throw new EventBusError( 'Invalid job ID: must be a non-empty string', 'INVALID_JOB_ID' ); } // Create event object const event = { type: eventType, jobId, data, timestamp: data.timestamp || Date.now() }; // Add to history this._addToHistory(jobId, event); // Emit event const hasListeners = this.emit(eventType, event); // Update statistics this.stats.eventsEmitted++; debugLog(`EventBus: Emitted ${eventType} for job ${jobId}`); return hasListeners; } catch (error) { debugError(`EventBus: Failed to emit event ${eventType} for job ${jobId}:`, error); if (error instanceof EventBusError) { throw error; } throw new EventBusError( `Failed to emit event: ${error.message}`, 'EMISSION_ERROR' ); } } /** * Add event to job history with size limit * @param {string} jobId - Job identifier * @param {object} event - Event object * @private */ _addToHistory(jobId, event) { if (!this.eventHistory.has(jobId)) { this.eventHistory.set(jobId, []); } const history = this.eventHistory.get(jobId); history.push(event); // Limit history size if (history.length > this.maxEventHistory) { history.shift(); } } /** * Calculate duration since job creation * @param {string} jobId - Job identifier * @returns {number|null} Duration in milliseconds or null * @private */ _calculateDuration(jobId) { const history = this.eventHistory.get(jobId); if (!history || history.length === 0) { return null; } const createEvent = history.find(e => e.type === EVENT_TYPES.JOB_CREATED); if (!createEvent) { return null; } return Date.now() - createEvent.timestamp; } /** * Calculate approximate memory usage * @returns {number} Approximate memory usage in bytes * @private */ _calculateMemoryUsage() { // Rough estimation of memory usage let totalSize = 0; for (const [jobId, events] of this.eventHistory) { totalSize += jobId.length * 2; // UTF-16 string size totalSize += JSON.stringify(events).length; } return totalSize; } /** * Cleanup resources */ destroy() { this.removeAllListeners(); this.eventHistory.clear(); debugLog('EventBus: Destroyed and cleaned up resources'); } } // Singleton instance for global event bus let globalEventBus = null; /** * Get global EventBus instance (singleton) * @returns {EventBus} Global EventBus instance */ export function getEventBus() { if (!globalEventBus) { globalEventBus = new EventBus(); } return globalEventBus; } /** * Reset global EventBus instance (mainly for testing) */ export function resetEventBus() { if (globalEventBus) { globalEventBus.destroy(); globalEventBus = null; } } export default EventBus;