msw 2.14.7 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/lib/browser/index.js +1 -1
  2. package/lib/browser/index.mjs +1 -1
  3. package/lib/core/{HttpResponse-BTyqrRB6.d.mts → HttpResponse-BE_Y2Ak9.d.mts} +18 -0
  4. package/lib/core/{HttpResponse-BXyXQFMJ.d.ts → HttpResponse-DL-P1EeG.d.ts} +18 -0
  5. package/lib/core/HttpResponse.d.mts +1 -1
  6. package/lib/core/HttpResponse.d.ts +1 -1
  7. package/lib/core/experimental/compat.d.mts +1 -1
  8. package/lib/core/experimental/compat.d.ts +1 -1
  9. package/lib/core/experimental/define-network.d.mts +1 -1
  10. package/lib/core/experimental/define-network.d.ts +1 -1
  11. package/lib/core/experimental/frames/http-frame.d.mts +1 -1
  12. package/lib/core/experimental/frames/http-frame.d.ts +1 -1
  13. package/lib/core/experimental/frames/network-frame.d.mts +1 -1
  14. package/lib/core/experimental/frames/network-frame.d.ts +1 -1
  15. package/lib/core/experimental/frames/websocket-frame.d.mts +1 -1
  16. package/lib/core/experimental/frames/websocket-frame.d.ts +1 -1
  17. package/lib/core/experimental/handlers-controller.d.mts +1 -1
  18. package/lib/core/experimental/handlers-controller.d.ts +1 -1
  19. package/lib/core/experimental/index.d.mts +1 -1
  20. package/lib/core/experimental/index.d.ts +1 -1
  21. package/lib/core/experimental/on-unhandled-frame.d.mts +1 -1
  22. package/lib/core/experimental/on-unhandled-frame.d.ts +1 -1
  23. package/lib/core/experimental/setup-api.d.mts +1 -1
  24. package/lib/core/experimental/setup-api.d.ts +1 -1
  25. package/lib/core/experimental/sources/interceptor-source.d.mts +1 -1
  26. package/lib/core/experimental/sources/interceptor-source.d.ts +1 -1
  27. package/lib/core/experimental/sources/network-source.d.mts +1 -1
  28. package/lib/core/experimental/sources/network-source.d.ts +1 -1
  29. package/lib/core/getResponse.d.mts +1 -1
  30. package/lib/core/getResponse.d.ts +1 -1
  31. package/lib/core/graphql.d.mts +1 -1
  32. package/lib/core/graphql.d.ts +1 -1
  33. package/lib/core/handlers/GraphQLHandler.d.mts +1 -1
  34. package/lib/core/handlers/GraphQLHandler.d.ts +1 -1
  35. package/lib/core/handlers/HttpHandler.d.mts +1 -1
  36. package/lib/core/handlers/HttpHandler.d.ts +1 -1
  37. package/lib/core/handlers/RequestHandler.d.mts +1 -1
  38. package/lib/core/handlers/RequestHandler.d.ts +1 -1
  39. package/lib/core/handlers/RequestHandler.js +88 -23
  40. package/lib/core/handlers/RequestHandler.js.map +1 -1
  41. package/lib/core/handlers/RequestHandler.mjs +88 -23
  42. package/lib/core/handlers/RequestHandler.mjs.map +1 -1
  43. package/lib/core/http.d.mts +1 -1
  44. package/lib/core/http.d.ts +1 -1
  45. package/lib/core/index.d.mts +1 -1
  46. package/lib/core/index.d.ts +1 -1
  47. package/lib/core/passthrough.d.mts +1 -1
  48. package/lib/core/passthrough.d.ts +1 -1
  49. package/lib/core/sse.d.mts +2 -2
  50. package/lib/core/sse.d.ts +2 -2
  51. package/lib/core/sse.js +66 -48
  52. package/lib/core/sse.js.map +1 -1
  53. package/lib/core/sse.mjs +65 -47
  54. package/lib/core/sse.mjs.map +1 -1
  55. package/lib/core/utils/HttpResponse/decorators.d.mts +8 -3
  56. package/lib/core/utils/HttpResponse/decorators.d.ts +8 -3
  57. package/lib/core/utils/HttpResponse/decorators.js +13 -0
  58. package/lib/core/utils/HttpResponse/decorators.js.map +1 -1
  59. package/lib/core/utils/HttpResponse/decorators.mjs +13 -0
  60. package/lib/core/utils/HttpResponse/decorators.mjs.map +1 -1
  61. package/lib/core/utils/executeHandlers.d.mts +1 -1
  62. package/lib/core/utils/executeHandlers.d.ts +1 -1
  63. package/lib/core/utils/handleRequest.d.mts +1 -1
  64. package/lib/core/utils/handleRequest.d.ts +1 -1
  65. package/lib/core/utils/internal/attachSiblingHandlers.d.mts +1 -1
  66. package/lib/core/utils/internal/attachSiblingHandlers.d.ts +1 -1
  67. package/lib/core/utils/internal/isHandlerKind.d.mts +1 -1
  68. package/lib/core/utils/internal/isHandlerKind.d.ts +1 -1
  69. package/lib/core/utils/internal/observe-response-body-stream.d.mts +18 -0
  70. package/lib/core/utils/internal/observe-response-body-stream.d.ts +18 -0
  71. package/lib/core/utils/internal/observe-response-body-stream.js +64 -0
  72. package/lib/core/utils/internal/observe-response-body-stream.js.map +1 -0
  73. package/lib/core/utils/internal/observe-response-body-stream.mjs +44 -0
  74. package/lib/core/utils/internal/observe-response-body-stream.mjs.map +1 -0
  75. package/lib/core/utils/internal/parseGraphQLRequest.d.mts +1 -1
  76. package/lib/core/utils/internal/parseGraphQLRequest.d.ts +1 -1
  77. package/lib/core/utils/internal/parseMultipartData.d.mts +1 -1
  78. package/lib/core/utils/internal/parseMultipartData.d.ts +1 -1
  79. package/lib/core/ws/handleWebSocketEvent.d.mts +1 -1
  80. package/lib/core/ws/handleWebSocketEvent.d.ts +1 -1
  81. package/lib/iife/index.js +270 -137
  82. package/lib/iife/index.js.map +1 -1
  83. package/lib/mockServiceWorker.js +21 -9
  84. package/package.json +2 -2
  85. package/src/core/handlers/HttpHandler.test.ts +89 -0
  86. package/src/core/handlers/RequestHandler.ts +142 -32
  87. package/src/core/sse.ts +85 -53
  88. package/src/core/utils/HttpResponse/decorators.ts +23 -2
  89. package/src/core/utils/internal/observe-response-body-stream.test.ts +64 -0
  90. package/src/core/utils/internal/observe-response-body-stream.ts +77 -0
  91. package/src/mockServiceWorker.js +19 -7
