/** * v2 Channel adapter registry. * * Channels self-register on import. The host calls initChannelAdapters() at startup * to instantiate and set up all registered adapters. */ import { NetworkError } from '@chat-adapter/shared'; import type { ChannelAdapter, ChannelRegistration, ChannelSetup } from './adapter.js'; import { log } from '../log.js'; const SETUP_RETRY_DELAYS_MS = [2000, 5000, 10000]; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const registry = new Map(); const activeAdapters = new Map(); /** Register a channel adapter factory. Called by channel modules on import. */ export function registerChannelAdapter(name: string, registration: ChannelRegistration): void { registry.set(name, registration); } /** Get a live adapter by channel type. */ export function getChannelAdapter(channelType: string): ChannelAdapter | undefined { return activeAdapters.get(channelType); } /** Get all active adapters. */ export function getActiveAdapters(): ChannelAdapter[] { return [...activeAdapters.values()]; } /** Get all registered channel names. */ export function getRegisteredChannelNames(): string[] { return [...registry.keys()]; } /** Get container config for a channel (used by container-runner for additional mounts/env). */ export function getChannelContainerConfig(name: string): ChannelRegistration['containerConfig'] { return registry.get(name)?.containerConfig; } /** * Instantiate and set up all registered channel adapters. * Skips adapters that return null (missing credentials). */ export async function initChannelAdapters(setupFn: (adapter: ChannelAdapter) => ChannelSetup): Promise { for (const [name, registration] of registry) { try { const adapter = await registration.factory(); if (!adapter) { log.warn('Channel credentials missing, skipping', { channel: name }); continue; } const setup = setupFn(adapter); // Transient network failures during adapter init (e.g. Telegram deleteWebhook // hitting a DNS hiccup at boot) would otherwise leave the channel permanently // dead until manual restart. Retry only on NetworkError so misconfigs (bad // tokens, etc.) still fail fast. let attempt = 0; while (true) { try { await adapter.setup(setup); break; } catch (err) { if (err instanceof NetworkError && attempt < SETUP_RETRY_DELAYS_MS.length) { const delay = SETUP_RETRY_DELAYS_MS[attempt]!; log.warn('Channel adapter setup failed with network error, retrying', { channel: name, attempt: attempt + 1, delayMs: delay, err: err.message, }); await sleep(delay); attempt += 1; continue; } throw err; } } activeAdapters.set(adapter.channelType, adapter); log.info('Channel adapter started', { channel: name, type: adapter.channelType }); } catch (err) { log.error('Failed to start channel adapter', { channel: name, err }); } } } /** Tear down all active adapters. */ export async function teardownChannelAdapters(): Promise { for (const [name, adapter] of activeAdapters) { try { await adapter.teardown(); log.info('Channel adapter stopped', { channel: name }); } catch (err) { log.error('Failed to stop channel adapter', { channel: name, err }); } } activeAdapters.clear(); }