@mswjs/interceptors 0.25.1 → 0.25.3

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 (85) hide show
  1. package/lib/browser/Interceptor-8d5fd4c6.d.ts +86 -0
  2. package/lib/browser/{chunk-ANLPTCZ5.mjs → chunk-AN3YI76R.mjs} +33 -26
  3. package/lib/browser/{chunk-4CFMDU7Z.js → chunk-CWVY2E3W.js} +20 -134
  4. package/lib/browser/{chunk-OSIUQA4X.js → chunk-FFBQOFWV.js} +38 -31
  5. package/lib/browser/{chunk-VMXB5F2J.mjs → chunk-HXJPKJY3.mjs} +25 -15
  6. package/lib/browser/{chunk-DBFLI5DJ.js → chunk-KITNLK66.js} +30 -20
  7. package/lib/browser/chunk-KK6APRON.mjs +58 -0
  8. package/lib/browser/{chunk-GXJLJMOT.mjs → chunk-QPMXOLDO.mjs} +21 -135
  9. package/lib/browser/chunk-X3NRJIZW.js +58 -0
  10. package/lib/browser/index.d.ts +7 -3
  11. package/lib/browser/index.js +24 -5
  12. package/lib/browser/index.mjs +22 -3
  13. package/lib/browser/interceptors/XMLHttpRequest/index.d.ts +4 -3
  14. package/lib/browser/interceptors/XMLHttpRequest/index.js +4 -4
  15. package/lib/browser/interceptors/XMLHttpRequest/index.mjs +3 -3
  16. package/lib/browser/interceptors/fetch/index.d.ts +2 -1
  17. package/lib/browser/interceptors/fetch/index.js +4 -4
  18. package/lib/browser/interceptors/fetch/index.mjs +3 -3
  19. package/lib/browser/presets/browser.d.ts +2 -1
  20. package/lib/browser/presets/browser.js +6 -6
  21. package/lib/browser/presets/browser.mjs +4 -4
  22. package/lib/node/{BatchInterceptor-c841b068.d.ts → BatchInterceptor-9785c567.d.ts} +5 -2
  23. package/lib/node/Interceptor-7a701c1f.d.ts +86 -0
  24. package/lib/node/RemoteHttpInterceptor.d.ts +3 -2
  25. package/lib/node/RemoteHttpInterceptor.js +19 -18
  26. package/lib/node/RemoteHttpInterceptor.mjs +15 -14
  27. package/lib/node/chunk-3IYIKC3X.mjs +6 -0
  28. package/lib/node/{chunk-XYZRP5S2.js → chunk-3XFLRXRY.js} +20 -134
  29. package/lib/node/chunk-5PTPJLB7.js +58 -0
  30. package/lib/node/{chunk-HSCXCLVT.mjs → chunk-FB53TMYN.mjs} +33 -26
  31. package/lib/node/{chunk-RGYCLCLK.mjs → chunk-GM3YBSM3.mjs} +21 -135
  32. package/lib/node/{chunk-E6YC337Q.js → chunk-JCWVLTP7.js} +35 -28
  33. package/lib/node/{chunk-OL7OR4RL.mjs → chunk-MCO3RLQC.mjs} +48 -26
  34. package/lib/node/{chunk-3MYUI4B2.js → chunk-NCHFM2TB.js} +50 -28
  35. package/lib/node/chunk-OGN3ZR35.js +6 -0
  36. package/lib/node/{chunk-VS3GJPUE.mjs → chunk-UBEFEZXT.mjs} +22 -3
  37. package/lib/node/{chunk-MVPEJK4V.js → chunk-UF7QIAQ5.js} +23 -4
  38. package/lib/node/chunk-YQGTMMOZ.mjs +58 -0
  39. package/lib/node/index.d.ts +3 -2
  40. package/lib/node/index.js +3 -3
  41. package/lib/node/index.mjs +2 -2
  42. package/lib/node/interceptors/ClientRequest/index.d.ts +4 -3
  43. package/lib/node/interceptors/ClientRequest/index.js +5 -4
  44. package/lib/node/interceptors/ClientRequest/index.mjs +4 -3
  45. package/lib/node/interceptors/XMLHttpRequest/index.d.ts +4 -3
  46. package/lib/node/interceptors/XMLHttpRequest/index.js +5 -4
  47. package/lib/node/interceptors/XMLHttpRequest/index.mjs +4 -3
  48. package/lib/node/interceptors/fetch/index.d.ts +2 -1
  49. package/lib/node/interceptors/fetch/index.js +27 -17
  50. package/lib/node/interceptors/fetch/index.mjs +25 -15
  51. package/lib/node/presets/node.d.ts +3 -2
  52. package/lib/node/presets/node.js +7 -6
  53. package/lib/node/presets/node.mjs +5 -4
  54. package/package.json +2 -2
  55. package/src/BatchInterceptor.test.ts +141 -0
  56. package/src/BatchInterceptor.ts +38 -4
  57. package/src/Interceptor.test.ts +46 -0
  58. package/src/Interceptor.ts +35 -16
  59. package/src/RemoteHttpInterceptor.ts +11 -9
  60. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +10 -10
  61. package/src/interceptors/ClientRequest/NodeClientRequest.ts +80 -43
  62. package/src/interceptors/ClientRequest/http.get.ts +3 -1
  63. package/src/interceptors/ClientRequest/http.request.ts +3 -1
  64. package/src/interceptors/ClientRequest/index.test.ts +2 -3
  65. package/src/interceptors/ClientRequest/index.ts +2 -2
  66. package/src/interceptors/ClientRequest/utils/createRequest.test.ts +2 -2
  67. package/src/interceptors/ClientRequest/utils/createResponse.test.ts +23 -0
  68. package/src/interceptors/ClientRequest/utils/createResponse.ts +18 -13
  69. package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +29 -25
  70. package/src/interceptors/XMLHttpRequest/index.ts +2 -2
  71. package/src/interceptors/XMLHttpRequest/utils/createResponse.ts +4 -2
  72. package/src/interceptors/fetch/index.ts +26 -13
  73. package/src/utils/RequestController.ts +21 -0
  74. package/src/utils/emitAsync.ts +25 -0
  75. package/src/utils/responseUtils.ts +5 -0
  76. package/src/utils/toInteractiveRequest.ts +17 -23
  77. package/lib/browser/Interceptor-0a020bc4.d.ts +0 -116
  78. package/lib/browser/chunk-PCFJD76X.js +0 -64
  79. package/lib/browser/chunk-RT3ATOJH.mjs +0 -64
  80. package/lib/node/Interceptor-738f79c5.d.ts +0 -116
  81. package/lib/node/chunk-STA6QBYM.mjs +0 -64
  82. package/lib/node/chunk-ZJOF5MEZ.js +0 -64
  83. package/src/utils/AsyncEventEmitter.test.ts +0 -102
  84. package/src/utils/AsyncEventEmitter.ts +0 -193
  85. package/src/utils/createLazyCallback.ts +0 -49
