nyk
fix: reduce server memory footprint (#405)
c963447 unverified
Raw
History Blame Contribute Delete
2.59 kB
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
},
})
}