import fs from "fs"; import { getRun } from "@/lib/db"; import { jobManager } from "@/lib/jobs"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const encoder = new TextEncoder(); function sseLine(line: string, ts: number): Uint8Array { return encoder.encode(`data: ${JSON.stringify({ line, ts })}\n\n`); } function sseDone(status: string): Uint8Array { return encoder.encode(`event: done\ndata: ${JSON.stringify({ status })}\n\n`); } export async function GET( _request: Request, { params }: { params: Promise<{ id: string }> } ) { try { const { id } = await params; const runId = Number(id); const run = getRun(runId); if (!run) { return Response.json({ error: "run not found" }, { status: 404 }); } let cleanup: () => void = () => {}; const stream = new ReadableStream({ start(controller) { let unsubscribe: (() => void) | null = null; let closed = false; const close = (status: string) => { if (closed) return; closed = true; try { controller.enqueue(sseDone(status)); controller.close(); } catch { // client already gone } }; const replayFile = () => { if (run.log_path && fs.existsSync(run.log_path)) { const content = fs.readFileSync(run.log_path, "utf8"); for (const line of content.split("\n")) { if (line) controller.enqueue(sseLine(line, 0)); } } close(run.status); }; if (run.status === "running" || run.status === "queued") { unsubscribe = jobManager.subscribe( runId, (ev) => { if (!closed) controller.enqueue(sseLine(ev.line, ev.ts)); }, (status) => close(status) ); if (!unsubscribe) { // Not active in this process (e.g. after HMR): fall back to file. replayFile(); return; } cleanup = () => { closed = true; unsubscribe?.(); }; } else { replayFile(); } }, cancel() { cleanup(); }, }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }, }); } catch (err) { return Response.json({ error: (err as Error).message }, { status: 500 }); } }