175 lines
No EOL
4.7 KiB
TypeScript
175 lines
No EOL
4.7 KiB
TypeScript
/**
|
|
* Runtime — manages application lifecycle phases.
|
|
*
|
|
* Usage:
|
|
* import { Runtime } from "@systems/runtime";
|
|
*
|
|
* // Register hooks in any module:
|
|
* Runtime.phase("load", () => Config.load());
|
|
* Runtime.phase("restore", () => restoreLayout());
|
|
* Runtime.phase("ready", () => Scheduler.start(client));
|
|
*
|
|
* // In index.ts:
|
|
* await Runtime.start();
|
|
*/
|
|
|
|
import { Logger } from "@systems/logger";
|
|
|
|
const log = Logger.for("runtime");
|
|
|
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
export type RuntimePhase =
|
|
| "load" // read data from disk
|
|
| "restore" // restore in-memory state from loaded data
|
|
| "connect" // external connections (Discord client login)
|
|
| "schedule" // start cron jobs and timers
|
|
| "ready" // bot is fully operational
|
|
| "shutdown"; // graceful shutdown
|
|
|
|
export type RuntimeState =
|
|
| "idle"
|
|
| "starting"
|
|
| "ready"
|
|
| "stopping"
|
|
| "stopped";
|
|
|
|
interface Hook {
|
|
phase: RuntimePhase;
|
|
fn: () => void | Promise<void>;
|
|
priority: number;
|
|
name?: string;
|
|
}
|
|
|
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
|
|
const PHASE_ORDER: RuntimePhase[] = [
|
|
"load",
|
|
"restore",
|
|
"connect",
|
|
"schedule",
|
|
"ready",
|
|
];
|
|
|
|
const SHUTDOWN_PHASES: RuntimePhase[] = ["shutdown"];
|
|
|
|
const _hooks: Hook[] = [];
|
|
let _state: RuntimeState = "idle";
|
|
let _errorHandlers: ((err: Error) => void)[] = [];
|
|
|
|
// ─── Runtime namespace ────────────────────────────────────────────────────────
|
|
|
|
export const Runtime = {
|
|
/**
|
|
* Register a hook for a lifecycle phase.
|
|
* Hooks within the same phase run in priority order (ascending),
|
|
* then registration order for equal priorities.
|
|
*/
|
|
phase(
|
|
phase: RuntimePhase,
|
|
fn: () => void | Promise<void>,
|
|
options?: { priority?: number; name?: string }
|
|
): void {
|
|
_hooks.push({
|
|
phase,
|
|
fn,
|
|
priority: options?.priority ?? 0,
|
|
name: options?.name,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Register a global error handler.
|
|
*/
|
|
on(event: "error", handler: (err: Error) => void): void {
|
|
if (event === "error") _errorHandlers.push(handler);
|
|
},
|
|
|
|
/**
|
|
* Current runtime state.
|
|
*/
|
|
state(): RuntimeState {
|
|
return _state;
|
|
},
|
|
|
|
/**
|
|
* Run all startup phases in order.
|
|
*/
|
|
async start(): Promise<void> {
|
|
if (_state !== "idle") {
|
|
log.warn(`Runtime.start() called in state: ${_state}`);
|
|
return;
|
|
}
|
|
|
|
_state = "starting";
|
|
log.info("Starting...");
|
|
|
|
for (const phase of PHASE_ORDER) {
|
|
const hooks = _hooks
|
|
.filter((h) => h.phase === phase)
|
|
.sort((a, b) => a.priority - b.priority);
|
|
|
|
if (hooks.length === 0) continue;
|
|
|
|
log.debug(`Phase [${phase}] — ${hooks.length} hook(s)`);
|
|
|
|
for (const hook of hooks) {
|
|
try {
|
|
await hook.fn();
|
|
if (hook.name) log.debug(` ✓ ${hook.name}`);
|
|
} catch (err: any) {
|
|
log.error(`Phase [${phase}] hook failed:`, err.message);
|
|
_errorHandlers.forEach((h) => h(err));
|
|
throw err; // halt startup on error
|
|
}
|
|
}
|
|
}
|
|
|
|
_state = "ready";
|
|
log.info("Ready.");
|
|
},
|
|
|
|
/**
|
|
* Run shutdown hooks and stop the runtime.
|
|
*/
|
|
async stop(): Promise<void> {
|
|
if (_state === "stopped") return;
|
|
_state = "stopping";
|
|
log.info("Shutting down...");
|
|
|
|
const hooks = _hooks
|
|
.filter((h) => h.phase === "shutdown")
|
|
.sort((a, b) => a.priority - b.priority);
|
|
|
|
for (const hook of hooks) {
|
|
try {
|
|
await hook.fn();
|
|
if (hook.name) log.debug(` ✓ ${hook.name}`);
|
|
} catch (err: any) {
|
|
log.error("Shutdown hook failed:", err.message);
|
|
}
|
|
}
|
|
|
|
_state = "stopped";
|
|
log.info("Stopped.");
|
|
},
|
|
|
|
/**
|
|
* Re-run a specific phase (e.g. "restore" on hot reload).
|
|
*/
|
|
async rerun(phase: RuntimePhase): Promise<void> {
|
|
const hooks = _hooks
|
|
.filter((h) => h.phase === phase)
|
|
.sort((a, b) => a.priority - b.priority);
|
|
|
|
log.debug(`Rerunning phase [${phase}] — ${hooks.length} hook(s)`);
|
|
|
|
for (const hook of hooks) {
|
|
try {
|
|
await hook.fn();
|
|
} catch (err: any) {
|
|
log.error(`Rerun [${phase}] hook failed:`, err.message);
|
|
}
|
|
}
|
|
},
|
|
}; |