(
@Param('runId') runId: string,
@Query(new ZodValidationPipe(StreamRunQuerySchema)) query: StreamRunQueryDto,
@CurrentAuth() auth: AuthContext | null,
@Res() res: Response,
@Req() req: Request,
)
| 861 | @Get('/runs/:runId/stream') |
| 862 | @ApiOkResponse({ description: 'Server-sent events stream for workflow run updates' }) |
| 863 | async stream( |
| 864 | @Param('runId') runId: string, |
| 865 | @Query(new ZodValidationPipe(StreamRunQuerySchema)) query: StreamRunQueryDto, |
| 866 | @CurrentAuth() auth: AuthContext | null, |
| 867 | @Res() res: Response, |
| 868 | @Req() req: Request, |
| 869 | ): Promise<void> { |
| 870 | // Auth is now handled via headers (Authorization and X-Organization-Id) |
| 871 | // using a fetch-based SSE client that supports custom headers |
| 872 | res.setHeader('Content-Type', 'text/event-stream'); |
| 873 | res.setHeader('Cache-Control', 'no-cache'); |
| 874 | res.setHeader('Connection', 'keep-alive'); |
| 875 | if (typeof (res as any).flushHeaders === 'function') { |
| 876 | (res as any).flushHeaders(); |
| 877 | } |
| 878 | |
| 879 | await this.workflowsService.ensureRunAccess(runId, auth); |
| 880 | |
| 881 | let lastSequence = Number.parseInt(query.cursor ?? '0', 10); |
| 882 | let terminalCursor = query.terminalCursor; |
| 883 | let lastLogCursor = query.logCursor ?? null; |
| 884 | if (Number.isNaN(lastSequence) || lastSequence < 0) { |
| 885 | lastSequence = 0; |
| 886 | } |
| 887 | |
| 888 | let active = true; |
| 889 | let lastStatusSignature: string | null = null; |
| 890 | let intervalId: NodeJS.Timeout | undefined; |
| 891 | // eslint-disable-next-line prefer-const -- initialized later after cleanup function is defined |
| 892 | let heartbeatId: NodeJS.Timeout | undefined; |
| 893 | let earliestEventTimestamp: number | null = null; |
| 894 | let latestEventTimestamp: number | null = null; |
| 895 | |
| 896 | const send = (event: string, payload: unknown) => { |
| 897 | if (!active) { |
| 898 | return; |
| 899 | } |
| 900 | try { |
| 901 | res.write(`event: ${event}\n`); |
| 902 | res.write(`data: ${JSON.stringify(payload)}\n\n`); |
| 903 | // Flush headers if available (helps with immediate delivery) |
| 904 | if (typeof (res as any).flush === 'function') { |
| 905 | (res as any).flush(); |
| 906 | } |
| 907 | } catch (error) { |
| 908 | // Connection closed or error writing |
| 909 | console.warn(`Failed to send SSE event ${event}:`, error); |
| 910 | if (!active) { |
| 911 | return; |
| 912 | } |
| 913 | active = false; |
| 914 | void cleanup(); |
| 915 | } |
| 916 | }; |
| 917 | |
| 918 | const cleanup = async () => { |
| 919 | if (!active) { |
| 920 | return; |
no test coverage detected