Spaces:
Runtime error
Runtime error
| import { NextRequest , NextResponse } from 'next/server' | |
| import { eventBus, ServerEvent } from '@/lib/event-bus' | |
| import { requireRole } from '@/lib/auth' | |
| export const dynamic = 'force-dynamic' | |
| export const runtime = 'nodejs' | |
| /** | |
| * GET /api/events - Server-Sent Events stream for real-time DB mutations. | |
| * Clients connect via EventSource and receive JSON-encoded events. | |
| */ | |
| export async function GET(request: NextRequest) { | |
| const auth = requireRole(request, 'viewer') | |
| if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) | |
| const encoder = new TextEncoder() | |
| // Cleanup function, set in start(), called in cancel() | |
| let cleanup: (() => void) | null = null | |
| const stream = new ReadableStream({ | |
| start(controller) { | |
| // Send initial connection event | |
| controller.enqueue( | |
| encoder.encode(`data: ${JSON.stringify({ type: 'connected', data: null, timestamp: Date.now() })}\n\n`) | |
| ) | |
| // Forward workspace-scoped server events to this SSE client | |
| const userWorkspaceId = auth.user.workspace_id ?? 1 | |
| const handler = (event: ServerEvent) => { | |
| // Skip events from other workspaces (if event carries workspace_id) | |
| if (event.data?.workspace_id && event.data.workspace_id !== userWorkspaceId) return | |
| try { | |
| controller.enqueue( | |
| encoder.encode(`data: ${JSON.stringify(event)}\n\n`) | |
| ) | |
| } catch { | |
| // Client disconnected, cleanup will happen in cancel() | |
| } | |
| } | |
| eventBus.on('server-event', handler) | |
| // Heartbeat every 30s to keep connection alive through proxies | |
| const heartbeat = setInterval(() => { | |
| try { | |
| controller.enqueue(encoder.encode(': heartbeat\n\n')) | |
| } catch { | |
| clearInterval(heartbeat) | |
| } | |
| }, 30_000) | |
| cleanup = () => { | |
| eventBus.off('server-event', handler) | |
| clearInterval(heartbeat) | |
| } | |
| }, | |
| cancel() { | |
| if (cleanup) { | |
| cleanup() | |
| cleanup = null | |
| } | |
| }, | |
| }) | |
| // Defense-in-depth: if the request is aborted (proxy timeout, network drop) | |
| // ensure we clean up the event listener even if cancel() doesn't fire. | |
| request.signal.addEventListener('abort', () => { | |
| if (cleanup) { | |
| cleanup() | |
| cleanup = null | |
| } | |
| }, { once: true }) | |
| return new Response(stream, { | |
| headers: { | |
| 'Content-Type': 'text/event-stream', | |
| 'Cache-Control': 'no-cache, no-transform', | |
| Connection: 'keep-alive', | |
| 'X-Accel-Buffering': 'no', // Disable nginx buffering | |
| }, | |
| }) | |
| } | |