166 lines
4.3 KiB
TypeScript
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);
|
|
});
|
|
}
|