import { NextRequest } from "next/server"; import { getServerSession } from "@/lib/auth/guards"; import { prisma } from "@/lib/db"; import { isTerminal } from "@/lib/episodes/status"; export const dynamic = "force-dynamic"; /** * Server-Sent Events stream of an episode's generation status. Polls the row * every 1.5s and emits on change until the episode reaches a terminal state. * (LISTEN/NOTIFY is a future optimization; polling is simpler and robust.) */ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { const { id } = await params; const session = await getServerSession(); if (!session) return new Response("Unauthorized", { status: 401 }); const ep = await prisma.episode.findUnique({ where: { id }, select: { userId: true } }); if (!ep) return new Response("Not found", { status: 404 }); if (ep.userId !== session.user.id && session.user.role !== "admin") { return new Response("Forbidden", { status: 403 }); } const encoder = new TextEncoder(); const stream = new ReadableStream({ start(controller) { let lastSig = ""; let stopped = false; let pollTimer: ReturnType; let pingTimer: ReturnType; const send = (data: unknown) => { if (!stopped) controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); }; const stop = () => { if (stopped) return; stopped = true; clearInterval(pollTimer); clearInterval(pingTimer); try { controller.close(); } catch { /* already closed */ } }; const poll = async () => { if (stopped) return; const e = await prisma.episode.findUnique({ where: { id }, select: { status: true, stage: true, errorMessage: true }, }); if (!e) { send({ status: "FAILED", error: "Episode not found" }); stop(); return; } const sig = `${e.status}:${e.stage ?? ""}`; if (sig !== lastSig) { lastSig = sig; send({ status: e.status, stage: e.stage, error: e.errorMessage }); } if (isTerminal(e.status)) stop(); }; send({ type: "open" }); void poll(); pollTimer = setInterval(poll, 1500); pingTimer = setInterval(() => { if (!stopped) controller.enqueue(encoder.encode(": ping\n\n")); }, 15000); req.signal.addEventListener("abort", stop); }, }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }, }); }