@@ -1,6 +1,7 @@
1
1
  import { ClientRequest, IncomingMessage } from 'http'
2
2
  import type { Logger } from '@open-draft/logger'
3
3
  import { until } from '@open-draft/until'
4
+ import { DeferredPromise } from '@open-draft/deferred-promise'
4
5
  import type { ClientRequestEmitter } from '.'
5
6
  import {
6
7
  ClientRequestEndCallback,
@@ -17,7 +18,7 @@ import { createResponse } from './utils/createResponse'
17
18
  import { createRequest } from './utils/createRequest'
18
19
  import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
19
20
  import { uuidv4 } from '../../utils/uuid'
20
- import { DeferredPromise } from '@open-draft/deferred-promise'
21
+ import { emitAsync } from '../../utils/emitAsync'
21
22
 
22
23
  export type Protocol = 'http' | 'https'
23
24
 
@@ -138,7 +139,17 @@ export class NodeClientRequest extends ClientRequest {
138
139
  this.writeRequestBodyChunk(chunk, encoding || undefined)
139
140
 
140
141
  const capturedRequest = createRequest(this)
141
- const interactiveRequest = toInteractiveRequest(capturedRequest)
142
+ const { interactiveRequest, requestController } =
143
+ toInteractiveRequest(capturedRequest)
144
+
145
+ /**
146
+ * @todo Remove this modification of the original request
147
+ * and expose the controller alongside it in the "request"
148
+ * listener argument.
149
+ */
150
+ Object.defineProperty(capturedRequest, 'respondWith', {
151
+ value: requestController.respondWith.bind(requestController),
152
+ })
142
153
 
143
154
  // Prevent handling this request if it has already been handled
144
155
  // in another (parent) interceptor (like XMLHttpRequest -> ClientRequest).
@@ -149,34 +160,46 @@ export class NodeClientRequest extends ClientRequest {
149
160
  return this.passthrough(chunk, encoding, callback)
150
161
  }
151
162
 
152
- // Notify the interceptor about the request.
153
- // This will call any "request" listeners the users have.
154
- this.logger.info(
155
- 'emitting the "request" event for %d listener(s)...',
156
- this.emitter.listenerCount('request')
157
- )
158
- this.emitter.emit('request', {
159
- request: interactiveRequest,
160
- requestId,
163
+ // Add the last "request" listener that always resolves
164
+ // the pending response Promise. This way if the consumer
165
+ // hasn't handled the request themselves, we will prevent
166
+ // the response Promise from pending indefinitely.
167
+ this.emitter.once('request', ({ requestId: pendingRequestId }) => {
168
+ /**
169
+ * @note Ignore request events emitted by irrelevant
170
+ * requests. This happens when response patching.
171
+ */
172
+ if (pendingRequestId !== requestId) {
173
+ return
174
+ }
175
+
176
+ if (requestController.responsePromise.state === 'pending') {
177
+ this.logger.info(
178
+ 'request has not been handled in listeners, executing fail-safe listener...'
179
+ )
180
+
181
+ requestController.responsePromise.resolve(undefined)
182
+ }
161
183
  })
162
184
 
163
185
  // Execute the resolver Promise like a side-effect.
164
186
  // Node.js 16 forces "ClientRequest.end" to be synchronous and return "this".
165
187
  until(async () => {
166
- await this.emitter.untilIdle(
167
- 'request',
168
- ({ args: [{ requestId: pendingRequestId }] }) => {
169
- /**
170
- * @note Await only those listeners that are relevant to this request.
171
- * This prevents extraneous parallel request from blocking the resolution
172
- * of another, unrelated request. For example, during response patching,
173
- * when request resolution is nested.
174
- */
175
- return pendingRequestId === requestId
176
- }
188
+ // Notify the interceptor about the request.
189
+ // This will call any "request" listeners the users have.
190
+ this.logger.info(
191
+ 'emitting the "request" event for %d listener(s)...',
192
+ this.emitter.listenerCount('request')
177
193
  )
178
194
 
179
- const [mockedResponse] = await interactiveRequest.respondWith.invoked()
195
+ await emitAsync(this.emitter, 'request', {
196
+ request: interactiveRequest,
197
+ requestId,
198
+ })
199
+
200
+ this.logger.info('all "request" listeners done!')
201
+
202
+ const mockedResponse = await requestController.responsePromise
180
203
  this.logger.info('event.respondWith called with:', mockedResponse)
181
204
 
182
205
  return mockedResponse
@@ -203,6 +226,8 @@ export class NodeClientRequest extends ClientRequest {
203
226
  'encountered resolver exception, aborting request...',
204
227
  resolverResult.error
205
228
  )
229
+
230
+ this.destroyed = true
206
231
  this.emit('error', resolverResult.error)
207
232
  this.terminate()
208
233
 
@@ -212,7 +237,17 @@ export class NodeClientRequest extends ClientRequest {
212
237
  const mockedResponse = resolverResult.data
213
238
 
214
239
  if (mockedResponse) {
215
- this.logger.info('received mocked response:', mockedResponse)
240
+ this.logger.info(
241
+ 'received mocked response:',
242
+ mockedResponse.status,
243
+ mockedResponse.statusText
244
+ )
245
+
246
+ /**
247
+ * @note Ignore this request being destroyed by TLS in Node.js
248
+ * due to connection errors.
249
+ */
250
+ this.destroyed = false
216
251
 
217
252
  // Handle mocked "Response.error" network error responses.
218
253
  if (mockedResponse.type === 'error') {
@@ -431,10 +466,29 @@ export class NodeClientRequest extends ClientRequest {
431
466
  }
432
467
  this.logger.info('mocked response headers ready:', headers)
433
468
 
469
+ /**
470
+ * Set the internal "res" property to the mocked "OutgoingMessage"
471
+ * to make the "ClientRequest" instance think there's data received
472
+ * from the socket.
473
+ * @see https://github.com/nodejs/node/blob/9c405f2591f5833d0247ed0fafdcd68c5b14ce7a/lib/_http_client.js#L501
474
+ *
475
+ * Set the response immediately so the interceptor could stream data
476
+ * chunks to the request client as they come in.
477
+ */
478
+ // @ts-ignore
479
+ this.res = this.response
480
+ this.emit('response', this.response)
481
+
434
482
  const isResponseStreamFinished = new DeferredPromise<void>()
435
483
 
436
484
  const finishResponseStream = () => {
437
485
  this.logger.info('finished response stream!')
486
+
487
+ // Push "null" to indicate that the response body is complete
488
+ // and shouldn't be written to anymore.
489
+ this.response.push(null)
490
+ this.response.complete = true
491
+
438
492
  isResponseStreamFinished.resolve()
439
493
  }
440
494
 
@@ -458,29 +512,12 @@ export class NodeClientRequest extends ClientRequest {
458
512
  finishResponseStream()
459
513
  }
460
514
 
461
- /**
462
- * Set the internal "res" property to the mocked "OutgoingMessage"
463
- * to make the "ClientRequest" instance think there's data received
464
- * from the socket.
465
- * @see https://github.com/nodejs/node/blob/9c405f2591f5833d0247ed0fafdcd68c5b14ce7a/lib/_http_client.js#L501
466
- *
467
- * Set the response immediately so the interceptor could stream data
468
- * chunks to the request client as they come in.
469
- */
470
- // @ts-ignore
471
- this.res = this.response
472
- this.emit('response', this.response)
473
-
474
515
  isResponseStreamFinished.then(() => {
475
516
  this.logger.info('finalizing response...')
476
-
477
- // Push "null" to indicate that the response body is complete
478
- // and shouldn't be written to anymore.
479
- this.response.push(null)
480
- this.response.complete = true
481
517
  this.response.emit('end')
482
-
483
518
  this.terminate()
519
+
520
+ this.logger.info('request complete!')
484
521
  })
485
522
  }
486
523
 
@@ -10,7 +10,9 @@ import {
10
10
  } from './utils/normalizeClientRequestArgs'
11
11
 
12
12
  export function get(protocol: Protocol, options: NodeClientOptions) {
13
- return (...args: ClientRequestArgs): ClientRequest => {
13
+ return function interceptorsHttpGet(
14
+ ...args: ClientRequestArgs
15
+ ): ClientRequest {
14
16
  const clientRequestArgs = normalizeClientRequestArgs(
15
17
  `${protocol}:`,
16
18
  ...args
@@ -13,7 +13,9 @@ import {
13
13
  const logger = new Logger('http request')
14
14
 
15
15
  export function request(protocol: Protocol, options: NodeClientOptions) {
16
- return (...args: ClientRequestArgs): ClientRequest => {
16
+ return function interceptorsHttpRequest(
17
+ ...args: ClientRequestArgs
18
+ ): ClientRequest {
17
19
  logger.info('request call (protocol "%s"):', protocol, args)
18
20
 
19
21
  const clientRequestArgs = normalizeClientRequestArgs(
@@ -57,14 +57,13 @@ it('forbids calling "respondWith" multiple times for the same request', async ()
57
57
  expect(response.statusMessage).toBe('')
58
58
  })
59
59
 
60
-
61
60
  it('abort the request if the abort signal is emitted', async () => {
62
61
  const requestUrl = httpServer.http.url('/')
63
62
 
64
63
  const requestEmitted = new DeferredPromise<void>()
65
64
  interceptor.on('request', async function delayedResponse({ request }) {
66
65
  requestEmitted.resolve()
67
- await sleep(10000)
66
+ await sleep(10_000)
68
67
  request.respondWith(new Response())
69
68
  })
70
69
 
@@ -76,7 +75,7 @@ it('abort the request if the abort signal is emitted', async () => {
76
75
  abortController.abort()
77
76
 
78
77
  const requestAborted = new DeferredPromise<void>()
79
- request.on('error', function(err) {
78
+ request.on('error', function (err) {
80
79
  expect(err.name).toEqual('AbortError')
81
80
  requestAborted.resolve()
82
81
  })
@@ -1,13 +1,13 @@
1
1
  import http from 'http'
2
2
  import https from 'https'
3
+ import type { Emitter } from 'strict-event-emitter'
3
4
  import { HttpRequestEventMap } from '../../glossary'
4
5
  import { Interceptor } from '../../Interceptor'
5
- import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
6
6
  import { get } from './http.get'
7
7
  import { request } from './http.request'
8
8
  import { NodeClientOptions, Protocol } from './NodeClientRequest'
9
9
 
10
- export type ClientRequestEmitter = AsyncEventEmitter<HttpRequestEventMap>
10
+ export type ClientRequestEmitter = Emitter<HttpRequestEventMap>
11
11
 
12
12
  export type ClientRequestModules = Map<Protocol, typeof http | typeof https>
13
13
 
@@ -1,11 +1,11 @@
1
1
  import { it, expect } from 'vitest'
2
2
  import { Logger } from '@open-draft/logger'
3
3
  import { HttpRequestEventMap } from '../../..'
4
- import { AsyncEventEmitter } from '../../../utils/AsyncEventEmitter'
5
4
  import { NodeClientRequest } from '../NodeClientRequest'
6
5
  import { createRequest } from './createRequest'
6
+ import { Emitter } from 'strict-event-emitter'
7
7
 
8
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
8
+ const emitter = new Emitter<HttpRequestEventMap>()
9
9
  const logger = new Logger('test')
10
10
 
11
11
  it('creates a fetch Request with a JSON body', async () => {
@@ -2,6 +2,7 @@ import { it, expect } from 'vitest'
2
2
  import { Socket } from 'net'
3
3
  import * as http from 'http'
4
4
  import { createResponse } from './createResponse'
5
+ import { responseStatusCodesWithoutBody } from '../../../utils/responseUtils'
5
6
 
6
7
  it('creates a fetch api response from http incoming message', async () => {
7
8
  const message = new http.IncomingMessage(new Socket())
@@ -20,3 +21,25 @@ it('creates a fetch api response from http incoming message', async () => {
20
21
  expect(response.headers.get('content-type')).toBe('application/json')
21
22
  expect(await response.json()).toEqual({ firstName: 'John' })
22
23
  })
24
+
25
+ it.each(responseStatusCodesWithoutBody)(
26
+ 'ignores message body for %i response status',
27
+ (responseStatus) => {
28
+ const message = new http.IncomingMessage(new Socket())
29
+ message.statusCode = responseStatus
30
+
31
+ const response = createResponse(message)
32
+
33
+ // These chunks will be ignored: this response
34
+ // cannot have body. We don't forward this error to
35
+ // the consumer because it's us who converts the
36
+ // internal stream to a Fetch API Response instance.
37
+ // Consumers will rely on the Response API when constructing
38
+ // mocked responses.
39
+ message.emit('data', Buffer.from('hello'))
40
+ message.emit('end')
41
+
42
+ expect(response.status).toBe(responseStatus)
43
+ expect(response.body).toBe(null)
44
+ }
45
+ )
@@ -1,24 +1,29 @@
1
1
  import type { IncomingHttpHeaders, IncomingMessage } from 'http'
2
+ import { responseStatusCodesWithoutBody } from '../../../utils/responseUtils'
2
3
 
3
4
  /**
4
5
  * Creates a Fetch API `Response` instance from the given
5
6
  * `http.IncomingMessage` instance.
6
7
  */
7
8
  export function createResponse(message: IncomingMessage): Response {
8
- const readable = new ReadableStream({
9
- start(controller) {
10
- message.on('data', (chunk) => controller.enqueue(chunk))
11
- message.on('end', () => controller.close())
12
-
13
- /**
14
- * @todo Should also listen to the "error" on the message
15
- * and forward it to the controller. Otherwise the stream
16
- * will pend indefinitely.
17
- */
18
- },
19
- })
9
+ const responseBodyOrNull = responseStatusCodesWithoutBody.includes(
10
+ message.statusCode || 200
11
+ )
12
+ ? null
13
+ : new ReadableStream({
14
+ start(controller) {
15
+ message.on('data', (chunk) => controller.enqueue(chunk))
16
+ message.on('end', () => controller.close())
17
+
18
+ /**
19
+ * @todo Should also listen to the "error" on the message
20
+ * and forward it to the controller. Otherwise the stream
21
+ * will pend indefinitely.
22
+ */
23
+ },
24
+ })
20
25
 
21
- return new Response(readable, {
26
+ return new Response(responseBodyOrNull, {
22
27
  status: message.statusCode,
23
28
  statusText: message.statusMessage,
24
29
  headers: createHeadersFromIncomingHttpHeaders(message.headers),
@@ -2,6 +2,7 @@ import { until } from '@open-draft/until'
2
2
  import type { Logger } from '@open-draft/logger'
3
3
  import { XMLHttpRequestEmitter } from '.'
4
4
  import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
5
+ import { emitAsync } from '../../utils/emitAsync'
5
6
  import { XMLHttpRequestController } from './XMLHttpRequestController'
6
7
 
7
8
  export interface XMLHttpRequestProxyOptions {
@@ -42,38 +43,41 @@ export function createXMLHttpRequestProxy({
42
43
  )
43
44
  }
44
45
 
45
- const requestController = new XMLHttpRequestController(
46
+ const xhrRequestController = new XMLHttpRequestController(
46
47
  originalRequest,
47
48
  logger
48
49
  )
49
50
 
50
- requestController.onRequest = async function ({ request, requestId }) {
51
- // Notify the consumer about a new request.
52
- const interactiveRequest = toInteractiveRequest(request)
53
-
54
- this.logger.info(
55
- 'emitting the "request" event for %s listener(s)...',
56
- emitter.listenerCount('request')
57
- )
58
- emitter.emit('request', {
59
- request: interactiveRequest,
60
- requestId,
61
- })
51
+ xhrRequestController.onRequest = async function ({ request, requestId }) {
52
+ const { interactiveRequest, requestController } =
53
+ toInteractiveRequest(request)
62
54
 
63
55
  this.logger.info('awaiting mocked response...')
64
56
 
57
+ emitter.once('request', ({ requestId: pendingRequestId }) => {
58
+ if (pendingRequestId !== requestId) {
59
+ return
60
+ }
61
+
62
+ if (requestController.responsePromise.state === 'pending') {
63
+ requestController.respondWith(undefined)
64
+ }
65
+ })
66
+
65
67
  const resolverResult = await until(async () => {
66
- await emitter.untilIdle(
67
- 'request',
68
- ({ args: [{ requestId: pendingRequestId }] }) => {
69
- return pendingRequestId === requestId
70
- }
68
+ this.logger.info(
69
+ 'emitting the "request" event for %s listener(s)...',
70
+ emitter.listenerCount('request')
71
71
  )
72
72
 
73
+ await emitAsync(emitter, 'request', {
74
+ request: interactiveRequest,
75
+ requestId,
76
+ })
77
+
73
78
  this.logger.info('all "request" listeners settled!')
74
79
 
75
- const [mockedResponse] =
76
- await interactiveRequest.respondWith.invoked()
80
+ const mockedResponse = await requestController.responsePromise
77
81
 
78
82
  this.logger.info('event.respondWith called with:', mockedResponse)
79
83
 
@@ -91,7 +95,7 @@ export function createXMLHttpRequestProxy({
91
95
  * since not all consumers are expecting to handle errors.
92
96
  * If they don't, this error will be swallowed.
93
97
  */
94
- requestController.errorWith(resolverResult.error)
98
+ xhrRequestController.errorWith(resolverResult.error)
95
99
  return
96
100
  }
97
101
 
@@ -109,11 +113,11 @@ export function createXMLHttpRequestProxy({
109
113
  'received a network error response, rejecting the request promise...'
110
114
  )
111
115
 
112
- requestController.errorWith(new TypeError('Network error'))
116
+ xhrRequestController.errorWith(new TypeError('Network error'))
113
117
  return
114
118
  }
115
119
 
116
- return requestController.respondWith(mockedResponse)
120
+ return xhrRequestController.respondWith(mockedResponse)
117
121
  }
118
122
 
119
123
  this.logger.info(
@@ -121,7 +125,7 @@ export function createXMLHttpRequestProxy({
121
125
  )
122
126
  }
123
127
 
124
- requestController.onResponse = async function ({
128
+ xhrRequestController.onResponse = async function ({
125
129
  response,
126
130
  isMockedResponse,
127
131
  request,
@@ -143,7 +147,7 @@ export function createXMLHttpRequestProxy({
143
147
  // Return the proxied request from the controller
144
148
  // so that the controller can react to the consumer's interactions
145
149
  // with this request (opening/sending/etc).
146
- return requestController.request
150
+ return xhrRequestController.request
147
151
  },
148
152
  })
149
153
 
@@ -1,8 +1,8 @@
1
1
  import { invariant } from 'outvariant'
2
+ import { Emitter } from 'strict-event-emitter'
2
3
  import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
3
4
  import { InteractiveRequest } from '../../utils/toInteractiveRequest'
4
5
  import { Interceptor } from '../../Interceptor'
5
- import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
6
6
  import { createXMLHttpRequestProxy } from './XMLHttpRequestProxy'
7
7
 
8
8
  export type XMLHttpRequestEventListener = (args: {
@@ -10,7 +10,7 @@ export type XMLHttpRequestEventListener = (args: {
10
10
  requestId: string
11
11
  }) => Promise<void> | void
12
12
 
13
- export type XMLHttpRequestEmitter = AsyncEventEmitter<HttpRequestEventMap>
13
+ export type XMLHttpRequestEmitter = Emitter<HttpRequestEventMap>
14
14
 
15
15
  export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap> {
16
16
  static interceptorSymbol = Symbol('xhr')
@@ -1,4 +1,4 @@
1
- const statusCodesWithoutBody = [204, 205, 304]
1
+ import { responseStatusCodesWithoutBody } from '../../../utils/responseUtils'
2
2
 
3
3
  /**
4
4
  * Creates a Fetch API `Response` instance from the given
@@ -16,7 +16,9 @@ export function createResponse(
16
16
  * when constructing a Response instance.
17
17
  * @see https://github.com/mswjs/interceptors/issues/379
18
18
  */
19
- const responseBodyOrNull = statusCodesWithoutBody.includes(request.status)
19
+ const responseBodyOrNull = responseStatusCodesWithoutBody.includes(
20
+ request.status
21
+ )
20
22
  ? null
21
23
  : body
22
24
 
@@ -1,10 +1,11 @@
1
- import { DeferredPromise } from '@open-draft/deferred-promise'
2
1
  import { invariant } from 'outvariant'
2
+ import { DeferredPromise } from '@open-draft/deferred-promise'
3
3
  import { until } from '@open-draft/until'
4
4
  import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
5
5
  import { Interceptor } from '../../Interceptor'
6
6
  import { uuidv4 } from '../../utils/uuid'
7
7
  import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
8
+ import { emitAsync } from '../../utils/emitAsync'
8
9
 
9
10
  export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
10
11
  static symbol = Symbol('fetch')
@@ -34,15 +35,22 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
34
35
 
35
36
  this.logger.info('[%s] %s', request.method, request.url)
36
37
 
37
- const interactiveRequest = toInteractiveRequest(request)
38
+ const { interactiveRequest, requestController } =
39
+ toInteractiveRequest(request)
38
40
 
39
41
  this.logger.info(
40
42
  'emitting the "request" event for %d listener(s)...',
41
43
  this.emitter.listenerCount('request')
42
44
  )
43
- this.emitter.emit('request', {
44
- request: interactiveRequest,
45
- requestId,
45
+
46
+ this.emitter.once('request', ({ requestId: pendingRequestId }) => {
47
+ if (pendingRequestId !== requestId) {
48
+ return
49
+ }
50
+
51
+ if (requestController.responsePromise.state === 'pending') {
52
+ requestController.responsePromise.resolve(undefined)
53
+ }
46
54
  })
47
55
 
48
56
  this.logger.info('awaiting for the mocked response...')
@@ -59,18 +67,23 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
59
67
  )
60
68
 
61
69
  const resolverResult = await until(async () => {
62
- const allListenersResolved = this.emitter.untilIdle(
63
- 'request',
64
- ({ args: [{ requestId: pendingRequestId }] }) => {
65
- return pendingRequestId === requestId
66
- }
67
- )
70
+ const listenersFinished = emitAsync(this.emitter, 'request', {
71
+ request: interactiveRequest,
72
+ requestId,
73
+ })
68
74
 
69
- await Promise.race([requestAborted, allListenersResolved])
75
+ await Promise.race([
76
+ requestAborted,
77
+ // Put the listeners invocation Promise in the same race condition
78
+ // with the request abort Promise because otherwise awaiting the listeners
79
+ // would always yield some response (or undefined).
80
+ listenersFinished,
81
+ requestController.responsePromise,
82
+ ])
70
83
 
71
84
  this.logger.info('all request listeners have been resolved!')
72
85
 
73
- const [mockedResponse] = await interactiveRequest.respondWith.invoked()
86
+ const mockedResponse = await requestController.responsePromise
74
87
  this.logger.info('event.respondWith called with:', mockedResponse)
75
88
 
76
89
  return mockedResponse
@@ -0,0 +1,21 @@
1
+ import { invariant } from 'outvariant'
2
+ import { DeferredPromise } from '@open-draft/deferred-promise'
3
+
4
+ export class RequestController {
5
+ public responsePromise: DeferredPromise<Response | undefined>
6
+
7
+ constructor(protected request: Request) {
8
+ this.responsePromise = new DeferredPromise()
9
+ }
10
+
11
+ public respondWith(response?: Response): void {
12
+ invariant(
13
+ this.responsePromise.state === 'pending',
14
+ 'Failed to respond to "%s %s" request: the "request" event has already been responded to.',
15
+ this.request.method,
16
+ this.request.url
17
+ )
18
+
19
+ this.responsePromise.resolve(response)
20
+ }
21
+ }
@@ -0,0 +1,25 @@
1
+ import { Emitter, EventMap } from 'strict-event-emitter'
2
+
3
+ /**
4
+ * Emits an event on the given emitter but executes
5
+ * the listeners sequentially. This accounts for asynchronous
6
+ * listeners (e.g. those having "sleep" and handling the request).
7
+ */
8
+ export async function emitAsync<
9
+ Events extends EventMap,
10
+ EventName extends keyof Events
11
+ >(
12
+ emitter: Emitter<Events>,
13
+ eventName: EventName,
14
+ ...data: Events[EventName]
15
+ ): Promise<void> {
16
+ const listners = emitter.listeners(eventName)
17
+
18
+ if (listners.length === 0) {
19
+ return
20
+ }
21
+
22
+ for (const listener of listners) {
23
+ await listener.apply(emitter, data)
24
+ }
25
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Response status codes for responses that cannot have body.
3
+ * @see https://fetch.spec.whatwg.org/#statuses
4
+ */
5
+ export const responseStatusCodesWithoutBody = [204, 205, 304]