import { api } from "@/convex/_generated/api" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { resolveCorsOrigin } from "@/server/cors" export const runtime = "nodejs" export const dynamic = "force-dynamic" // GET /api/machines/chat/stream?token=xxx // Server-Sent Events endpoint para atualizacoes de chat em tempo real export async function GET(request: Request) { const origin = request.headers.get("origin") const resolvedOrigin = resolveCorsOrigin(origin) // Extrair token da query string const url = new URL(request.url) const token = url.searchParams.get("token") if (!token) { return new Response("Missing token", { status: 400, headers: { "Access-Control-Allow-Origin": resolvedOrigin, "Access-Control-Allow-Credentials": resolvedOrigin !== "*" ? "true" : "false", }, }) } let client try { client = createConvexClient() } catch (error) { if (error instanceof ConvexConfigurationError) { return new Response(error.message, { status: 500, headers: { "Access-Control-Allow-Origin": resolvedOrigin, "Access-Control-Allow-Credentials": resolvedOrigin !== "*" ? "true" : "false", }, }) } throw error } // Validar token antes de iniciar stream try { await client.query(api.liveChat.checkMachineUpdates, { machineToken: token }) } catch (error) { const message = error instanceof Error ? error.message : "Token invalido" return new Response(message, { status: 401, headers: { "Access-Control-Allow-Origin": resolvedOrigin, "Access-Control-Allow-Credentials": resolvedOrigin !== "*" ? "true" : "false", }, }) } const encoder = new TextEncoder() const stream = new ReadableStream({ async start(controller) { let isAborted = false let previousState: string | null = null const sendEvent = (event: string, data: unknown) => { if (isAborted) return try { controller.enqueue(encoder.encode(`event: ${event}\n`)) controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)) } catch { // Stream fechado isAborted = true } } // Heartbeat a cada 30s para manter conexao viva const heartbeatInterval = setInterval(() => { if (isAborted) { clearInterval(heartbeatInterval) return } sendEvent("heartbeat", { ts: Date.now() }) }, 30_000) // Poll interno a cada 5s e push via SSE (balanco entre responsividade e carga) const pollInterval = setInterval(async () => { if (isAborted) { clearInterval(pollInterval) return } try { const result = await client.query(api.liveChat.checkMachineUpdates, { machineToken: token, }) // Criar hash do estado para detectar mudancas const currentState = JSON.stringify({ hasActiveSessions: result.hasActiveSessions, totalUnread: result.totalUnread, sessions: result.sessions, }) // Enviar update apenas se houver mudancas if (currentState !== previousState) { sendEvent("update", { ...result, ts: Date.now(), }) previousState = currentState } } catch (error) { console.error("[SSE] Poll error:", error) // Enviar erro e fechar conexao sendEvent("error", { message: "Poll failed" }) isAborted = true clearInterval(pollInterval) clearInterval(heartbeatInterval) controller.close() } }, 5_000) // Enviar evento inicial de conexao sendEvent("connected", { ts: Date.now() }) // Cleanup quando conexao for abortada request.signal.addEventListener("abort", () => { isAborted = true clearInterval(heartbeatInterval) clearInterval(pollInterval) try { controller.close() } catch { // Ja fechado } }) }, }) return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", "X-Accel-Buffering": "no", // Desabilita buffering no nginx "Access-Control-Allow-Origin": resolvedOrigin, "Access-Control-Allow-Credentials": resolvedOrigin !== "*" ? "true" : "false", }, }) } // OPTIONS para CORS preflight export async function OPTIONS(request: Request) { const origin = request.headers.get("origin") const resolvedOrigin = resolveCorsOrigin(origin) return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": resolvedOrigin, "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Allow-Credentials": resolvedOrigin !== "*" ? "true" : "false", "Access-Control-Max-Age": "86400", }, }) }