import { NextRequest } from "next/server"; import { auth } from "@/lib/auth"; import { probeStatus } from "@/lib/server-status"; import { readChatMessages, logMtime } from "@/lib/chat-log"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; const STATUS_INTERVAL_MS = 5000; const LOG_POLL_MS = 1500; const HEARTBEAT_MS = 15_000; const MAX_LIFETIME_MS = 10 * 60 * 1000; export async function GET(req: NextRequest) { const session = await auth(); if (!session) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 403, headers: { "Content-Type": "application/json" }, }); } const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { let closed = false; const timers: ReturnType[] = []; const safeSend = (data: string) => { if (closed) return; try { controller.enqueue(encoder.encode(data)); } catch { closed = true; } }; const send = (event: string, payload: unknown) => safeSend(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`); const heartbeat = () => safeSend(`: hb ${Date.now()}\n\n`); const cleanup = () => { if (closed) return; closed = true; for (const t of timers) clearTimeout(t); try { controller.close(); } catch {} }; req.signal.addEventListener("abort", cleanup); // Initial payload try { const status = await probeStatus(); send("status", status); const chat = readChatMessages(50); send("chat", chat); } catch {} let lastLogMtime = logMtime(); const pollStatus = async () => { if (closed) return; try { const status = await probeStatus(); send("status", status); } catch {} if (!closed) timers.push(setTimeout(pollStatus, STATUS_INTERVAL_MS)); }; const pollLog = () => { if (closed) return; try { const mt = logMtime(); if (mt && mt !== lastLogMtime) { lastLogMtime = mt; const chat = readChatMessages(50); send("chat", chat); } } catch {} if (!closed) timers.push(setTimeout(pollLog, LOG_POLL_MS)); }; const beat = () => { if (closed) return; heartbeat(); if (!closed) timers.push(setTimeout(beat, HEARTBEAT_MS)); }; timers.push(setTimeout(pollStatus, STATUS_INTERVAL_MS)); timers.push(setTimeout(pollLog, LOG_POLL_MS)); timers.push(setTimeout(beat, HEARTBEAT_MS)); // Hard cap stream lifetime so auth/session stays fresh on reconnect timers.push(setTimeout(cleanup, MAX_LIFETIME_MS)); }, }); return new Response(stream, { headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", "X-Accel-Buffering": "no", }, }); }