157 lines
3.8 KiB
TypeScript
157 lines
3.8 KiB
TypeScript
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<T>(name: string): T {
|
|
return this.#registry.get(name);
|
|
}
|
|
|
|
all<T>(name: string): T[] {
|
|
return this.#registry.all(name);
|
|
}
|
|
|
|
on<T>(event: string, handler: EventHandler<T>): Unsubscribe {
|
|
return this.#events.on(event, handler);
|
|
}
|
|
|
|
emit<T>(event: string, payload: T): Promise<unknown[]> {
|
|
return this.#events.emit(event, payload);
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
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<void> {
|
|
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<unknown[]> {
|
|
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;
|
|
}
|
|
}
|