tg-bot-ts/src/systems/runtime.ts
2026-06-10 04:44:57 +01:00

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);
}
}
},
};