(Init): Added shit
This commit is contained in:
165
crates/core/src/main.ts
Normal file
165
crates/core/src/main.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user