Skip to content

Commit d50f73e

Browse files
authored
fix(sse): respect the worker quiet option (#2644)
1 parent 51b3e2d commit d50f73e

6 files changed

Lines changed: 105 additions & 38 deletions

File tree

src/browser/setupWorker/start/createFallbackRequestListener.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export function createFallbackRequestListener(
2929
options,
3030
context.emitter,
3131
{
32+
resolutionContext: {
33+
quiet: options.quiet,
34+
},
3235
onMockedResponse(_, { handler, parsedResult }) {
3336
if (!options.quiet) {
3437
context.emitter.once('response:mocked', ({ response }) => {

src/browser/setupWorker/start/createRequestListener.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export const createRequestListener = (
4646
options,
4747
context.emitter,
4848
{
49+
resolutionContext: {
50+
quiet: options.quiet,
51+
},
4952
onPassthroughResponse() {
5053
event.postMessage('PASSTHROUGH')
5154
},

src/core/sse.ts

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type HttpRequestResolverExtras,
77
type HttpRequestParsedResult,
88
} from './handlers/HttpHandler'
9+
import type { ResponseResolutionContext } from '~/core/utils/executeHandlers'
910
import type { Path, PathParams } from './utils/matching/matchRequestUrl'
1011
import { delay } from './delay'
1112
import { getTimestamp } from './utils/logging/getTimestamp'
@@ -67,47 +68,32 @@ export const sse: ServerSentEventRequestHandler = (path, resolver) => {
6768
return new ServerSentEventHandler(path, resolver)
6869
}
6970

71+
const SSE_RESPONSE_INIT: ResponseInit = {
72+
headers: {
73+
'content-type': 'text/event-stream',
74+
'cache-control': 'no-cache',
75+
connection: 'keep-alive',
76+
},
77+
}
78+
7079
class ServerSentEventHandler<
7180
EventMap extends EventMapConstraint,
7281
> extends HttpHandler {
82+
#emitter: Emitter<ServerSentEventClientEventMap>
83+
7384
constructor(path: Path, resolver: ServerSentEventResolver<EventMap, any>) {
7485
invariant(
7586
typeof EventSource !== 'undefined',
7687
'Failed to construct a Server-Sent Event handler for path "%s": the EventSource API is not supported in this environment',
7788
path,
7889
)
7990

80-
const clientEmitter = new Emitter<ServerSentEventClientEventMap>()
81-
8291
super('GET', path, async (info) => {
83-
const responseInit: ResponseInit = {
84-
headers: {
85-
'content-type': 'text/event-stream',
86-
'cache-control': 'no-cache',
87-
connection: 'keep-alive',
88-
},
89-
}
90-
91-
/**
92-
* @note Log the intercepted request early.
93-
* Normally, the `this.log()` method is called when the handler returns a response.
94-
* For SSE, call that method earlier so the logs are in correct order.
95-
*/
96-
await super.log({
97-
request: info.request,
98-
/**
99-
* @note Construct a placeholder response since SSE response
100-
* is being streamed and cannot be cloned/consumed for logging.
101-
*/
102-
response: new Response('[streaming]', responseInit),
103-
})
104-
this.#attachClientLogger(info.request, clientEmitter)
105-
10692
const stream = new ReadableStream({
107-
async start(controller) {
93+
start: async (controller) => {
10894
const client = new ServerSentEventClient<EventMap>({
10995
controller,
110-
emitter: clientEmitter,
96+
emitter: this.#emitter,
11197
})
11298
const server = new ServerSentEventServer({
11399
request: info.request,
@@ -122,19 +108,42 @@ class ServerSentEventHandler<
122108
},
123109
})
124110

125-
return new Response(stream, responseInit)
111+
return new Response(stream, SSE_RESPONSE_INIT)
126112
})
113+
114+
this.#emitter = new Emitter<ServerSentEventClientEventMap>()
127115
}
128116

129117
async predicate(args: {
130118
request: Request
131119
parsedResult: HttpRequestParsedResult
120+
resolutionContext?: ResponseResolutionContext
132121
}) {
133122
if (args.request.headers.get('accept') !== 'text/event-stream') {
134123
return false
135124
}
136125

137-
return super.predicate(args)
126+
const matches = await super.predicate(args)
127+
128+
if (matches && !args.resolutionContext?.quiet) {
129+
/**
130+
* @note Log the intercepted request early.
131+
* Normally, the `this.log()` method is called when the handler returns a response.
132+
* For SSE, call that method earlier so the logs are in correct order.
133+
*/
134+
await super.log({
135+
request: args.request,
136+
/**
137+
* @note Construct a placeholder response since SSE response
138+
* is being streamed and cannot be cloned/consumed for logging.
139+
*/
140+
response: new Response('[streaming]', SSE_RESPONSE_INIT),
141+
})
142+
143+
this.#attachClientLogger(args.request, this.#emitter)
144+
}
145+
146+
return matches
138147
}
139148

140149
async log(_args: { request: Request; response: Response }): Promise<void> {

src/core/utils/executeHandlers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ export interface HandlersExecutionResult {
1010
}
1111

1212
export interface ResponseResolutionContext {
13+
/**
14+
* A base url to use when resolving relative urls.
15+
* @note This is primarily used by the `@mswjs/http-middleware`
16+
* to resolve relative urls in the context of the running server
17+
*/
1318
baseUrl?: string
19+
quiet?: boolean
1420
}
1521

1622
/**

src/core/utils/handleRequest.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { Emitter } from 'strict-event-emitter'
33
import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions'
44
import { RequiredDeep } from '../typeUtils'
55
import type { RequestHandler } from '../handlers/RequestHandler'
6-
import { HandlersExecutionResult, executeHandlers } from './executeHandlers'
6+
import {
7+
type HandlersExecutionResult,
8+
type ResponseResolutionContext,
9+
executeHandlers,
10+
} from './executeHandlers'
711
import { onUnhandledRequest } from './request/onUnhandledRequest'
812
import { storeResponseCookies } from './request/storeResponseCookies'
913

@@ -13,14 +17,7 @@ export interface HandleRequestOptions {
1317
* but is exposed to aid in creating extensions like
1418
* `@mswjs/http-middleware`.
1519
*/
16-
resolutionContext?: {
17-
/**
18-
* A base url to use when resolving relative urls.
19-
* @note This is primarily used by the `@mswjs/http-middleware`
20-
* to resolve relative urls in the context of the running server
21-
*/
22-
baseUrl?: string
23-
}
20+
resolutionContext?: ResponseResolutionContext
2421

2522
/**
2623
* Invoked whenever a request is performed as-is.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { sse } from 'msw'
2+
import { setupWorker } from 'msw/browser'
3+
import { test, expect } from '../playwright.extend'
4+
5+
declare namespace window {
6+
export const msw: {
7+
setupWorker: typeof setupWorker
8+
sse: typeof sse
9+
}
10+
}
11+
12+
const EXAMPLE_URL = new URL('./sse.mocks.ts', import.meta.url)
13+
14+
test('does not log anything if the "quiet" option is set to true', async ({
15+
loadExample,
16+
spyOnConsole,
17+
page,
18+
}) => {
19+
const consoleSpy = spyOnConsole()
20+
await loadExample(EXAMPLE_URL, {
21+
skipActivation: true,
22+
})
23+
24+
await page.evaluate(async () => {
25+
const { setupWorker, sse } = window.msw
26+
27+
const worker = setupWorker(
28+
sse('http://localhost/stream', ({ client }) => {
29+
client.send({
30+
data: { username: 'john' },
31+
})
32+
}),
33+
)
34+
await worker.start({ quiet: true })
35+
})
36+
37+
await page.evaluate(() => {
38+
return new Promise<string>((resolve, reject) => {
39+
const source = new EventSource('http://localhost/stream')
40+
source.onerror = () => reject()
41+
42+
source.addEventListener('message', (event) => {
43+
resolve(`${event.type}:${event.data}`)
44+
})
45+
})
46+
})
47+
48+
expect(consoleSpy.get('startGroupCollapsed')).toBeUndefined()
49+
})

0 commit comments

Comments
 (0)