Skip to content

Commit 172dbd7

Browse files
committed
feat(wip): forward life-cycle events over http
1 parent 28875f9 commit 172dbd7

File tree

2 files changed

+208
-76
lines changed

2 files changed

+208
-76
lines changed

src/node/SetupServerApi.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export class SetupServerApi
123123
const remoteConnectionPromise = remoteClient.connect().then(
124124
() => {
125125
// Forward the life-cycle events from this process to the remote.
126-
// this.forwardLifeCycleEventsToRemote()
126+
this.forwardLifeCycleEventsToRemote()
127127

128128
this.handlersController.currentHandlers = new Proxy(
129129
this.handlersController.currentHandlers,
@@ -184,10 +184,10 @@ export class SetupServerApi
184184
for (const event of events) {
185185
this.emitter.on(event, (args) => {
186186
if (!shouldBypassRequest(args.request)) {
187-
// remoteClient.handleLifeCycleEvent({
188-
// type: event,
189-
// args,
190-
// })
187+
remoteClient.handleLifeCycleEvent({
188+
type: event,
189+
args,
190+
})
191191
}
192192
})
193193
}

src/node/setupRemoteServer.ts

+203-71
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import * as http from 'node:http'
22
import { Readable } from 'node:stream'
3+
import * as streamConsumers from 'node:stream/consumers'
34
import { AsyncLocalStorage } from 'node:async_hooks'
45
import { invariant } from 'outvariant'
56
import { createRequestId, FetchResponse } from '@mswjs/interceptors'
67
import { DeferredPromise } from '@open-draft/deferred-promise'
8+
import { Emitter } from 'strict-event-emitter'
79
import { SetupApi } from '~/core/SetupApi'
810
import { delay } from '~/core/delay'
11+
import { bypass } from '~/core/bypass'
912
import type { RequestHandler } from '~/core/handlers/RequestHandler'
1013
import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
1114
import { handleRequest } from '~/core/utils/handleRequest'
@@ -24,6 +27,30 @@ interface RemoteServerBoundaryContext {
2427
handlers: Array<RequestHandler | WebSocketHandler>
2528
}
2629

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+
2754
export const remoteHandlersContext =
2855
new AsyncLocalStorage<RemoteServerBoundaryContext>()
2956

@@ -97,13 +124,18 @@ export class SetupRemoteServerApi
97124
}
98125

99126
public async listen(): Promise<void> {
127+
const dummyEmitter = new Emitter<LifeCycleEventsMap>()
128+
100129
const server = await createSyncServer()
101130
this[kServerUrl] = getServerUrl(server)
102131

103132
process
104133
.once('SIGTERM', () => closeSyncServer(server))
105134
.once('SIGINT', () => closeSyncServer(server))
106135

136+
// Close the server if the setup API is disposed.
137+
this.subscriptions.push(() => closeSyncServer(server))
138+
107139
server.on('request', async (incoming, outgoing) => {
108140
if (!incoming.method) {
109141
return
@@ -173,7 +205,12 @@ export class SetupRemoteServerApi
173205
handlers,
174206
/** @todo Support listen options */
175207
{ 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,
177214
)
178215

179216
if (response) {
@@ -262,19 +299,35 @@ export class SetupRemoteServerApi
262299
}
263300

264301
private async handleLifeCycleEventRequest(
265-
_incoming: http.IncomingMessage,
266-
_outgoing: http.ServerResponse<http.IncomingMessage> & {
302+
incoming: http.IncomingMessage,
303+
outgoing: http.ServerResponse<http.IncomingMessage> & {
267304
req: http.IncomingMessage
268305
},
269306
) {
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()
278331
}
279332
}
280333

@@ -426,14 +479,13 @@ export class RemoteClient {
426479
args.request.url,
427480
)
428481

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+
})
437489
const request = http.request(this.url, {
438490
method: fetchRequest.method,
439491
headers: Object.fromEntries(fetchRequest.headers),
@@ -445,6 +497,8 @@ export class RemoteClient {
445497
request.end()
446498
}
447499

500+
const responsePromise = new DeferredPromise<Response | undefined>()
501+
448502
request
449503
.once('response', (response) => {
450504
if (response.statusCode === 404) {
@@ -474,56 +528,134 @@ export class RemoteClient {
474528
return responsePromise
475529
}
476530

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
529661
}

0 commit comments

Comments
 (0)