import { EventBus, type EventHandler, type Unsubscribe } from "./events"; import type { Extension, ExtensionRuntimeContext, ExtensionSetupContext, } from "./extension"; import { ExtensionRegistry } from "./registry"; export class Kernel { #extensions: Extension[] = []; #activeExtensions: Extension[] = []; #registry = new ExtensionRegistry(); #events = new EventBus(); #setupContext: ExtensionSetupContext; #runtimeContext: ExtensionRuntimeContext; #state = "created"; constructor() { const registry = this.#registry; const events = this.#events; this.#setupContext = { add(name, value) { registry.add(name, value); }, on(event, handler) { return events.on(event, handler); }, }; this.#runtimeContext = { get(name) { return registry.get(name); }, all(name) { return registry.all(name); }, on(event, handler) { return events.on(event, handler); }, emit(event, payload) { return events.emit(event, payload); }, }; } get state(): string { return this.#state; } get installedExtensionIds(): string[] { return this.#extensions.map((extension) => extension.id); } use(...extensions: Extension[]): this { if (this.#state !== "created") { throw new Error(`Cannot install extensions while kernel is ${this.#state}.`); } const ids = new Set(this.#extensions.map((extension) => extension.id)); for (const extension of extensions) { if (ids.has(extension.id)) { throw new Error(`Extension "${extension.id}" is already installed.`); } ids.add(extension.id); } this.#extensions.push(...extensions); return this; } get(name: string): T { return this.#registry.get(name); } all(name: string): T[] { return this.#registry.all(name); } on(event: string, handler: EventHandler): Unsubscribe { return this.#events.on(event, handler); } emit(event: string, payload: T): Promise { return this.#events.emit(event, payload); } async start(): Promise { if (this.#state !== "created") { throw new Error(`Cannot start kernel while it is ${this.#state}.`); } this.#state = "starting"; try { for (const extension of this.#extensions) { await extension.setup(this.#setupContext); } for (const extension of this.#extensions) { this.#activeExtensions.push(extension); await extension.start?.(this.#runtimeContext); } this.#state = "running"; } catch (error) { const cleanupErrors = await this.#stopActiveExtensions(); this.#events.clear(); this.#registry.clear(); this.#state = "stopped"; if (cleanupErrors.length > 0) { throw new AggregateError( [error, ...cleanupErrors], "Kernel failed to start and one or more extensions failed to clean up.", ); } throw error; } } async stop(): Promise { if (this.#state === "stopped") return; if (this.#state !== "running") { throw new Error(`Cannot stop kernel while it is ${this.#state}.`); } this.#state = "stopping"; const errors = await this.#stopActiveExtensions(); this.#events.clear(); this.#registry.clear(); this.#state = "stopped"; if (errors.length > 0) { throw new AggregateError(errors, "One or more extensions failed to stop."); } } async #stopActiveExtensions(): Promise { const errors: unknown[] = []; for (const extension of [...this.#activeExtensions].reverse()) { try { await extension.stop?.(this.#runtimeContext); } catch (error) { errors.push(error); } } this.#activeExtensions = []; return errors; } }