/** * Channel registry — manages adapter lifecycle and message routing. * * The ChannelRegistry holds all registered channel adapters and routes * inbound messages through a single MessageHandler. Each adapter's * onMessage callback is wired at registration time so that messages * flow through handleInbound → messageHandler → reply. */ import type { ChannelAdapter, InboundMessage, MessageHandler, OutboundMessage, } from './types.js'; export class ChannelRegistry { private adapters: Map = new Map(); private messageHandler?: MessageHandler; /** Register an adapter. Throws if name already registered. */ register(adapter: ChannelAdapter): void { if (this.adapters.has(adapter.name)) { throw new Error(`Channel adapter '${adapter.name}' is already registered`); } // Wire the adapter's onMessage to route through our messageHandler adapter.onMessage((msg) => this.handleInbound(msg)); this.adapters.set(adapter.name, adapter); } /** Unregister an adapter by name. Calls disconnect() if connected. */ async unregister(name: string): Promise { const adapter = this.adapters.get(name); if (!adapter) {return;} if (adapter.status === 'connected' || adapter.status === 'connecting') { await adapter.disconnect(); } this.adapters.delete(name); } /** Get an adapter by name. */ get(name: string): ChannelAdapter | undefined { return this.adapters.get(name); } /** List all registered adapters. */ list(): ChannelAdapter[] { return Array.from(this.adapters.values()); } /** Set the message handler that all adapters route inbound messages to. */ setMessageHandler(handler: MessageHandler): void { this.messageHandler = handler; } /** Start all registered adapters. Logs errors per adapter, doesn't throw. */ async startAll(): Promise { const adapters = Array.from(this.adapters.values()); const results = await Promise.allSettled( adapters.map((a) => a.connect()), ); for (const [i, result] of results.entries()) { if (result.status === 'rejected') { console.error( `Failed to start channel '${adapters[i].name}':`, result.reason, ); } } } /** Stop all registered adapters. */ async stopAll(): Promise { const adapters = Array.from(this.adapters.values()); const results = await Promise.allSettled( adapters.map((a) => a.disconnect()), ); for (const [i, result] of results.entries()) { if (result.status === 'rejected') { console.error( `Failed to stop channel '${adapters[i].name}':`, result.reason, ); } } } /** Internal: route an inbound message to the message handler. */ private handleInbound(msg: InboundMessage): void { if (!this.messageHandler) { console.warn(`No message handler set, dropping message from '${msg.channel}'`); return; } const adapter = this.adapters.get(msg.channel); if (!adapter) { console.warn(`Unknown channel '${msg.channel}' in inbound message`); return; } // Create a reply function bound to this message's channel and sender const reply = async (response: OutboundMessage): Promise => { await adapter.send(msg.senderId, response); }; // Fire and forget — errors are logged, not propagated this.messageHandler(msg, reply).catch((err: unknown) => { console.error(`Error handling message from '${msg.channel}':`, err); }); } }