130 lines
3.5 KiB
JavaScript
130 lines
3.5 KiB
JavaScript
#!/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)
|
|
})
|