UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

271 lines 9.27 kB
/** * Implementation of the Adapter Registry for managing debug adapters * * @since 2.0.0 */ import { EventEmitter } from 'events'; import { AdapterNotFoundError, DuplicateRegistrationError, FactoryValidationError } from './adapter-registry-interface.js'; /** * Default registry configuration */ const DEFAULT_CONFIG = { validateOnRegister: true, allowOverride: false, maxInstancesPerLanguage: 10, autoDispose: true, autoDisposeTimeout: 300000, // 5 minutes }; /** * Implementation of the adapter registry */ export class AdapterRegistry extends EventEmitter { factories = new Map(); activeAdapters = new Map(); config; disposeTimers = new Map(); constructor(config = {}) { super(); this.config = { ...DEFAULT_CONFIG, ...config }; } /** * Register a new adapter factory for a language */ async register(language, factory) { // Check for duplicate registration if (this.factories.has(language) && !this.config.allowOverride) { throw new DuplicateRegistrationError(language); } // Validate factory if configured if (this.config.validateOnRegister) { const validationResult = await factory.validate(); if (!validationResult.valid) { throw new FactoryValidationError(language, validationResult); } } // Register the factory this.factories.set(language, factory); this.emit('factoryRegistered', language, factory.getMetadata()); } /** * Unregister an adapter factory */ unregister(language) { const factory = this.factories.get(language); if (!factory) { return false; } // Dispose all active adapters for this language const activeSet = this.activeAdapters.get(language); if (activeSet) { for (const adapter of activeSet) { adapter.dispose().catch(err => { this.emit('error', new Error(`Failed to dispose adapter: ${err.message}`)); }); } this.activeAdapters.delete(language); } // Clear any dispose timers const timer = this.disposeTimers.get(language); if (timer) { clearTimeout(timer); this.disposeTimers.delete(language); } // Remove the factory this.factories.delete(language); this.emit('factoryUnregistered', language); return true; } /** * Create a new adapter instance for the specified language */ async create(language, config) { const factory = this.factories.get(language); if (!factory) { throw new AdapterNotFoundError(language, this.getSupportedLanguages()); } // Check instance limit const activeSet = this.activeAdapters.get(language) || new Set(); if (activeSet.size >= this.config.maxInstancesPerLanguage) { throw new Error(`Maximum adapter instances (${this.config.maxInstancesPerLanguage}) reached for language: ${language}`); } // Create dependencies for the adapter const dependencies = await this.createDependencies(config); // Create the adapter const adapter = factory.createAdapter(dependencies); // Initialize the adapter await adapter.initialize(); // Track the active adapter if (!this.activeAdapters.has(language)) { this.activeAdapters.set(language, new Set()); } this.activeAdapters.get(language).add(adapter); // Set up auto-dispose if configured if (this.config.autoDispose) { this.setupAutoDispose(language, adapter); } // Listen for adapter disposal adapter.once('disposed', () => { const set = this.activeAdapters.get(language); if (set) { set.delete(adapter); if (set.size === 0) { this.activeAdapters.delete(language); } } }); this.emit('adapterCreated', language, adapter); return adapter; } /** * Get list of all supported languages */ getSupportedLanguages() { return Array.from(this.factories.keys()); } /** * Check if a language is supported */ isLanguageSupported(language) { return this.factories.has(language); } /** * Get metadata about a registered adapter */ getAdapterInfo(language) { const factory = this.factories.get(language); if (!factory) { return undefined; } const metadata = factory.getMetadata(); const activeSet = this.activeAdapters.get(language); return { ...metadata, language, available: true, activeInstances: activeSet?.size || 0, registeredAt: new Date(), // In a real implementation, track this }; } /** * Get all registered adapter information */ getAllAdapterInfo() { const result = new Map(); for (const [language] of this.factories) { const info = this.getAdapterInfo(language); if (info) { result.set(language, info); } } return result; } /** * Dispose all created adapters and clear registry */ async disposeAll() { const disposePromises = []; // Dispose all active adapters for (const [language, activeSet] of this.activeAdapters) { for (const adapter of activeSet) { disposePromises.push(adapter.dispose().catch(err => { this.emit('error', new Error(`Failed to dispose adapter for ${language}: ${err.message}`)); })); } } // Clear all dispose timers for (const timer of this.disposeTimers.values()) { clearTimeout(timer); } this.disposeTimers.clear(); // Wait for all disposals to complete await Promise.all(disposePromises); // Clear all tracking this.activeAdapters.clear(); this.factories.clear(); this.emit('registryDisposed'); } /** * Get count of active adapter instances */ getActiveAdapterCount() { let count = 0; for (const activeSet of this.activeAdapters.values()) { count += activeSet.size; } return count; } /** * Create dependencies for adapter creation */ async createDependencies(config) { // In a real implementation, these would be injected or created from a container // For now, we'll import the implementations directly const { createProductionDependencies } = await import('../container/dependencies.js'); const deps = createProductionDependencies({ logLevel: 'debug', logFile: `${config.logDir}/${config.sessionId}.log` }); return { fileSystem: deps.fileSystem, logger: deps.logger, environment: deps.environment, processLauncher: deps.processLauncher, networkManager: deps.networkManager, }; } /** * Set up auto-dispose for an adapter */ setupAutoDispose(language, adapter) { // Clear any existing timer for this language const existingTimer = this.disposeTimers.get(`${language}-${adapter}`); if (existingTimer) { clearTimeout(existingTimer); } // Listen for adapter state changes adapter.on('stateChanged', (oldState, newState) => { if (newState === 'disconnected' || newState === 'error') { // Start dispose timer const timer = setTimeout(() => { adapter.dispose().catch(err => { this.emit('error', new Error(`Auto-dispose failed: ${err.message}`)); }); }, this.config.autoDisposeTimeout); this.disposeTimers.set(`${language}-${adapter}`, timer); } else if (newState === 'connected' || newState === 'debugging') { // Cancel dispose timer if adapter becomes active again const timer = this.disposeTimers.get(`${language}-${adapter}`); if (timer) { clearTimeout(timer); this.disposeTimers.delete(`${language}-${adapter}`); } } }); } } /** * Create a singleton instance of the adapter registry */ let registryInstance = null; /** * Get the singleton adapter registry instance */ export function getAdapterRegistry(config) { if (!registryInstance) { registryInstance = new AdapterRegistry(config); } return registryInstance; } /** * Reset the singleton instance (mainly for testing) */ export function resetAdapterRegistry() { if (registryInstance) { registryInstance.disposeAll().catch(() => { // Ignore errors during reset }); registryInstance = null; } } //# sourceMappingURL=adapter-registry.js.map