Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
This commit is contained in:
@@ -1,215 +1,215 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest): Promise<Response> {
|
||||
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',
|
||||
},
|
||||
})
|
||||
}
|
||||
import { NextRequest } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest): Promise<Response> {
|
||||
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',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user