@@ -7,8 +7,8 @@
7
7
  * - Please do NOT modify this file.
8
8
  */
9
9
 
10
- const PACKAGE_VERSION = '2.14.7'
11
- const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
10
+ const PACKAGE_VERSION = '2.15.0'
11
+ const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e'
12
12
  const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
13
13
  const activeClientIds = new Set()
14
14
 
@@ -137,8 +137,18 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
137
137
  if (client && activeClientIds.has(client.id)) {
138
138
  const serializedRequest = await serializeRequest(requestCloneForEvents)
139
139
 
140
+ // Omit the body of server-sent event stream responses.
141
+ // Cloning such responses would prevent client-side stream cancelations
142
+ // from reaching the original stream (a teed stream only cancels its
143
+ // source once both of its branches cancel) and would buffer the
144
+ // entire stream into the unconsumed clone indefinitely.
145
+ const isEventStreamResponse = response.headers
146
+ .get('content-type')
147
+ ?.toLowerCase()
148
+ .startsWith('text/event-stream')
149
+
140
150
  // Clone the response so both the client and the library could consume it.
141
- const responseClone = response.clone()
151
+ const responseClone = isEventStreamResponse ? null : response.clone()
142
152
 
143
153
  sendToClient(
144
154
  client,
@@ -151,15 +161,17 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
151
161
  ...serializedRequest,
152
162
  },
153
163
  response: {
154
- type: responseClone.type,
155
- status: responseClone.status,
156
- statusText: responseClone.statusText,
157
- headers: Object.fromEntries(responseClone.headers.entries()),
158
- body: responseClone.body,
164
+ type: response.type,
165
+ status: response.status,
166
+ statusText: response.statusText,
167
+ headers: Object.fromEntries(response.headers.entries()),
168
+ body: responseClone ? responseClone.body : null,
159
169
  },
160
170
  },
