From 508f915cf9e4cc078a50e246a7771cb9373f03d3 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Tue, 9 Dec 2025 21:49:04 -0300 Subject: [PATCH] fix: corrigir memory leaks e testes de mocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fechar ConvexClient antigo antes de criar novo (evita memory leak) - Adicionar flag disposed para prevenir race condition em useEffect - Reduzir polling SSE de 1s para 5s (balanco entre responsividade e carga) - Adicionar .take() aos mocks de testes para compatibilidade 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src/chat/convexMachineClient.ts | 9 +++++++++ apps/desktop/src/main.tsx | 13 +++++++++++-- src/app/api/machines/chat/stream/route.ts | 4 ++-- tests/machines.getById.test.ts | 4 ++++ tests/utils/report-test-helpers.ts | 15 +++++++++++++-- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/chat/convexMachineClient.ts b/apps/desktop/src/chat/convexMachineClient.ts index e64cae0..833284b 100644 --- a/apps/desktop/src/chat/convexMachineClient.ts +++ b/apps/desktop/src/chat/convexMachineClient.ts @@ -83,6 +83,15 @@ async function ensureClient(): Promise { return cached } + // Fechar cliente antigo antes de criar novo (evita memory leak) + if (cached) { + try { + cached.client.close() + } catch { + // Ignora erro ao fechar cliente antigo + } + } + const client = new ConvexClient(convexUrl) cached = { client, token: data.token, convexUrl } return cached diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index 1e5bdb1..befab89 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -1090,9 +1090,11 @@ const resolvedAppUrl = useMemo(() => { let prevUnread = 0 let unsub: (() => void) | null = null + let disposed = false + subscribeMachineUpdates( (payload) => { - if (!payload) return + if (disposed || !payload) return const totalUnread = payload.totalUnread ?? 0 const hasSessions = (payload.sessions ?? []).length > 0 @@ -1107,6 +1109,7 @@ const resolvedAppUrl = useMemo(() => { prevUnread = totalUnread }, (err) => { + if (disposed) return console.error("chat updates (Convex) erro:", err) const msg = (err?.message || "").toLowerCase() if (msg.includes("token de máquina") || msg.includes("revogado") || msg.includes("expirado") || msg.includes("inválido")) { @@ -1115,10 +1118,16 @@ const resolvedAppUrl = useMemo(() => { } } ).then((u) => { - unsub = u + // Se o effect já foi desmontado antes da Promise resolver, cancelar imediatamente + if (disposed) { + u() + } else { + unsub = u + } }) return () => { + disposed = true unsub?.() } }, [token, attemptSelfHeal]) diff --git a/src/app/api/machines/chat/stream/route.ts b/src/app/api/machines/chat/stream/route.ts index efa7c29..e86a01d 100644 --- a/src/app/api/machines/chat/stream/route.ts +++ b/src/app/api/machines/chat/stream/route.ts @@ -82,7 +82,7 @@ export async function GET(request: Request) { sendEvent("heartbeat", { ts: Date.now() }) }, 30_000) - // Poll interno a cada 1s e push via SSE (responsivo para chat) + // Poll interno a cada 5s e push via SSE (balanco entre responsividade e carga) const pollInterval = setInterval(async () => { if (isAborted) { clearInterval(pollInterval) @@ -118,7 +118,7 @@ export async function GET(request: Request) { clearInterval(heartbeatInterval) controller.close() } - }, 1_000) + }, 5_000) // Enviar evento inicial de conexao sendEvent("connected", { ts: Date.now() }) diff --git a/tests/machines.getById.test.ts b/tests/machines.getById.test.ts index ef197f6..66e0140 100644 --- a/tests/machines.getById.test.ts +++ b/tests/machines.getById.test.ts @@ -60,8 +60,12 @@ describe("convex.machines.getById", () => { collect: vi.fn(async () => [ { revoked: false, expiresAt: FIXED_NOW + 60_000, lastUsedAt: FIXED_NOW - 1000, usageCount: 5 }, ]), + take: vi.fn(async () => [ + { revoked: false, expiresAt: FIXED_NOW + 60_000, lastUsedAt: FIXED_NOW - 1000, usageCount: 5 }, + ]), })), collect: vi.fn(async () => []), + take: vi.fn(async () => []), })), } diff --git a/tests/utils/report-test-helpers.ts b/tests/utils/report-test-helpers.ts index 5744f96..8f3a4bc 100644 --- a/tests/utils/report-test-helpers.ts +++ b/tests/utils/report-test-helpers.ts @@ -31,6 +31,7 @@ function ticketsChain(collection: Doc<"tickets">[]) { }), order: vi.fn(() => chain), collect: vi.fn(async () => collection), + take: vi.fn(async (limit: number) => collection.slice(0, limit)), } return chain } @@ -74,9 +75,11 @@ export function createReportsCtx({ cb?.(noopIndexBuilder) return { collect: vi.fn(async () => queues), + take: vi.fn(async (limit: number) => queues.slice(0, limit)), } }), collect: vi.fn(async () => queues), + take: vi.fn(async (limit: number) => queues.slice(0, limit)), } } @@ -86,9 +89,11 @@ export function createReportsCtx({ cb?.(noopIndexBuilder) return { collect: vi.fn(async () => categories), + take: vi.fn(async (limit: number) => categories.slice(0, limit)), } }), collect: vi.fn(async () => categories), + take: vi.fn(async (limit: number) => categories.slice(0, limit)), } } @@ -103,8 +108,10 @@ export function createReportsCtx({ }, } cb?.(builder as { eq: (field: unknown, value: unknown) => unknown }) + const events = ticketId ? ticketEventsByTicket.get(ticketId) ?? [] : [] return { - collect: vi.fn(async () => (ticketId ? ticketEventsByTicket.get(ticketId) ?? [] : [])), + collect: vi.fn(async () => events), + take: vi.fn(async (limit: number) => events.slice(0, limit)), } }), } @@ -121,8 +128,10 @@ export function createReportsCtx({ }, } cb?.(builder as { eq: (field: unknown, value: unknown) => unknown }) + const sessions = agentId ? ticketWorkSessionsByAgent.get(agentId) ?? [] : [] return { - collect: vi.fn(async () => (agentId ? ticketWorkSessionsByAgent.get(agentId) ?? [] : [])), + collect: vi.fn(async () => sessions), + take: vi.fn(async (limit: number) => sessions.slice(0, limit)), } }), } @@ -131,8 +140,10 @@ export function createReportsCtx({ return { withIndex: vi.fn(() => ({ collect: vi.fn(async () => []), + take: vi.fn(async () => []), })), collect: vi.fn(async () => []), + take: vi.fn(async () => []), } }), }