From e08dc21003258a9010003968461e8bf93d67e780 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Tue, 9 Dec 2025 01:53:51 -0300 Subject: [PATCH] desktop/chat: fallback de polling se WS falhar --- apps/desktop/src/chat/convexMachineClient.ts | 142 ++++++++++++++++++- 1 file changed, 136 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/chat/convexMachineClient.ts b/apps/desktop/src/chat/convexMachineClient.ts index 7ca9bbe..538398d 100644 --- a/apps/desktop/src/chat/convexMachineClient.ts +++ b/apps/desktop/src/chat/convexMachineClient.ts @@ -34,6 +34,11 @@ type MachineUpdatePayload = { totalUnread: number } +type MessagesPayload = { + messages: ChatMessage[] + hasSession: boolean +} + const FN_CHECK_UPDATES = "liveChat.checkMachineUpdates" as const const FN_LIST_MESSAGES = "liveChat.listMachineMessages" as const const FN_POST_MESSAGE = "liveChat.postMachineMessage" as const @@ -92,13 +97,71 @@ export async function subscribeMachineUpdates( onError?: (error: Error) => void ): Promise<() => void> { const { client, token } = await ensureClient() + let stopped = false + let pollTimer: ReturnType | null = null + + const stopPoll = () => { + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null + } + } + + const startPoll = async () => { + stopPoll() + try { + const { apiBaseUrl } = await getMachineStoreConfig() + const poll = async () => { + try { + const res = await fetch(`${apiBaseUrl}/api/machines/chat/poll`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ machineToken: token }), + }) + if (!res.ok) throw new Error(`poll failed: ${res.status}`) + const data = (await res.json()) as MachineUpdatePayload + if (!stopped) callback(data) + } catch (err) { + if (!stopped) onError?.(err as Error) + } + } + await poll() + pollTimer = setInterval(poll, 4000) + } catch (err) { + onError?.(err as Error) + } + } + const sub = client.onUpdate( FN_CHECK_UPDATES as unknown as FunctionReference<"query">, { machineToken: token }, - (value) => callback(value), - onError + (value) => { + stopPoll() + callback(value) + }, + (err) => { + onError?.(err) + startPoll().catch(() => { + // erro já reportado via onError + }) + } ) - return () => sub.unsubscribe() + + // fallback caso a inscrição falhe imediatamente + setTimeout(() => { + if (stopped) return + // Se não houver mensagens recebidas pelo WS em 3s, inicia polling + // (o callback do WS encerra o polling caso volte a funcionar) + startPoll().catch(() => { + // erro já reportado via onError + }) + }, 3000) + + return () => { + stopped = true + stopPoll() + sub.unsubscribe() + } } export async function subscribeMachineMessages( @@ -107,16 +170,83 @@ export async function subscribeMachineMessages( onError?: (error: Error) => void ): Promise<() => void> { const { client, token } = await ensureClient() + let stopped = false + let pollTimer: ReturnType | null = null + let lastSince = 0 + + const stopPoll = () => { + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null + } + } + + const startPoll = async () => { + stopPoll() + try { + const { apiBaseUrl } = await getMachineStoreConfig() + const poll = async () => { + try { + const res = await fetch(`${apiBaseUrl}/api/machines/chat/messages`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "list", + machineToken: token, + ticketId, + since: lastSince || undefined, + }), + }) + if (!res.ok) throw new Error(`messages poll failed: ${res.status}`) + const data = (await res.json()) as MessagesPayload + if (!stopped) { + callback(data) + const newest = data.messages.reduce((max, msg) => Math.max(max, msg.createdAt), lastSince) + lastSince = newest + } + } catch (err) { + if (!stopped) onError?.(err as Error) + } + } + await poll() + pollTimer = setInterval(poll, 4000) + } catch (err) { + onError?.(err as Error) + } + } + const sub = client.onUpdate( FN_LIST_MESSAGES as unknown as FunctionReference<"query">, { machineToken: token, ticketId, }, - (value) => callback(value), - onError + (value) => { + stopPoll() + callback(value) + const newest = value.messages.reduce((max, msg) => Math.max(max, msg.createdAt), lastSince) + lastSince = newest + }, + (err) => { + onError?.(err) + startPoll().catch(() => { + // erro já reportado via onError + }) + } ) - return () => sub.unsubscribe() + + setTimeout(() => { + if (stopped) return + startPoll().catch(() => { + // erro já reportado via onError + }) + }, 3000) + + return () => { + stopped = true + stopPoll() + sub.unsubscribe() + } } export async function sendMachineMessage(input: {