@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
271 lines • 9.27 kB
JavaScript
/**
* 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