diff --git a/apps/web/src/app/api/sse/timeline/route.ts b/apps/web/src/app/api/sse/timeline/route.ts index 2b4e085..acaf4d4 100644 --- a/apps/web/src/app/api/sse/timeline/route.ts +++ b/apps/web/src/app/api/sse/timeline/route.ts @@ -5,13 +5,14 @@ import { prisma } from "@capakraken/db"; import { SSE_EVENT_TYPES, SystemRole, type PermissionOverrides } from "@capakraken/shared"; import { auth } from "~/server/auth.js"; -// Start the reminder scheduler (idempotent — only starts once) -startReminderScheduler(); - export const dynamic = "force-dynamic"; export const runtime = "nodejs"; export async function GET() { + // Start lazily on the first real SSE request so builds/import-time evaluation + // never attempt reminder processing against a live database. + startReminderScheduler(); + const session = await auth(); if (!session?.user) { diff --git a/apps/web/src/hooks/useTimelineSSE.ts b/apps/web/src/hooks/useTimelineSSE.ts index 7f7de54..ad0a785 100644 --- a/apps/web/src/hooks/useTimelineSSE.ts +++ b/apps/web/src/hooks/useTimelineSSE.ts @@ -15,10 +15,27 @@ export function useTimelineSSE() { useEffect(() => { let es: EventSource | null = null; let reconnectAttempts = 0; + let isDisposed = false; + + function scheduleReconnect() { + if (isDisposed || reconnectTimeout.current) return; + reconnectAttempts++; + const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); + reconnectTimeout.current = setTimeout(() => { + reconnectTimeout.current = null; + if (isDisposed) return; + connect(); + }, delay); + } function connect() { + if (isDisposed) return; es = new EventSource("/api/sse/timeline"); + es.onopen = () => { + reconnectAttempts = 0; + }; + es.onmessage = (event) => { try { const data = JSON.parse(event.data as string) as { type: string }; @@ -35,6 +52,7 @@ export function useTimelineSSE() { void queryClient.invalidateQueries({ queryKey: [["timeline", "getMyEntriesView"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getHolidayOverlays"]] }); void queryClient.invalidateQueries({ queryKey: [["timeline", "getMyHolidayOverlays"]] }); + void queryClient.invalidateQueries({ queryKey: [["vacation", "list"]] }); void queryClient.invalidateQueries({ queryKey: [["allocation", "list"]] }); break; @@ -82,18 +100,18 @@ export function useTimelineSSE() { es.onerror = () => { es?.close(); - reconnectAttempts++; - const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); - reconnectTimeout.current = setTimeout(connect, delay); + scheduleReconnect(); }; } connect(); return () => { + isDisposed = true; es?.close(); if (reconnectTimeout.current) { clearTimeout(reconnectTimeout.current); + reconnectTimeout.current = null; } }; }, [queryClient]);