(config: WorkspaceSSEConfig)
| 26 | const HEARTBEAT_INTERVAL_MS = 30_000 |
| 27 | |
| 28 | export function createWorkspaceSSE(config: WorkspaceSSEConfig) { |
| 29 | const logger = createLogger(`${config.label}-SSE`) |
| 30 | |
| 31 | return async function GET(request: NextRequest): Promise<Response> { |
| 32 | const session = await getSession() |
| 33 | if (!session?.user?.id) { |
| 34 | return new Response('Unauthorized', { status: 401 }) |
| 35 | } |
| 36 | |
| 37 | const { searchParams } = new URL(request.url) |
| 38 | const workspaceId = searchParams.get('workspaceId') |
| 39 | if (!workspaceId) { |
| 40 | return new Response('Missing workspaceId query parameter', { status: 400 }) |
| 41 | } |
| 42 | |
| 43 | const permissions = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) |
| 44 | if (!permissions) { |
| 45 | return new Response('Access denied to workspace', { status: 403 }) |
| 46 | } |
| 47 | |
| 48 | const encoder = new TextEncoder() |
| 49 | const unsubscribers: Array<() => void> = [] |
| 50 | let cleaned = false |
| 51 | |
| 52 | const cleanup = () => { |
| 53 | if (cleaned) return |
| 54 | cleaned = true |
| 55 | for (const unsub of unsubscribers) { |
| 56 | unsub() |
| 57 | } |
| 58 | logger.info(`SSE connection closed for workspace ${workspaceId}`) |
| 59 | } |
| 60 | |
| 61 | const stream = new ReadableStream({ |
| 62 | start(controller) { |
| 63 | const send = (eventName: string, data: Record<string, unknown>) => { |
| 64 | if (cleaned) return |
| 65 | try { |
| 66 | controller.enqueue( |
| 67 | encoder.encode(`event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`) |
| 68 | ) |
| 69 | } catch { |
| 70 | // Stream already closed |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | for (const subscription of config.subscriptions) { |
| 75 | const unsub = subscription.subscribe(workspaceId, send) |
| 76 | unsubscribers.push(unsub) |
| 77 | } |
| 78 | |
| 79 | const heartbeat = setInterval(() => { |
| 80 | if (cleaned) { |
| 81 | clearInterval(heartbeat) |
| 82 | return |
| 83 | } |
| 84 | try { |
| 85 | controller.enqueue(encoder.encode(': heartbeat\n\n')) |
no test coverage detected