import "dotenv/config"; import express from "express"; import cors from "cors"; import path from "path"; import { fileURLToPath } from "url"; import { ensureTenant } from "./src/db/repo.js"; import { addSseClient, removeSseClient } from "./src/services/sse.js"; import { makeGetConversations } from "./src/controllers/conversations.js"; import { makeListRuns, makeGetRunById } from "./src/controllers/runs.js"; import { makeSimSend } from "./src/controllers/sim.js"; import { makeEvolutionWebhook } from "./src/controllers/evolution.js"; import { makeGetConversationState } from "./src/controllers/conversationState.js"; import { makeListMessages } from "./src/controllers/messages.js"; import { makeSearchProducts } from "./src/controllers/products.js"; import { makeDeleteConversation, makeDeleteUser, makeListUsers, makeRetryLast } from "./src/controllers/admin.js"; async function configureUndiciDispatcher() { // Node 18+ usa undici debajo de fetch. Esto suele arreglar timeouts “fantasma” por keep-alive/pooling. // Nota: si el módulo `undici` no está disponible, no rompemos el arranque (solo logueamos warning). try { const { setGlobalDispatcher, Agent } = await import("undici"); setGlobalDispatcher( new Agent({ connections: 10, pipelining: 0, keepAliveTimeout: 10_000, keepAliveMaxTimeout: 10_000, }) ); console.log("[http] undici global dispatcher configured"); } catch (e) { console.warn("[http] undici dispatcher not configured:", e?.message || e); } } const app = express(); app.use(cors()); app.use(express.json({ limit: "1mb" })); // Serve /public as static (UI + webcomponents) const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const publicDir = path.join(__dirname, "public"); app.use(express.static(publicDir)); /** * --- Tenant --- */ const TENANT_KEY = process.env.TENANT_KEY || "piaf"; let TENANT_ID = null; function nowIso() { return new Date().toISOString(); } /** * --- SSE endpoint --- */ app.get("/stream", (req, res) => { res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.flushHeaders?.(); res.write(`event: hello\ndata: ${JSON.stringify({ ts: nowIso() })}\n\n`); addSseClient(res); req.on("close", () => { removeSseClient(res); res.end(); }); }); /** * --- UI data endpoints --- */ app.post("/sim/send", makeSimSend()); app.get("/conversations", makeGetConversations(() => TENANT_ID)); app.get("/conversations/state", makeGetConversationState(() => TENANT_ID)); app.delete("/conversations/:chat_id", makeDeleteConversation(() => TENANT_ID)); app.post("/conversations/:chat_id/retry-last", makeRetryLast(() => TENANT_ID)); app.get("/messages", makeListMessages(() => TENANT_ID)); app.get("/products", makeSearchProducts(() => TENANT_ID)); app.get("/users", makeListUsers(() => TENANT_ID)); app.delete("/users/:chat_id", makeDeleteUser(() => TENANT_ID)); app.get("/runs", makeListRuns(() => TENANT_ID)); app.get("/runs/:run_id", makeGetRunById(() => TENANT_ID)); app.post("/webhook/evolution", makeEvolutionWebhook()); app.get("/", (req, res) => { res.sendFile(path.join(publicDir, "index.html")); }); /** * --- Boot --- */ const port = process.env.PORT || 3000; (async function boot() { await configureUndiciDispatcher(); TENANT_ID = await ensureTenant({ key: TENANT_KEY, name: TENANT_KEY.toUpperCase() }); app.listen(port, () => console.log(`UI: http://localhost:${port} (tenant=${TENANT_KEY})`)); })().catch((err) => { console.error("Boot failed:", err); process.exit(1); });