161
171
  },
162
- responseClone.body ? [serializedRequest.body, responseClone.body] : [],
172
+ responseClone && responseClone.body
173
+ ? [serializedRequest.body, responseClone.body]
174
+ : [],
163
175
  )
164
176
  }
165
177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msw",
3
- "version": "2.14.7",
3
+ "version": "2.15.0",
4
4
  "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.",
5
5
  "type": "commonjs",
6
6
  "main": "./lib/core/index.js",
@@ -285,7 +285,7 @@
285
285
  "vitest": "^4.1.4",
286
286
  "webpack": "^5.106.2",
287
287
  "webpack-http-server": "^0.5.0",
288
- "msw": "^2.14.7"
288
+ "msw": "^2.15.0"
289
289
  },
290
290
  "peerDependencies": {
291
291
  "typescript": ">= 4.8.x"
@@ -252,3 +252,92 @@ describe('run', () => {
252
252
  await expect(run()).resolves.toBe('complete')
253
253
  })
254
254
  })
255
+
256
+ describe('finalize', () => {
257
+ it('returns the exact response instance if the resolver never accesses finalize', async () => {
258
+ const response = new HttpResponse(
259
+ new ReadableStream({
260
+ start(controller) {
261
+ controller.enqueue(new TextEncoder().encode('hello'))
262
+ controller.close()
263
+ },
264
+ }),
265
+ )
266
+ const handler = new HttpHandler('GET', '/resource', () => {
267
+ return response
268
+ })
269
+
270
+ const result = await handler.run({
271
+ request: new Request(new URL('/resource', location.href)),
272
+ requestId: createRequestId(),
273
+ })
274
+
275
+ expect(result?.response).toBe(response)
276
+ })
277
+
278
+ it('returns the exact response instance if finalize is accessed but never called', async () => {
279
+ const response = new HttpResponse(
280
+ new ReadableStream({
281
+ start(controller) {
282
+ controller.close()
283
+ },
284
+ }),
285
+ )
286
+ const handler = new HttpHandler('GET', '/resource', ({ finalize }) => {
287
+ expect(finalize).toBeInstanceOf(Function)
288
+ return response
289
+ })
290
+
291
+ const result = await handler.run({
292
+ request: new Request(new URL('/resource', location.href)),
293
+ requestId: createRequestId(),
294
+ })
295
+
296
+ expect(result?.response).toBe(response)
297
+ })
298
+
299
+ it('defers the cleanup until the response stream settles', async () => {
300
+ const cleanup = vi.fn()
301
+ const response = new HttpResponse(
302
+ new ReadableStream({
303
+ start(controller) {
304
+ controller.enqueue(new TextEncoder().encode('hello'))
305
+ controller.close()
306
+ },
307
+ }),
308
+ )
309
+ const handler = new HttpHandler('GET', '/resource', ({ finalize }) => {
310
+ finalize(cleanup)
311
+ return response
312
+ })
313
+
314
+ const result = await handler.run({
315
+ request: new Request(new URL('/resource', location.href)),
316
+ requestId: createRequestId(),
317
+ })
318
+
319
+ // The observed response is a copy of the original response.
320
+ expect(result?.response).not.toBe(response)
321
+ expect(cleanup).not.toHaveBeenCalled()
322
+
323
+ await expect(result?.response?.text()).resolves.toBe('hello')
324
+ await vi.waitFor(() => {
325
+ expect(cleanup).toHaveBeenCalledOnce()
326
+ })
327
+ })
328
+
329
+ it('runs the cleanup immediately for responses without a body', async () => {
330
+ const cleanup = vi.fn()
331
+ const handler = new HttpHandler('GET', '/resource', ({ finalize }) => {
332
+ finalize(cleanup)
333
+ return new HttpResponse(null)
334
+ })
335
+
336
+ await handler.run({
337
+ request: new Request(new URL('/resource', location.href)),
338
+ requestId: createRequestId(),
339
+ })
340
+
341
+ expect(cleanup).toHaveBeenCalledOnce()
342
+ })
343
+ })
@@ -15,6 +15,7 @@ import {
15
15
  import type { GraphQLRequestBody } from './GraphQLHandler'
16
16
  import { devUtils } from '../utils/internal/devUtils'
17
17
  import { getRawSetCookie } from '../utils/HttpResponse/decorators'
18
+ import { observeResponseBodyStream } from '../utils/internal/observe-response-body-stream'
18
19
 
19
20
  export type DefaultRequestMultipartBody = Record<
20
21
  string,
@@ -31,12 +32,7 @@ export type DefaultBodyType =
31
32
  | undefined
32
33
 
33
34
  export type JsonBodyType =
34
- | Record<string, any>
35
- | string
36
- | number
37
- | boolean
38
- | null
39
- | undefined
35
+ Record<string, any> | string | number | boolean | null | undefined
40
36
 
41
37
  export interface RequestHandlerDefaultInfo {
42
38
  header: string
@@ -94,6 +90,10 @@ export type ResponseResolverInfo<
94
90
  /**
95
91
  * Schedule a callback to run after this response resolver completes.
96
92
  * Handy for cleaning up the side effects introduced in the resolver.
93
+ *
94
+ * For responses with a `ReadableStream` body (including `sse()` handlers),
95
+ * the callback runs once the response stream settles: it is read to
96
+ * completion, errored, or canceled, or the request is aborted.
97
97
  * @example
98
98
  * sse('/', ({ client, finalize }) => {
99
99
  * const interval = setInterval(() => client.send({ data: 'ping' }))
@@ -355,20 +355,54 @@ export abstract class RequestHandler<
355
355
 
356
356
  const listenerController = new AbortController()
357
357
 
358
- args.request.signal.addEventListener(
359
- 'abort',
360
- () => this.runScheduledCleanups(args.requestId),
361
- {
362
- once: true,
363
- signal: listenerController.signal,
364
- },
365
- )
358
+ /**
359
+ * @note Initialize the `finalize` machinery lazily, on the first
360
+ * access of the `finalize` property by the resolver. If the resolver
361
+ * never accesses it, the handler behaves as if `finalize` never
362
+ * existed: no abort listeners, no scheduled cleanups, and no response
363
+ * body stream observation (see `this.complete()`).
364
+ */
365
+ let finalizeFunction: ResponseResolverFinalizeFunction | undefined
366
+
367
+ const getFinalize = (): ResponseResolverFinalizeFunction => {
368
+ if (finalizeFunction == null) {
369
+ // Run any scheduled cleanups if the request gets aborted
370
+ // while the resolver is still executing.
371
+ if (!args.request.signal.aborted) {
372
+ args.request.signal.addEventListener(
373
+ 'abort',
374
+ () => this.runScheduledCleanups(args.requestId),
375
+ {
376
+ once: true,
377
+ signal: listenerController.signal,
378
+ },
379
+ )
380
+ }
381
+
382
+ finalizeFunction = (callback) => {
383
+ this.scheduleCleanup(args.requestId, callback)
384
+
385
+ /**
386
+ * @note Run the cleanup immediately if the request has already
387
+ * been aborted. The "abort" listener above never fires for an
388
+ * already-aborted signal (and fires at most once), while
389
+ * long-lived resolvers (streams, generators) may never settle
390
+ * to run the cleanups on completion.
391
+ */
392
+ if (args.request.signal.aborted) {
393
+ void this.runScheduledCleanups(args.requestId)
394
+ }
395
+ }
396
+ }
397
+
398
+ return finalizeFunction
399
+ }
366
400
 
367
401
  const mockedResponsePromise = (
368
402
  executeResolver({
369
403
  ...resolverExtras,
370
- finalize: (callback) => {
371
- this.scheduleCleanup(args.requestId, callback)
404
+ get finalize(): ResponseResolverFinalizeFunction {
405
+ return getFinalize()
372
406
  },
373
407
  requestId: args.requestId,
374
408
  request: args.request,
@@ -423,9 +457,13 @@ export abstract class RequestHandler<
423
457
  }
424
458
 
425
459
  if (!isIterable(result)) {
426
- // Otherwise, run the cleanup immediately if it's a plain response resolver.
427
- await this.runScheduledCleanups(info.requestId)
428
- return result
460
+ // Otherwise, run the cleanups if it's a plain response resolver
461
+ // (deferred until the response stream settles for streamed responses).
462
+ return this.complete({
463
+ request: info.request,
464
+ requestId: info.requestId,
465
+ response: result,
466
+ })
429
467
  }
430
468
 
431
469
  /**
@@ -465,11 +503,13 @@ export abstract class RequestHandler<
465
503
  // only after it's been completely exhausted.
466
504
  this.isUsed = true
467
505
 
468
- await this.runScheduledCleanups(info.requestId)
469
-
470
506
  // Clone the previously stored response so it can be read
471
507
  // when receiving it repeatedly from the "done" generator.
472
- return this.resolverIteratorResult?.clone()
508
+ return this.complete({
509
+ request: info.request,
510
+ requestId: info.requestId,
511
+ response: this.resolverIteratorResult?.clone(),
512
+ })
473
513
  }
474
514
 
475
515
  return nextResponse
@@ -532,28 +572,98 @@ export abstract class RequestHandler<
532
572
  }
533
573
  }
534
574
 
535
- private async runScheduledCleanups(requestId: string): Promise<void> {
575
+ /**
576
+ * Remove and return the cleanups scheduled for the given request
577
+ * (or the pending iterator cleanups for generator resolvers).
578
+ */
579
+ private takeScheduledCleanups(
580
+ requestId: string,
581
+ ): Array<() => MaybePromise<void>> | undefined {
536
582
  if (
537
583
  this.resolverIterator &&
538
584
  this.resolverIteratorCleanups != null &&
539
585
  this.resolverIteratorCleanups.length > 0
540
586
  ) {
541
- try {
542
- await this.exhaustCleanups(this.resolverIteratorCleanups)
543
- } finally {
544
- this.resolverIteratorCleanups = undefined
545
- }
546
- return
587
+ const cleanups = this.resolverIteratorCleanups
588
+ this.resolverIteratorCleanups = undefined
589
+ return cleanups
547
590
  }
548
591
 
549
592
  const cleanups = this.scheduledCleanups.get(requestId)
550
593
 
551
- if (!cleanups || cleanups.length == 0) {
552
- return
594
+ if (!cleanups || cleanups.length === 0) {
595
+ return undefined
553
596
  }
554
597
 
555
- await this.exhaustCleanups(cleanups)
556
598
  this.scheduledCleanups.delete(requestId)
599
+ return cleanups
600
+ }
601
+
602
+ private async runScheduledCleanups(requestId: string): Promise<void> {
603
+ const cleanups = this.takeScheduledCleanups(requestId)
604
+
605
+ if (cleanups) {
606
+ await this.exhaustCleanups(cleanups)
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Conclude the response resolution for the given request.
612
+ * Runs the scheduled cleanups immediately for responses without a
613
+ * `ReadableStream` body. For streamed responses, returns an observed
614
+ * copy of the response and defers the cleanups until its body settles
615
+ * (is read to completion, errored, or canceled) or the request is
616
+ * aborted, whichever comes first.
617
+ */
618
+ private async complete(args: {
619
+ request: StrictRequest<any>
620
+ requestId: string
621
+ response: Response | undefined | void
622
+ }): Promise<Response | undefined | void> {
623
+ const cleanups = this.takeScheduledCleanups(args.requestId)
624
+
625
+ if (!cleanups) {
626
+ return args.response
627
+ }
628
+
629
+ const observedResponse = args.response
630
+ ? observeResponseBodyStream(args.response)
631
+ : null
632
+
633
+ if (!observedResponse) {
634
+ await this.exhaustCleanups(cleanups)
635
+ return args.response
636
+ }
637
+
638
+ const listenerController = new AbortController()
639
+
640
+ const runCleanupsOnce = (): void => {
641
+ if (listenerController.signal.aborted) {
642
+ return
643
+ }
644
+
645
+ listenerController.abort()
646
+ void this.exhaustCleanups(cleanups)
647
+ }
648
+
649
+ void observedResponse.settled.then(runCleanupsOnce)
650
+
651
+ /**
652
+ * @note Also run the cleanups when the request is aborted.
653
+ * Stream cancellation does not always propagate to the observed
654
+ * response (e.g. if an unconsumed clone of the response exists),
655
+ * while the request abort reliably means the response is unused.
656
+ */
657
+ if (args.request.signal.aborted) {
658
+ runCleanupsOnce()
659
+ } else {
660
+ args.request.signal.addEventListener('abort', runCleanupsOnce, {
661
+ once: true,
662
+ signal: listenerController.signal,
663
+ })
664
+ }
665
+
666
+ return observedResponse.response
557
667
  }
558
668
  }
559
669
 
package/src/core/sse.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { invariant } from 'outvariant'
2
2
  import { DeferredPromise } from '@open-draft/deferred-promise'
3
- import { Emitter } from 'strict-event-emitter'
3
+ import { Emitter, TypedEvent } from 'rettime'
4
4
  import type { ResponseResolver } from './handlers/RequestHandler'
5
5
  import {
6
6
  HttpHandler,
@@ -105,11 +105,17 @@ class ServerSentEventHandler<
105
105
  this.#attachClientLogger(info.request, emitter)
106
106
  }
107
107
 
108
- await resolver({
109
- ...info,
110
- client,
111
- server,
112
- })
108
+ /**
109
+ * @note Extend the resolver info via assignment instead of a spread.
110
+ * Spreading the info would invoke its lazy `finalize` getter,
111
+ * initializing the finalize machinery for resolvers that never use it.
112
+ */
113
+ await resolver(
114
+ Object.assign(info, {
115
+ client,
116
+ server,
117
+ }),
118
+ )
113
119
 
114
120
  return response
115
121
  })
@@ -151,7 +157,14 @@ class ServerSentEventHandler<
151
157
  return matches
152
158
  }
153
159
 
154
- async log(_args: { request: Request; response: Response }): Promise<void> {
160
+ async log(args: { request: Request; response: Response }): Promise<void> {
161
+ /**
162
+ * @note Cancel the response stream because it's not needed for logging.
163
+ * Otherwise, this cloned response remains unconsumed and its original
164
+ * doesn't propagate stream cancelations at all.
165
+ */
166
+ args.response.body?.cancel()
167
+
155
168
  /**
156
169
  * @note Skip the default `this.log()` logic so that when this handler is logged
157
170
  * upon handling the request, nothing is printed (we log SSE requests early).
@@ -166,16 +179,14 @@ class ServerSentEventHandler<
166
179
  const publicUrl = toPublicUrl(request.url)
167
180
 
168
181
  /* eslint-disable no-console */
169
- emitter.on('message', (payload) => {
182
+ emitter.on('message', ({ data }) => {
170
183
  console.groupCollapsed(
171
- devUtils.formatMessage(
172
- `${getTimestamp()} SSE %s %c⇣%c ${payload.event}`,
173
- ),
184
+ devUtils.formatMessage(`${getTimestamp()} SSE %s %c⇣%c ${data.event}`),
174
185
  publicUrl,
175
186
  `color:${colors.mocked}`,
176
187
  'color:inherit',
177
188
  )
178
- console.log(payload.frames)
189
+ console.log(data.frames)
179
190
  console.groupEnd()
180
191
  })
181
192
 
@@ -227,9 +238,9 @@ type ToEventDiscriminatedUnion<T> = Values<{
227
238
  }>
228
239
 
229
240
  type ServerSentEventClientEventMap = {
230
- message: [payload: EventStreamMessage]
231
- error: []
232
- close: []
241
+ message: TypedEvent<EventStreamMessage>
242
+ error: TypedEvent
243
+ close: TypedEvent
233
244
  }
234
245
 
235
246
  const kClientEmitter = Symbol.for('kClientEmitter')
@@ -240,11 +251,13 @@ class ServerSentEventClient<
240
251
  private [kClientEmitter]?: Emitter<ServerSentEventClientEventMap>
241
252
 
242
253
  #encoder: TextEncoder
243
- #writer: WritableStreamDefaultWriter
254
+ #controller: ReadableStreamDefaultController<Uint8Array>
255
+ #closed: DeferredPromise<void>
244
256
 
245
- constructor(writable: WritableStream) {
257
+ constructor(controller: ReadableStreamDefaultController<Uint8Array>) {
246
258
  this.#encoder = new TextEncoder()
247
- this.#writer = writable.getWriter()
259
+ this.#controller = controller
260
+ this.#closed = new DeferredPromise()
248
261
  }
249
262
 
250
263
  /**
@@ -300,37 +313,50 @@ class ServerSentEventClient<
300
313
  * error.
301
314
  */
302
315
  public error(): void {
303
- this.#writer.abort().catch((error) => {
304
- console.error(error)
305
- devUtils.error(
306
- 'Failed to abort server-side EventSource. Please see the original error above.',
307
- )
308
- })
309
- this[kClientEmitter]?.emit('error')
316
+ if (this.#closed.state !== 'pending') {
317
+ return
318
+ }
319
+
320
+ this.#controller.error()
321
+ this.#closed.resolve()
322
+ this[kClientEmitter]?.emit(new TypedEvent('error'))
310
323
  }
311
324
 
312
325
  /**
313
326
  * Closes the underlying `EventSource`, closing the connection.
314
327
  */
315
328
  public close(): void {
316
- this.#writer.close().catch((error) => {
329
+ if (this.#closed.state !== 'pending') {
330
+ return
331
+ }
332
+
333
+ try {
334
+ this.#controller.close()
335
+ this.#closed.resolve()
336
+ } catch {
337
+ //
338
+ }
339
+
340
+ this[kClientEmitter]?.emit(new TypedEvent('close'))
341
+ }
342
+
343
+ #enqueue(chunk: Uint8Array): void {
344
+ if (this.#closed.state !== 'pending') {
345
+ return
346
+ }
347
+
348
+ try {
349
+ this.#controller.enqueue(chunk)
350
+ } catch (error) {
317
351
  console.error(error)
318
352
  devUtils.error(
319
- 'Failed to close server-side EventSource. Please see the original error above.',
353
+ 'Failed to write to server-side EventSource. Please see the original error above.',
320
354
  )
321
- })
322
- this[kClientEmitter]?.emit('close')
355
+ }
323
356
  }
324
357
 
325
358
  #sendRetry(retry: number): void {
326
- this.#writer
327
- .write(this.#encoder.encode(`retry:${retry}\n\n`))
328
- .catch((error) => {
329
- console.error(error)
330
- devUtils.error(
331
- 'Failed to send a retry packet to server-side EventSource. Please see the original error above.',
332
- )
333
- })
359
+ this.#enqueue(this.#encoder.encode(`retry:${retry}\n\n`))
334
360
  }
335
361
 
336
362
  #sendMessage(message: {
@@ -361,21 +387,18 @@ class ServerSentEventClient<
361
387
 
362
388
  frames.push('', '')
363
389
 
364
- this.#writer
365
- .write(this.#encoder.encode(frames.join('\n')))
366
- .catch((error) => {
367
- console.error(error)
368
- devUtils.error(
369
- 'Failed to send a message to server-side EventSource. Please see the original error above.',
370
- )
371
- })
390
+ this.#enqueue(this.#encoder.encode(frames.join('\n')))
372
391
 
373
- this[kClientEmitter]?.emit('message', {
374
- id: message.id,
375
- event: message.event?.toString() || 'message',
376
- data: message.data,
377
- frames,
378
- })
392
+ this[kClientEmitter]?.emit(
393
+ new TypedEvent('message', {
394
+ data: {
395
+ id: message.id,
396
+ event: message.event?.toString() || 'message',
397
+ data: message.data,
398
+ frames,
399
+ },
400
+ }),
401
+ )
379
402
  }
380
403
  }
381
404
 
@@ -1002,9 +1025,18 @@ function createEventStream<EventMap extends EventMapConstraint>(
1002
1025
  request.url,
1003
1026
  )
1004
1027
 
1005
- const { readable, writable } = new TransformStream()
1028
+ let controller!: ReadableStreamDefaultController<Uint8Array>
1029
+
1030
+ const readable = new ReadableStream<Uint8Array>({
1031
+ start(defaultController) {
1032
+ controller = defaultController
1033
+ },
1034
+ cancel() {
1035
+ client.close()
1036
+ },
1037
+ })
1006
1038
 
1007
- const client = new ServerSentEventClient<EventMap>(writable)
1039
+ const client = new ServerSentEventClient<EventMap>(controller)
1008
1040
  const server = new ServerSentEventServer({
1009
1041
  request,
1010
1042
  client,
@@ -1,5 +1,5 @@
1
1
  import statuses from '../../../shims/statuses'
2
- import type { HttpResponseInit } from '../../HttpResponse'
2
+ import type { HttpResponse, HttpResponseInit } from '../../HttpResponse'
3
3
 
4
4
  const { message } = statuses
5
5
 
@@ -27,7 +27,7 @@ export function normalizeResponseInit(
27
27
  }
28
28
 
29
29
  export function decorateResponse(
30
- response: Response,
30
+ response: HttpResponse<any>,
31
31
  init: HttpResponseDecoratedInit,
32
32
  ): Response {
33
33
  // Allow mocking the response type.
@@ -59,3 +59,24 @@ export function decorateResponse(
59
59
  export function getRawSetCookie(response: Response): string | undefined {
60
60
  return Reflect.get(response, kSetCookie)
61
61
  }
62
+
63
+ /**
64
+ * Copy the given response own properties, like internal symbols,
65
+ * onto another response. Used for faithful internal copying of responses.
66
+ */
67
+ export function copyResponseOwnProperties(
68
+ source: Response,
69
+ target: Response,
70
+ ): void {
71
+ for (const propertyName of Reflect.ownKeys(source)) {
72
+ const descriptor = Object.getOwnPropertyDescriptor(source, propertyName)
73
+ const existingDescriptor = Object.getOwnPropertyDescriptor(
74
+ target,
75
+ propertyName,
76
+ )
77
+
78
+ if (descriptor && existingDescriptor == null) {
79
+ Object.defineProperty(target, propertyName, descriptor)
80
+ }
81
+ }
82
+ }