1- import { Controller , Get , Param , UseGuards } from '@nestjs/common' ;
1+ import {
2+ Controller , Get , Param , UseGuards ,
3+ Sse , Res , NotFoundException ,
4+ } from '@nestjs/common' ;
25import { ApiTags , ApiBearerAuth } from '@nestjs/swagger' ;
6+ import { OnEvent } from '@nestjs/event-emitter' ;
7+ import { Response } from 'express' ;
38import { ExecutionsService } from './executions.service' ;
49import { JwtAuthGuard } from '../common/guards/jwt-auth.guard' ;
510import { CurrentWorkspaceId } from '../common/decorators/current-user.decorator' ;
@@ -20,4 +25,64 @@ export class ExecutionsController {
2025 findOne ( @Param ( 'id' ) id : string ) {
2126 return this . executionsService . findOne ( id ) ;
2227 }
28+
29+ @Get ( ':id/stream' )
30+ async stream (
31+ @Param ( 'id' ) id : string ,
32+ @Res ( ) res : Response ,
33+ ) : Promise < void > {
34+ const execution = await this . executionsService . findOne ( id ) ;
35+ if ( ! execution ) throw new NotFoundException ( `Execution ${ id } not found` ) ;
36+
37+ // SSE headers
38+ res . setHeader ( 'Content-Type' , 'text/event-stream' ) ;
39+ res . setHeader ( 'Cache-Control' , 'no-cache' ) ;
40+ res . setHeader ( 'Connection' , 'keep-alive' ) ;
41+ res . setHeader ( 'Access-Control-Allow-Origin' , '*' ) ;
42+ res . flushHeaders ( ) ;
43+
44+ const send = ( event : string , data : unknown ) => {
45+ res . write ( `event: ${ event } \ndata: ${ JSON . stringify ( data ) } \n\n` ) ;
46+ } ;
47+
48+ // Send existing logs immediately
49+ if ( execution . logs ?. length ) {
50+ for ( const log of execution . logs ) {
51+ send ( 'log' , { ...( log as Record < string , unknown > ) , executionId : id } ) ;
52+ }
53+ }
54+
55+ // Send current status
56+ send ( 'status' , { executionId : id , status : execution . status } ) ;
57+
58+ // Store listeners so we can remove them on close
59+ const onLog = ( payload : Record < string , unknown > ) => {
60+ if ( payload [ 'executionId' ] === id ) send ( 'log' , payload ) ;
61+ } ;
62+
63+ const onStatus = ( payload : Record < string , unknown > ) => {
64+ if ( payload [ 'executionId' ] === id ) {
65+ send ( 'status' , payload ) ;
66+ const terminal = [ 'SUCCESS' , 'FAILED' , 'CANCELLED' ] ;
67+ if ( terminal . includes ( String ( payload [ 'status' ] ) ) ) {
68+ send ( 'done' , { executionId : id } ) ;
69+ res . end ( ) ;
70+ }
71+ }
72+ } ;
73+
74+ this . executionsService . eventEmitter . on ( 'execution.log' , onLog ) ;
75+ this . executionsService . eventEmitter . on ( 'execution.status' , onStatus ) ;
76+
77+ // Heartbeat every 15s to keep connection alive
78+ const heartbeat = setInterval ( ( ) => {
79+ res . write ( ': heartbeat\n\n' ) ;
80+ } , 15_000 ) ;
81+
82+ res . on ( 'close' , ( ) => {
83+ clearInterval ( heartbeat ) ;
84+ this . executionsService . eventEmitter . off ( 'execution.log' , onLog ) ;
85+ this . executionsService . eventEmitter . off ( 'execution.status' , onStatus ) ;
86+ } ) ;
87+ }
2388}
0 commit comments