import { noopLogger, type AviationLogger } from '../logger'; import type { MediaPlugin, PluginContext, PluginStatus } from './MediaPlugin'; export interface PluginInstallFailure { name: string; error: unknown; } export interface PluginRegistry { /** Installed plugins in install order; a plugin sees itself during setup(). */ readonly installed: readonly MediaPlugin[]; /** * Installs in array order. Registers the plugin before its own setup() * runs so siblings observe it via `installed`. A throwing setup tears the * plugin back down best-effort; it aborts installation only when the * plugin is `critical`. */ install(plugin: MediaPlugin, context: PluginContext): Promise; /** Tears down installed plugins in reverse order. Never throws. */ teardownAll( contextFor: (plugin: MediaPlugin) => PluginContext ): Promise; status(name: string): PluginStatus; reset(): void; } export function createPluginRegistry( log: AviationLogger = noopLogger ): PluginRegistry { const installedPlugins: MediaPlugin[] = []; const statuses = new Map(); return { installed: installedPlugins, async install(plugin, context): Promise { if (installedPlugins.some((p) => p.name === plugin.name)) { log.warn('setup', `duplicate plugin '${plugin.name}' ignored`); return; } installedPlugins.push(plugin); try { await plugin.setup(context); } catch (e) { statuses.set(plugin.name, 'failed'); const index = installedPlugins.indexOf(plugin); if (index >= 0) installedPlugins.splice(index, 1); try { await plugin.teardown(context); } catch (teardownError) { log.warn( 'setup', `plugin ${plugin.name} teardown after failed setup threw`, { error: teardownError } ); } if (plugin.critical === true) throw e; log.warn( 'setup', `plugin '${plugin.name}' setup failed; continuing without it`, { error: e } ); return; } statuses.set(plugin.name, 'active'); }, async teardownAll(contextFor): Promise { const failures: PluginInstallFailure[] = []; for (const plugin of [...installedPlugins].reverse()) { try { await plugin.teardown(contextFor(plugin)); } catch (e) { log.warn('teardown', `plugin ${plugin.name} teardown failed`, { error: e, }); failures.push({ name: plugin.name, error: e }); } } return failures; }, status(name): PluginStatus { return statuses.get(name) ?? 'absent'; }, reset(): void { installedPlugins.length = 0; statuses.clear(); }, }; }