import { Server as SocketIoServer } from "socket.io"; import { createServer } from "./app.js"; import { env } from "./config/env.js"; import { logger } from "./lib/logger.service.js"; import { MonitoringBroadcastService } from "./lib/monitoring-broadcast.service.js"; import { techStepClassifier } from "./lib/recipe-matching/tech-step-matcher.js"; import { registerAllRecipeSources } from "./sources/index.js"; // Populates the recipe-source registry (recipe-source-registry.ts) before // the app starts — see registerAllRecipeSources' doc comment for why this // doesn't happen inside app.ts/createServer() itself. registerAllRecipeSources(); /** * Trains the tech-step classifier (a `POST /v1/train` round-trip per locale * to `services/tech-step-intent-service` — see * `TechStepClassifierService.warmUp`) before accepting any traffic, so the * first real recipe save/preview isn't the one stuck waiting for it. * * Retried with exponential backoff: in Docker Compose, `app`'s own * `depends_on: tech-step-intent-service: condition: service_healthy` * (`docker-compose.yml`) already means that service is up by the time this * runs, but native dev (`pnpm dev:api`, no Compose ordering at all) can * easily start this before the intent service has finished loading its * spaCy models — a transient connection failure here shouldn't need a * manual restart. Still non-fatal after every attempt is exhausted: the * *next* real call retries training itself (see `_ensureTrained`'s own * retry-on-failure comment), same graceful-degrade posture as before this * retry loop existed. */ async function warmUpTechStepClassifier(): Promise { const maxAttempts = 5; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { await techStepClassifier.warmUp(); return; } catch (err) { if (attempt === maxAttempts) { logger.error("Tech-step classifier warm-up failed after retries", { error: err instanceof Error ? err.message : String(err), attempts: attempt, }); return; } const delayMs = 1000 * 2 ** (attempt - 1); await new Promise((resolve) => setTimeout(resolve, delayMs)); } } } await warmUpTechStepClassifier(); const server = createServer(); // Attaches to the same underlying `http.Server` `server.listen()` below // will start — `socket.io` binds to the HTTP server itself, not to // Express or a port, hence `ExpressServer.httpServer` (packages/express- // tools) rather than going through `server.instance` (the Express app). // Own CORS config: the WebSocket handshake is a separate HTTP upgrade // request that never runs through `setupCore`'s `cors()` middleware. const io = new SocketIoServer(server.httpServer, { cors: { origin: env.CORS_ORIGIN, credentials: true }, }); new MonitoringBroadcastService(io).start(); server.listen(env.PORT, () => { logger.info("API listening", { port: env.PORT, nodeEnv: env.NODE_ENV }); });