#!/usr/bin/env node /** * Servidor WebSocket dedicado para notificações de chat (máquinas). * * Por enquanto ele replica a lógica de streaming via SSE/poll: * - autentica via machineToken (query ?token=) * - consulta checkMachineUpdates a cada 1s * - envia eventos "connected", "update" e "heartbeat" * - fecha em caso de erro de autenticação * * Isso permite remover SSE/poll no cliente, mantendo compatibilidade com o * backend Convex existente. */ import { WebSocketServer } from "ws" import { ConvexHttpClient } from "convex/browser" import { api } from "../convex/_generated/api.js" const PORT = Number(process.env.CHAT_WS_PORT ?? process.env.PORT_WS ?? 3030) const POLL_MS = Number(process.env.CHAT_WS_POLL_MS ?? 1000) const HEARTBEAT_MS = Number(process.env.CHAT_WS_HEARTBEAT_MS ?? 30000) const convexUrl = process.env.CONVEX_INTERNAL_URL ?? process.env.NEXT_PUBLIC_CONVEX_URL ?? process.env.CONVEX_URL ?? null if (!convexUrl) { console.error("[chat-ws] ERRO: defina CONVEX_INTERNAL_URL ou NEXT_PUBLIC_CONVEX_URL") process.exit(1) } const wss = new WebSocketServer({ port: PORT }) console.log(`[chat-ws] Servidor WebSocket iniciado na porta ${PORT}`) function buildClient() { return new ConvexHttpClient(convexUrl) } function parseToken(urlString) { try { const url = new URL(urlString, "http://localhost") return url.searchParams.get("token") } catch { return null } } wss.on("connection", (ws, req) => { const token = parseToken(req.url ?? "") if (!token) { ws.close(1008, "Missing token") return } const client = buildClient() let previousState = null let closed = false const send = (event, data) => { if (ws.readyState === ws.OPEN) { ws.send(JSON.stringify({ event, data })) } } // Heartbeat const heartbeat = setInterval(() => { if (closed) return send("heartbeat", { ts: Date.now() }) }, HEARTBEAT_MS) // Poll const poll = setInterval(async () => { if (closed) return try { const result = await client.query(api.liveChat.checkMachineUpdates, { machineToken: token, }) const currentState = JSON.stringify({ hasActiveSessions: result.hasActiveSessions, totalUnread: result.totalUnread, sessions: result.sessions, }) if (currentState !== previousState) { previousState = currentState send("update", { ...result, ts: Date.now() }) } } catch (error) { console.error("[chat-ws] Poll error:", error?.message ?? error) send("error", { message: "Poll failed" }) ws.close(1011, "Poll failed") } }, POLL_MS) // Primeira validação + evento inicial client .query(api.liveChat.checkMachineUpdates, { machineToken: token }) .then((result) => { previousState = JSON.stringify({ hasActiveSessions: result.hasActiveSessions, totalUnread: result.totalUnread, sessions: result.sessions, }) send("connected", { ts: Date.now(), ...result }) }) .catch((error) => { console.error("[chat-ws] Token inválido:", error?.message ?? error) send("error", { message: "Token inválido" }) ws.close(1008, "Invalid token") }) ws.on("close", () => { closed = true clearInterval(poll) clearInterval(heartbeat) }) ws.on("error", (err) => { console.error("[chat-ws] WS erro:", err?.message ?? err) closed = true clearInterval(poll) clearInterval(heartbeat) }) }) wss.on("error", (err) => { console.error("[chat-ws] Erro no servidor:", err?.message ?? err) })