@@ -11,6 +11,7 @@ import { delay } from '~/core/delay'
1111import { getTimestamp } from '~/core/utils/logging/getTimestamp'
1212import { devUtils } from '~/core/utils/internal/devUtils'
1313import { colors } from '~/core/ws/utils/attachWebSocketLogger'
14+ import { toPublicUrl } from '~/core/utils/request/toPublicUrl'
1415
1516type EventMapConstraint = {
1617 message ?: unknown
@@ -61,15 +62,38 @@ class ServerSentEventHandler<
6162 constructor ( path : Path , resolver : ServerSentEventResolver < EventMap , any > ) {
6263 invariant (
6364 typeof EventSource !== 'undefined' ,
64- 'Failed to construct a Server-Sent Event handler for path "%s": your environment does not support the EventSource API ' ,
65+ 'Failed to construct a Server-Sent Event handler for path "%s": the EventSource API is not supported in this environment ' ,
6566 path ,
6667 )
6768
6869 const clientEmitter = new Emitter < ServerSentEventClientEventMap > ( )
6970
70- super ( 'GET' , path , ( info ) => {
71+ super ( 'GET' , path , async ( info ) => {
72+ const responseInit : ResponseInit = {
73+ headers : {
74+ 'content-type' : 'text/event-stream' ,
75+ 'cache-control' : 'no-cache' ,
76+ connection : 'keep-alive' ,
77+ } ,
78+ }
79+
80+ /**
81+ * @note Log the intercepted request early.
82+ * Normally, the `this.log()` method is called when the handler returns a response.
83+ * For SSE, call that method earlier so the logs are in correct order.
84+ */
85+ await super . log ( {
86+ request : info . request ,
87+ /**
88+ * @note Construct a placeholder response since SSE response
89+ * is being streamed and cannot be cloned/consumed for logging.
90+ */
91+ response : new Response ( '[streaming]' , responseInit ) ,
92+ } )
93+ this . #attachClientLogger( info . request , clientEmitter )
94+
7195 const stream = new ReadableStream ( {
72- start ( controller ) {
96+ async start ( controller ) {
7397 const client = new ServerSentEventClient < EventMap > ( {
7498 controller,
7599 emitter : clientEmitter ,
@@ -79,24 +103,16 @@ class ServerSentEventHandler<
79103 client,
80104 } )
81105
82- resolver ( {
106+ await resolver ( {
83107 ...info ,
84108 client,
85109 server,
86110 } )
87111 } ,
88112 } )
89113
90- return new Response ( stream , {
91- headers : {
92- 'content-type' : 'text/event-stream' ,
93- 'cache-control' : 'no-cache' ,
94- connection : 'keep-alive' ,
95- } ,
96- } )
114+ return new Response ( stream , responseInit )
97115 } )
98-
99- this . attachClientLogger ( clientEmitter )
100116 }
101117
102118 predicate ( args : {
@@ -110,30 +126,27 @@ class ServerSentEventHandler<
110126 return super . predicate ( args )
111127 }
112128
113- async log ( args : { request : Request ; response : Response } ) : Promise < void > {
114- super . log ( {
115- request : args . request ,
116-
117- /**
118- * @note Construct a placeholder response since SSE response
119- * is being streamed and cannot be cloned/consumed at this point.
120- * This also allows us to rely on the same logging logic as in HTTP handlers.
121- */
122- response : new Response ( '[streaming]' , {
123- status : args . response . status ,
124- statusText : args . response . statusText ,
125- headers : args . response . headers ,
126- } ) ,
127- } )
129+ async log ( _args : { request : Request ; response : Response } ) : Promise < void > {
130+ /**
131+ * @note Skip the default `this.log()` logic so that when this handler is logged
132+ * upon handling the request, nothing is printed (we log SSE requests early).
133+ */
134+ return
128135 }
129136
130- private attachClientLogger (
137+ #attachClientLogger(
138+ request : Request ,
131139 emitter : Emitter < ServerSentEventClientEventMap > ,
132140 ) : void {
141+ const publicUrl = toPublicUrl ( request . url )
142+
133143 /* eslint-disable no-console */
134144 emitter . on ( 'message' , ( payload ) => {
135145 console . groupCollapsed (
136- devUtils . formatMessage ( `${ getTimestamp ( ) } %c⇣%c ${ payload . event } ` ) ,
146+ devUtils . formatMessage (
147+ `${ getTimestamp ( ) } SSE %s %c⇣%c ${ payload . event } ` ,
148+ ) ,
149+ publicUrl ,
137150 `color:${ colors . mocked } ` ,
138151 'color:inherit' ,
139152 )
@@ -142,19 +155,25 @@ class ServerSentEventHandler<
142155 } )
143156
144157 emitter . on ( 'error' , ( ) => {
145- console . log (
146- devUtils . formatMessage ( `${ getTimestamp ( ) } %c\u00D7%c error` ) ,
158+ console . groupCollapsed (
159+ devUtils . formatMessage ( `${ getTimestamp ( ) } SSE %s %c\u00D7%c error` ) ,
160+ publicUrl ,
147161 `color: ${ colors . system } ` ,
148162 'color:inherit' ,
149163 )
164+ console . log ( 'Handler:' , this )
165+ console . groupEnd ( )
150166 } )
151167
152168 emitter . on ( 'close' , ( ) => {
153- console . log (
154- devUtils . formatMessage ( `${ getTimestamp ( ) } %c■%c close` ) ,
169+ console . groupCollapsed (
170+ devUtils . formatMessage ( `${ getTimestamp ( ) } SSE %s %c■%c close` ) ,
171+ publicUrl ,
155172 `colors:${ colors . system } ` ,
156173 'color:inherit' ,
157174 )
175+ console . log ( 'Handler:' , this )
176+ console . groupEnd ( )
158177 } )
159178 /* eslint-enable no-console */
160179 }
@@ -200,11 +219,11 @@ class ServerSentEventClient<
200219> {
201220 #encoder: TextEncoder
202221 #controller: ReadableStreamDefaultController
203- #emitter? : Emitter < ServerSentEventClientEventMap >
222+ #emitter: Emitter < ServerSentEventClientEventMap >
204223
205224 constructor ( args : {
206225 controller : ReadableStreamDefaultController
207- emitter ? : Emitter < ServerSentEventClientEventMap >
226+ emitter : Emitter < ServerSentEventClientEventMap >
208227 } ) {
209228 this . #encoder = new TextEncoder ( )
210229 this . #controller = args . controller
@@ -260,24 +279,29 @@ class ServerSentEventClient<
260279 this . error ( )
261280 return
262281 }
282+
283+ if ( event . type === 'close' ) {
284+ this . close ( )
285+ return
286+ }
263287 }
264288
265289 /**
266290 * Errors the underlying `EventSource`, closing the connection with an error.
267- * Erroring the connection with an error will not trigger a reconnect from the client.
291+ * This is equivalent to aborting the connection and will produce a `TypeError: Failed to fetch`
292+ * error.
268293 */
269294 public error ( ) : void {
270295 this . #controller. error ( )
271- this . #emitter? .emit ( 'error' )
296+ this . #emitter. emit ( 'error' )
272297 }
273298
274299 /**
275300 * Closes the underlying `EventSource`, closing the connection.
276- * Closing the connection will trigger a reconnect from the client.
277301 */
278302 public close ( ) : void {
279303 this . #controller. close ( )
280- this . #emitter? .emit ( 'close' )
304+ this . #emitter. emit ( 'close' )
281305 }
282306
283307 #sendRetry( retry : number ) : void {
@@ -306,7 +330,7 @@ class ServerSentEventClient<
306330 frames . push ( '' , '' )
307331 this . #controller. enqueue ( this . #encoder. encode ( frames . join ( '\n' ) ) )
308332
309- this . #emitter? .emit ( 'message' , {
333+ this . #emitter. emit ( 'message' , {
310334 id : payload . id ,
311335 event : payload . event ?. toString ( ) || 'message' ,
312336 data : payload . data ,
0 commit comments