import { closeDb, migrate } from "@websensor/db"; import { closeDispatcher } from "@websensor/connectors"; import { loadRecent, refreshClusterStates } from "./cluster"; import { config, log } from "./config"; import { startMetricsServer } from "./metrics"; import { closeRedis } from "./redis"; import { runDiscovery, syncRegistry } from "./registry"; import { pruneNotifications, pruneRawSnapshots } from "./retention"; import { pruneOldRuns, rollupConnectorHealth, Scheduler } from "./scheduler"; async function main(): Promise { log.info({ env: config.env, version: config.version, llm: Boolean(config.llm.apiKey) }, "websensor engine starting"); const applied = await migrate(config.databaseUrl); if (applied.length) log.info({ applied }, "migrations applied"); await syncRegistry(); await loadRecent(); await startMetricsServer(); const scheduler = new Scheduler(); await scheduler.start(); const timers: NodeJS.Timeout[] = []; timers.push(setInterval(() => rollupConnectorHealth().catch((e) => log.warn({ err: (e as Error).message }, "health rollup failed")), 60_000)); timers.push(setInterval(() => pruneOldRuns().catch(() => undefined), 6 * 3600e3)); timers.push(setInterval(() => refreshClusterStates().catch((e) => log.warn({ err: (e as Error).message }, "cluster state refresh failed")), 5 * 60_000)); if (config.retention.enabled) { timers.push(setInterval(() => pruneRawSnapshots().catch((e) => log.warn({ err: (e as Error).message }, "retention failed")), 30 * 60_000)); timers.push(setInterval(() => pruneNotifications().catch(() => undefined), 24 * 3600e3)); setTimeout(() => pruneRawSnapshots().catch(() => undefined), 60_000); } await rollupConnectorHealth().catch(() => undefined); if (config.discovery.enabled) { // Background: validate feeds/sitemaps/status pages for sources not discovered recently, then weekly. setTimeout(() => runDiscovery({ onlyMissing: true }).catch((e) => log.warn({ err: (e as Error).message }, "discovery failed")), 5_000); timers.push(setInterval(() => runDiscovery({ onlyMissing: true }).catch(() => undefined), 24 * 3600e3)); } let shuttingDown = false; const shutdown = async (signal: string): Promise => { if (shuttingDown) return; shuttingDown = true; log.info({ signal }, "shutting down"); for (const t of timers) clearInterval(t); await scheduler.stop(); await closeDispatcher(); await closeRedis(); await closeDb(); process.exit(0); }; process.on("SIGINT", () => void shutdown("SIGINT")); process.on("SIGTERM", () => void shutdown("SIGTERM")); process.on("unhandledRejection", (e) => log.error({ err: e instanceof Error ? e.stack : String(e) }, "unhandled rejection")); } main().catch((e) => { log.fatal({ err: (e as Error).stack ?? String(e) }, "engine failed to start"); process.exit(1); });