import { NextRequest } from 'next/server' import { prisma } from '@/lib/prisma' import { auth } from '@/lib/auth' export const dynamic = 'force-dynamic' export async function GET(request: NextRequest): Promise { // Require authentication — prevent unauthenticated access to live vote data const userSession = await auth() if (!userSession?.user) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }) } const { searchParams } = new URL(request.url) const sessionId = searchParams.get('sessionId') if (!sessionId) { return new Response(JSON.stringify({ error: 'sessionId is required' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }) } // Verify the session exists const session = await prisma.liveVotingSession.findUnique({ where: { id: sessionId }, select: { id: true, status: true }, }) if (!session) { return new Response(JSON.stringify({ error: 'Session not found' }), { status: 404, headers: { 'Content-Type': 'application/json' }, }) } const encoder = new TextEncoder() const stream = new ReadableStream({ async start(controller) { // Track state for change detection let lastVoteCount = -1 let lastAudienceVoteCount = -1 let lastProjectId: string | null = null let lastStatus: string | null = null const sendEvent = (event: string, data: unknown) => { const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` controller.enqueue(encoder.encode(payload)) } // Send initial connection event sendEvent('connected', { sessionId, timestamp: new Date().toISOString() }) const poll = async () => { try { const currentSession = await prisma.liveVotingSession.findUnique({ where: { id: sessionId }, select: { status: true, currentProjectId: true, currentProjectIndex: true, votingEndsAt: true, allowAudienceVotes: true, }, }) if (!currentSession) { sendEvent('session_status', { status: 'DELETED' }) controller.close() return false } // Check for status changes if (lastStatus !== null && currentSession.status !== lastStatus) { sendEvent('session_status', { status: currentSession.status, timestamp: new Date().toISOString(), }) } lastStatus = currentSession.status // Check for project changes if ( lastProjectId !== null && currentSession.currentProjectId !== lastProjectId ) { sendEvent('project_change', { projectId: currentSession.currentProjectId, projectIndex: currentSession.currentProjectIndex, timestamp: new Date().toISOString(), }) } lastProjectId = currentSession.currentProjectId // Check for vote updates on the current project if (currentSession.currentProjectId) { // Jury votes const juryVoteCount = await prisma.liveVote.count({ where: { sessionId, projectId: currentSession.currentProjectId, isAudienceVote: false, }, }) if (lastVoteCount !== -1 && juryVoteCount !== lastVoteCount) { const latestVotes = await prisma.liveVote.findMany({ where: { sessionId, projectId: currentSession.currentProjectId, isAudienceVote: false, }, select: { score: true, isAudienceVote: true, votedAt: true, }, orderBy: { votedAt: 'desc' }, take: 1, }) const avgScore = await prisma.liveVote.aggregate({ where: { sessionId, projectId: currentSession.currentProjectId, isAudienceVote: false, }, _avg: { score: true }, _count: true, }) sendEvent('vote_update', { projectId: currentSession.currentProjectId, totalVotes: juryVoteCount, averageScore: avgScore._avg.score, latestVote: latestVotes[0] || null, timestamp: new Date().toISOString(), }) } lastVoteCount = juryVoteCount // Audience votes (separate event) if (currentSession.allowAudienceVotes) { const audienceVoteCount = await prisma.liveVote.count({ where: { sessionId, projectId: currentSession.currentProjectId, isAudienceVote: true, }, }) if (lastAudienceVoteCount !== -1 && audienceVoteCount !== lastAudienceVoteCount) { const audienceAvg = await prisma.liveVote.aggregate({ where: { sessionId, projectId: currentSession.currentProjectId, isAudienceVote: true, }, _avg: { score: true }, }) sendEvent('audience_vote', { projectId: currentSession.currentProjectId, audienceVotes: audienceVoteCount, audienceAverage: audienceAvg._avg.score, timestamp: new Date().toISOString(), }) } lastAudienceVoteCount = audienceVoteCount } } // Stop polling if session is completed if (currentSession.status === 'COMPLETED') { sendEvent('session_status', { status: 'COMPLETED', timestamp: new Date().toISOString(), }) controller.close() return false } return true } catch (error) { console.error('[SSE] Poll error:', error) return true // Keep trying } } // Initial poll to set baseline state const shouldContinue = await poll() if (!shouldContinue) return // Poll every 2 seconds const interval = setInterval(async () => { const cont = await poll() if (!cont) { clearInterval(interval) } }, 2000) // Clean up on abort request.signal.addEventListener('abort', () => { clearInterval(interval) try { controller.close() } catch { // Stream may already be closed } }) }, }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }) }