1
1
import * as http from 'node:http'
2
2
import { Readable } from 'node:stream'
3
+ import * as streamConsumers from 'node:stream/consumers'
3
4
import { AsyncLocalStorage } from 'node:async_hooks'
4
5
import { invariant } from 'outvariant'
5
6
import { createRequestId , FetchResponse } from '@mswjs/interceptors'
6
7
import { DeferredPromise } from '@open-draft/deferred-promise'
8
+ import { Emitter } from 'strict-event-emitter'
7
9
import { SetupApi } from '~/core/SetupApi'
8
10
import { delay } from '~/core/delay'
11
+ import { bypass } from '~/core/bypass'
9
12
import type { RequestHandler } from '~/core/handlers/RequestHandler'
10
13
import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
11
14
import { handleRequest } from '~/core/utils/handleRequest'
@@ -24,6 +27,30 @@ interface RemoteServerBoundaryContext {
24
27
handlers : Array < RequestHandler | WebSocketHandler >
25
28
}
26
29
30
+ export type ForwardedLifeCycleEventPayload = {
31
+ type : keyof LifeCycleEventsMap
32
+ args : {
33
+ requestId : string
34
+ request : {
35
+ method : string
36
+ url : string
37
+ headers : Array < [ string , string ] >
38
+ body : ArrayBuffer | null
39
+ }
40
+ response ?: {
41
+ status : number
42
+ statusText : string
43
+ headers : Array < [ string , string ] >
44
+ body : ArrayBuffer | null
45
+ }
46
+ error ?: {
47
+ name : string
48
+ message : string
49
+ stack ?: string
50
+ }
51
+ }
52
+ }
53
+
27
54
export const remoteHandlersContext =
28
55
new AsyncLocalStorage < RemoteServerBoundaryContext > ( )
29
56
@@ -97,13 +124,18 @@ export class SetupRemoteServerApi
97
124
}
98
125
99
126
public async listen ( ) : Promise < void > {
127
+ const dummyEmitter = new Emitter < LifeCycleEventsMap > ( )
128
+
100
129
const server = await createSyncServer ( )
101
130
this [ kServerUrl ] = getServerUrl ( server )
102
131
103
132
process
104
133
. once ( 'SIGTERM' , ( ) => closeSyncServer ( server ) )
105
134
. once ( 'SIGINT' , ( ) => closeSyncServer ( server ) )
106
135
136
+ // Close the server if the setup API is disposed.
137
+ this . subscriptions . push ( ( ) => closeSyncServer ( server ) )
138
+
107
139
server . on ( 'request' , async ( incoming , outgoing ) => {
108
140
if ( ! incoming . method ) {
109
141
return
@@ -173,7 +205,12 @@ export class SetupRemoteServerApi
173
205
handlers ,
174
206
/** @todo Support listen options */
175
207
{ onUnhandledRequest ( ) { } } ,
176
- this . emitter ,
208
+ /**
209
+ * @note Use a dummy emitter because this context
210
+ * is only one layer that can resolve a request. For example,
211
+ * request can be resolved in the remote process and not here.
212
+ */
213
+ dummyEmitter ,
177
214
)
178
215
179
216
if ( response ) {
@@ -262,19 +299,35 @@ export class SetupRemoteServerApi
262
299
}
263
300
264
301
private async handleLifeCycleEventRequest (
265
- _incoming : http . IncomingMessage ,
266
- _outgoing : http . ServerResponse < http . IncomingMessage > & {
302
+ incoming : http . IncomingMessage ,
303
+ outgoing : http . ServerResponse < http . IncomingMessage > & {
267
304
req : http . IncomingMessage
268
305
} ,
269
306
) {
270
- // const stream = Readable.toWeb(incoming)
271
- // const { event, requestId, request, response, error } = await new Request(
272
- // incoming.url,
273
- // { body: stream },
274
- // ).json()
275
- // /** @todo Finish this. */
276
- // this.emitter.emit(event, {})
277
- // outgoing.writeHead(200).end()
307
+ const event = ( await streamConsumers . json (
308
+ incoming ,
309
+ ) ) as ForwardedLifeCycleEventPayload
310
+
311
+ invariant (
312
+ event . type ,
313
+ 'Failed to emit a forwarded life-cycle event: request payload corrupted' ,
314
+ )
315
+
316
+ // Emit the forwarded life-cycle event on this emitter.
317
+ this . emitter . emit ( event . type , {
318
+ requestId : event . args . requestId ,
319
+ request : deserializeFetchRequest ( event . args . request ) ,
320
+ response :
321
+ event . args . response != null
322
+ ? deserializeFetchResponse ( event . args . response )
323
+ : undefined ,
324
+ error :
325
+ event . args . error != null
326
+ ? deserializeError ( event . args . error )
327
+ : undefined ,
328
+ } )
329
+
330
+ outgoing . writeHead ( 200 ) . end ( )
278
331
}
279
332
}
280
333
@@ -426,14 +479,13 @@ export class RemoteClient {
426
479
args . request . url ,
427
480
)
428
481
429
- const fetchRequest = args . request . clone ( )
430
- const responsePromise = new DeferredPromise < Response | undefined > ( )
431
-
432
- fetchRequest . headers . set ( 'accept' , 'msw/passthrough' )
433
- fetchRequest . headers . set ( 'x-msw-request-url' , args . request . url )
434
- fetchRequest . headers . set ( 'x-msw-request-id' , args . requestId )
435
- fetchRequest . headers . set ( 'x-msw-boundary-id' , args . boundaryId )
436
-
482
+ const fetchRequest = bypass ( args . request , {
483
+ headers : {
484
+ 'x-msw-request-url' : args . request . url ,
485
+ 'x-msw-request-id' : args . requestId ,
486
+ 'x-msw-boundary-id' : args . boundaryId ,
487
+ } ,
488
+ } )
437
489
const request = http . request ( this . url , {
438
490
method : fetchRequest . method ,
439
491
headers : Object . fromEntries ( fetchRequest . headers ) ,
@@ -445,6 +497,8 @@ export class RemoteClient {
445
497
request . end ( )
446
498
}
447
499
500
+ const responsePromise = new DeferredPromise < Response | undefined > ( )
501
+
448
502
request
449
503
. once ( 'response' , ( response ) => {
450
504
if ( response . statusCode === 404 ) {
@@ -474,56 +528,134 @@ export class RemoteClient {
474
528
return responsePromise
475
529
}
476
530
477
- // public async handleLifeCycleEvent<
478
- // EventType extends keyof LifeCycleEventsMap,
479
- // >(event: {
480
- // type: EventType
481
- // args: LifeCycleEventsMap[EventType][0]
482
- // }): Promise<void> {
483
- // const url = new URL('/life-cycle-events', this.url)
484
- // const payload: Record<string, unknown> = {
485
- // event: event.type,
486
- // requestId: event.args.requestId,
487
- // request: {
488
- // url: event.args.request.url,
489
- // method: event.args.request.method,
490
- // headers: Array.from(event.args.request.headers),
491
- // body: await event.args.request.arrayBuffer(),
492
- // },
493
- // }
494
-
495
- // switch (event.type) {
496
- // case 'unhandledException': {
497
- // payload.error = event.args.error
498
- // break
499
- // }
500
-
501
- // case 'response:bypass':
502
- // case 'response:mocked': {
503
- // payload.response = {
504
- // status: event.args.response.status,
505
- // statustext: event.args.response.statusText,
506
- // headers: Array.from(event.args.response.headers),
507
- // body: await event.args.response.arrayBuffer(),
508
- // }
509
- // break
510
- // }
511
- // }
512
-
513
- // const response = await fetch(url, {
514
- // method: 'POST',
515
- // headers: {
516
- // 'content-type': 'application/json',
517
- // },
518
- // body: JSON.stringify(payload),
519
- // })
520
-
521
- // invariant(
522
- // response && response.ok,
523
- // 'Failed to forward a life-cycle event "%s" (%s %s) to the remote',
524
- // event.type,
525
- // event.args.request.method,
526
- // event.args.request.url,
527
- // )
528
- // }
531
+ public async handleLifeCycleEvent <
532
+ EventType extends keyof LifeCycleEventsMap ,
533
+ > ( event : {
534
+ type : EventType
535
+ args : LifeCycleEventsMap [ EventType ] [ 0 ]
536
+ } ) : Promise < void > {
537
+ invariant (
538
+ this . connected ,
539
+ 'Failed to forward life-cycle events for "%s %s": remote client not connected' ,
540
+ event . args . request . method ,
541
+ event . args . request . url ,
542
+ )
543
+
544
+ const url = new URL ( '/life-cycle-events' , this . url )
545
+ const payload = JSON . stringify ( {
546
+ type : event . type ,
547
+ args : {
548
+ requestId : event . args . requestId ,
549
+ request : await serializeFetchRequest ( event . args . request ) ,
550
+ response :
551
+ 'response' in event . args
552
+ ? await serializeFetchResponse ( event . args . response )
553
+ : undefined ,
554
+ error :
555
+ 'error' in event . args ? serializeError ( event . args . error ) : undefined ,
556
+ } ,
557
+ } satisfies ForwardedLifeCycleEventPayload )
558
+
559
+ invariant (
560
+ payload ,
561
+ 'Failed to serialize a life-cycle event "%s" for request "%s %s"' ,
562
+ event . type ,
563
+ event . args . request . method ,
564
+ event . args . request . url ,
565
+ )
566
+
567
+ const donePromise = new DeferredPromise < void > ( )
568
+
569
+ http
570
+ . request (
571
+ url ,
572
+ {
573
+ method : 'POST' ,
574
+ headers : {
575
+ accept : 'msw/passthrough' ,
576
+ 'content-type' : 'application/json' ,
577
+ } ,
578
+ } ,
579
+ ( response ) => {
580
+ if ( response . statusCode === 200 ) {
581
+ donePromise . resolve ( )
582
+ } else {
583
+ donePromise . reject (
584
+ new Error (
585
+ `Failed to forward life-cycle event "${ event . type } " for request "${ event . args . request . method } ${ event . args . request . url } ": expected a 200 response but got ${ response . statusCode } ` ,
586
+ ) ,
587
+ )
588
+ }
589
+ } ,
590
+ )
591
+ . end ( payload )
592
+ . once ( 'error' , ( error ) => {
593
+ // eslint-disable-next-line no-console
594
+ console . error ( error )
595
+ donePromise . reject (
596
+ new Error (
597
+ `Failed to forward life-cycle event "${ event . type } " for request "${ event . args . request . method } ${ event . args . request . url } ": unexpected error. There's likely additional information above.` ,
598
+ ) ,
599
+ )
600
+ } )
601
+
602
+ return donePromise
603
+ }
604
+ }
605
+
606
+ async function serializeFetchRequest ( request : Request ) {
607
+ return {
608
+ url : request . url ,
609
+ method : request . method ,
610
+ headers : Array . from ( request . headers ) ,
611
+ body : request . body ? await request . arrayBuffer ( ) : null ,
612
+ }
613
+ }
614
+
615
+ function deserializeFetchRequest (
616
+ value : NonNullable < ForwardedLifeCycleEventPayload [ 'args' ] [ 'request' ] > ,
617
+ ) : Request {
618
+ return new Request ( value . url , {
619
+ method : value . method ,
620
+ headers : value . headers ,
621
+ body : value . body ,
622
+ } )
623
+ }
624
+
625
+ async function serializeFetchResponse ( response : Response ) {
626
+ return {
627
+ status : response . status ,
628
+ statusText : response . statusText ,
629
+ headers : Array . from ( response . headers ) ,
630
+ body : await response . arrayBuffer ( ) ,
631
+ }
632
+ }
633
+
634
+ function deserializeFetchResponse (
635
+ value : NonNullable < ForwardedLifeCycleEventPayload [ 'args' ] [ 'response' ] > ,
636
+ ) : Response {
637
+ return new Response ( value . body , {
638
+ status : value . status ,
639
+ statusText : value . statusText ,
640
+ headers : value . headers ,
641
+ } )
642
+ }
643
+
644
+ function serializeError (
645
+ error : Error ,
646
+ ) : ForwardedLifeCycleEventPayload [ 'args' ] [ 'error' ] {
647
+ return {
648
+ name : error . name ,
649
+ message : error . message ,
650
+ stack : error . stack ,
651
+ }
652
+ }
653
+
654
+ function deserializeError (
655
+ value : NonNullable < ForwardedLifeCycleEventPayload [ 'args' ] [ 'error' ] > ,
656
+ ) : Error {
657
+ const error = new Error ( value . message )
658
+ error . name = value . name
659
+ error . stack = value . stack
660
+ return error
529
661
}
0 commit comments