Files
EllyDiscordBot/crates/core/src/main.ts
2026-05-28 23:46:40 +00:00

166 lines
4.3 KiB
TypeScript

/**
* @elly/core entrypoint — Phase 2 boot.
*
* Lifecycle:
* 1. Load + validate `config.toml` (Zod).
* 2. Load + validate environment (Zod).
* 3. Build the root structured logger.
* 4. Build the DI container (DB, migrations, KV, bus, HTTP server).
* 5. Start the IPC HTTP server.
* 6. Publish `server.ready` on the domain bus.
* 7. Block on SIGINT/SIGTERM, then gracefully shut down the container.
*/
import {
ConfigError,
ConfigValidationError,
CoreEnvSchema,
createLogger,
EnvValidationError,
loadConfig,
loadEnv,
type Config,
type CoreEnv,
type Logger,
} from "@elly/shared";
import { buildContainer, type CoreContainer } from "./container.ts";
const CONFIG_PATH = Deno.env.get("CONFIG_PATH") ?? "./config.toml";
const LOGGER_NAME = "@elly/core";
const VERSION = "0.1.0";
async function main(): Promise<void> {
const config = await loadConfigOrExit();
const env = loadEnvOrExit();
const logger = buildLogger(config, env);
logger.info("phase 2 boot starting", {
crate: "core",
nodeEnv: env.NODE_ENV,
version: VERSION,
});
let container: CoreContainer | null = null;
try {
container = await buildContainer({ config, env, logger, version: VERSION });
} catch (err) {
logger.fatal("container build failed", {
err: err instanceof Error ? err : new Error(String(err)),
});
await logger.flush();
Deno.exit(1);
}
installSignalHandlers(container, logger);
try {
await container.http.start();
} catch (err) {
logger.fatal("ipc server failed to start", {
err: err instanceof Error ? err : new Error(String(err)),
});
await container.shutdown();
await logger.flush();
Deno.exit(1);
}
container.bus.publish({
type: "server.ready",
payload: { version: VERSION, pid: Deno.pid },
});
logger.info("phase 2 boot complete", {
pid: Deno.pid,
ipc: `${config.ipc.host}:${config.ipc.port}`,
});
// Hold the process open until a signal handler tears the container down.
await new Promise<void>(() => {});
}
// =====================================================================
// Boot helpers
// =====================================================================
async function loadConfigOrExit(): Promise<Config> {
try {
return await loadConfig(CONFIG_PATH);
} catch (err) {
bootFail(err, "config");
}
}
function loadEnvOrExit(): CoreEnv {
try {
return loadEnv(CoreEnvSchema);
} catch (err) {
bootFail(err, "env");
}
}
function buildLogger(config: Config, env: CoreEnv): Logger {
const level = env.LOG_LEVEL ?? config.logging.level;
const isProd = env.NODE_ENV === "production";
return createLogger({
name: LOGGER_NAME,
level,
format: isProd ? "json" : config.logging.format,
file: config.logging.file
? {
path: config.logging.file,
maxBytes: config.logging.file_max_bytes,
maxBackups: config.logging.file_max_backups,
}
: undefined,
});
}
function installSignalHandlers(container: CoreContainer, logger: Logger): void {
let shuttingDown = false;
const shutdown = (signal: string) => {
if (shuttingDown) {
logger.warn("shutdown already in progress; ignoring signal", { signal });
return;
}
shuttingDown = true;
logger.info("shutdown signal received", { signal });
container
.shutdown()
.catch((err) => {
logger.error("shutdown error", {
err: err instanceof Error ? err : new Error(String(err)),
});
})
.finally(() => {
logger.flush().finally(() => Deno.exit(0));
});
};
Deno.addSignalListener("SIGINT", () => shutdown("SIGINT"));
Deno.addSignalListener("SIGTERM", () => shutdown("SIGTERM"));
}
function bootFail(err: unknown, stage: "config" | "env"): never {
if (err instanceof ConfigValidationError || err instanceof EnvValidationError) {
console.error(`[@elly/core] ${stage} validation failed:`);
for (const issue of err.issues) {
console.error(` - ${issue.path}: ${issue.message}`);
}
Deno.exit(1);
}
if (err instanceof ConfigError) {
console.error(`[@elly/core] ${stage} error: ${err.message}`);
Deno.exit(1);
}
console.error(`[@elly/core] unexpected ${stage} error:`, err);
Deno.exit(1);
}
if (import.meta.main) {
main().catch((err) => {
console.error("[@elly/core] fatal boot error:", err);
Deno.exit(1);
});
}