@@ -301,7 +301,17 @@ export class HttpStreamTransport extends AbstractTransport {
301
301
logger . info ( `Initialized new session: ${ newSessionId } via stream` ) ;
302
302
}
303
303
304
- const sseConnection = this . setupSSEConnection ( req , res , newSessionId || session ?. id , undefined , additionalHeaders ) ;
304
+ const requestIds = new Set < string | number > ( ) ;
305
+ clientRequests . forEach ( req => requestIds . add ( req . id ) ) ;
306
+
307
+ const sseConnection = this . setupSSEConnection ( req , res , newSessionId || session ?. id , undefined , additionalHeaders , true ) ;
308
+
309
+ if ( requestIds . size > 0 ) {
310
+ sseConnection . pendingResponseIds = requestIds ;
311
+ logger . debug ( `Stream mode: Tracking ${ requestIds . size } pending responses for stream ${ sseConnection . streamId } ` ) ;
312
+ } else {
313
+ logger . debug ( `Stream mode: No request IDs to track for stream ${ sseConnection . streamId } . Connection will remain open.` ) ;
314
+ }
305
315
306
316
if ( newSessionId ) {
307
317
sseConnection . sessionId = newSessionId ;
@@ -380,7 +390,7 @@ export class HttpStreamTransport extends AbstractTransport {
380
390
logger . warn ( `Client sent Last-Event-ID (${ lastEventId } ) but resumability is disabled.` ) ;
381
391
}
382
392
383
- this . setupSSEConnection ( req , res , session ?. id , lastEventId ) ;
393
+ this . setupSSEConnection ( req , res , session ?. id , lastEventId , { } , false ) ;
384
394
logger . debug ( `Established SSE stream for GET request (Session: ${ session ?. id || 'initialization phase' } )` ) ;
385
395
}
386
396
@@ -398,17 +408,30 @@ export class HttpStreamTransport extends AbstractTransport {
398
408
res . writeHead ( 200 , { 'Content-Type' : 'text/plain' } ) . end ( "Session terminated" ) ;
399
409
}
400
410
401
- private setupSSEConnection ( req : IncomingMessage , res : ServerResponse , sessionId ?: string , lastEventId ?: string , additionalHeaders : Record < string , string > = { } ) : ActiveSseConnection {
411
+ private setupSSEConnection (
412
+ req : IncomingMessage ,
413
+ res : ServerResponse ,
414
+ sessionId ?: string ,
415
+ lastEventId ?: string ,
416
+ additionalHeaders : Record < string , string > = { } ,
417
+ isPostConnection : boolean = false
418
+ ) : ActiveSseConnection {
402
419
const streamId = randomUUID ( ) ;
403
420
const connection : ActiveSseConnection = {
404
421
res, sessionId, streamId, lastEventIdSent : null ,
405
- messageHistory : this . _config . resumability . enabled ? [ ] : undefined , pingInterval : undefined
422
+ messageHistory : this . _config . resumability . enabled ? [ ] : undefined ,
423
+ pingInterval : undefined ,
424
+ isPostConnection
406
425
} ;
407
426
408
427
const headers = { ...SSE_HEADERS , ...additionalHeaders } ;
409
428
res . writeHead ( 200 , headers ) ;
410
429
411
- logger . debug ( `SSE stream ${ streamId } setup (Session: ${ sessionId || 'N/A' } )` ) ;
430
+ const originInfo = isPostConnection ?
431
+ `POST (will close after responses)` :
432
+ `GET (persistent until client disconnects)` ;
433
+
434
+ logger . debug ( `SSE stream ${ streamId } setup (Session: ${ sessionId || 'N/A' } , Origin: ${ originInfo } )` ) ;
412
435
if ( res . socket ) { res . socket . setNoDelay ( true ) ; res . socket . setKeepAlive ( true ) ; res . socket . setTimeout ( 0 ) ; logger . debug ( `Optimized socket for SSE stream ${ streamId } ` ) ; }
413
436
else { logger . warn ( `Could not access socket for SSE stream ${ streamId } to optimize.` ) ; }
414
437
this . _activeSseConnections . add ( connection ) ;
@@ -421,7 +444,7 @@ export class HttpStreamTransport extends AbstractTransport {
421
444
res . on ( "close" , ( ) => cleanupHandler ( "Client closed connection" ) ) ;
422
445
res . on ( "error" , ( err ) => { logger . error ( `SSE stream ${ streamId } error: ${ err . message } ` ) ; cleanupHandler ( `Connection error: ${ err . message } ` ) ; this . _onerror ?.( err ) ; } ) ;
423
446
res . on ( "finish" , ( ) => cleanupHandler ( "Stream finished" ) ) ;
424
- logger . info ( `SSE stream ${ streamId } active (Session: ${ sessionId || 'N/A' } , Total: ${ this . _activeSseConnections . size } )` ) ;
447
+ logger . info ( `SSE stream ${ streamId } active (Session: ${ sessionId || 'N/A' } , Origin: ${ originInfo } , Total: ${ this . _activeSseConnections . size } )` ) ;
425
448
return connection ;
426
449
}
427
450
@@ -439,6 +462,23 @@ export class HttpStreamTransport extends AbstractTransport {
439
462
logger . debug ( `Total active SSE connections after cleanup: ${ this . _activeSseConnections . size } ` ) ;
440
463
}
441
464
465
+ /**
466
+ * Checks if a POST-initiated SSE connection has completed all responses.
467
+ * If all responses have been sent, closes the connection as per spec recommendation.
468
+ */
469
+ private checkAndCloseCompletedPostConnection ( connection : ActiveSseConnection ) : void {
470
+ if ( ! connection . isPostConnection || ! connection . pendingResponseIds ) {
471
+ return ;
472
+ }
473
+
474
+ if ( connection . pendingResponseIds . size > 0 ) {
475
+ return ;
476
+ }
477
+
478
+ logger . info ( `POST-initiated SSE stream ${ connection . streamId } has sent all responses. Closing as per spec recommendation.` ) ;
479
+ this . cleanupConnection ( connection , "All responses sent" ) ;
480
+ }
481
+
442
482
private cleanupAllConnections ( ) : void {
443
483
logger . info ( `Cleaning up all ${ this . _activeSseConnections . size } active SSE connections and ${ this . _pendingBatches . size } pending batches.` ) ;
444
484
Array . from ( this . _activeSseConnections ) . forEach ( conn => this . cleanupConnection ( conn , "Server shutting down" ) ) ;
@@ -493,19 +533,37 @@ export class HttpStreamTransport extends AbstractTransport {
493
533
if ( targetConnection ) {
494
534
this . _requestStreamMap . delete ( message . id ) ;
495
535
logger . debug ( `Stream mode: Found target stream ${ targetConnection . streamId } for response ID ${ message . id } ` ) ;
536
+
537
+ if ( targetConnection . pendingResponseIds && targetConnection . pendingResponseIds . has ( message . id ) ) {
538
+ targetConnection . pendingResponseIds . delete ( message . id ) ;
539
+ logger . debug ( `Stream ${ targetConnection . streamId } : Removed ID ${ message . id } from pending responses. Remaining: ${ targetConnection . pendingResponseIds . size } ` ) ;
540
+ }
496
541
} else {
497
542
logger . warn ( `Stream mode: No active stream found mapping to response ID ${ message . id } . Message dropped.` ) ;
498
543
return ;
499
544
}
500
- }
545
+ } else {
546
+ targetConnection = Array . from ( this . _activeSseConnections )
547
+ . filter ( c => {
548
+ return isResponse ( message ) ? c . isPostConnection : true ;
549
+ } )
550
+ . find ( c => c . res && ! c . res . writableEnded ) ;
501
551
502
- if ( ! targetConnection ) {
503
- targetConnection = Array . from ( this . _activeSseConnections ) . find ( c => c . res && ! c . res . writableEnded ) ;
504
- if ( targetConnection ) logger . debug ( `Stream mode: No specific target, selected available stream ${ targetConnection . streamId } ` ) ;
552
+ if ( targetConnection ) {
553
+ if ( isResponse ( message ) ) {
554
+ logger . debug ( `Stream mode: Using POST-originated stream ${ targetConnection . streamId } for response` ) ;
555
+ } else {
556
+ logger . debug ( `Stream mode: Selected available stream ${ targetConnection . streamId } for request/notification` ) ;
557
+ }
558
+ }
505
559
}
506
560
507
561
if ( ! targetConnection || ! targetConnection . res || targetConnection . res . writableEnded ) {
508
- logger . error ( `Cannot send message via SSE: No suitable stream found. Message dropped: ${ JSON . stringify ( message ) } ` ) ;
562
+ if ( isResponse ( message ) ) {
563
+ logger . error ( `Cannot send response message via SSE: No suitable POST-originated stream found. Message dropped: ${ JSON . stringify ( message ) } ` ) ;
564
+ } else {
565
+ logger . error ( `Cannot send request/notification message via SSE: No suitable stream found. Message dropped: ${ JSON . stringify ( message ) } ` ) ;
566
+ }
509
567
return ;
510
568
}
511
569
@@ -525,6 +583,10 @@ export class HttpStreamTransport extends AbstractTransport {
525
583
}
526
584
logger . debug ( `Sending SSE data on stream ${ targetConnection . streamId } : ${ JSON . stringify ( message ) } ` ) ;
527
585
targetConnection . res . write ( `data: ${ JSON . stringify ( message ) } \n\n` ) ;
586
+
587
+ if ( isResponse ( message ) ) {
588
+ this . checkAndCloseCompletedPostConnection ( targetConnection ) ;
589
+ }
528
590
} catch ( error : any ) {
529
591
logger . error ( `Error writing to SSE stream ${ targetConnection . streamId } : ${ error . message } . Cleaning up connection.` ) ;
530
592
this . cleanupConnection ( targetConnection , `Write error: ${ error . message } ` ) ;
0 commit comments