@mswjs/interceptors 0.25.0 → 0.25.2

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 (77) hide show
  1. package/lib/browser/Interceptor-8d5fd4c6.d.ts +86 -0
  2. package/lib/browser/{chunk-4CFMDU7Z.js → chunk-CWVY2E3W.js} +20 -134
  3. package/lib/browser/{chunk-VMXB5F2J.mjs → chunk-HXJPKJY3.mjs} +25 -15
  4. package/lib/browser/{chunk-DBFLI5DJ.js → chunk-KITNLK66.js} +30 -20
  5. package/lib/browser/chunk-KK6APRON.mjs +58 -0
  6. package/lib/browser/{chunk-OSIUQA4X.js → chunk-NMG5MQJJ.js} +32 -29
  7. package/lib/browser/{chunk-GXJLJMOT.mjs → chunk-QPMXOLDO.mjs} +21 -135
  8. package/lib/browser/{chunk-ANLPTCZ5.mjs → chunk-TYEVJTWH.mjs} +27 -24
  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 +18 -18
  26. package/lib/node/RemoteHttpInterceptor.mjs +14 -14
  27. package/lib/node/{chunk-XYZRP5S2.js → chunk-3XFLRXRY.js} +20 -134
  28. package/lib/node/chunk-5PTPJLB7.js +58 -0
  29. package/lib/node/{chunk-E6YC337Q.js → chunk-5YAV7CXX.js} +29 -26
  30. package/lib/node/{chunk-HSCXCLVT.mjs → chunk-7RGC35CC.mjs} +27 -24
  31. package/lib/node/{chunk-3MYUI4B2.js → chunk-B2CIOP5B.js} +22 -16
  32. package/lib/node/{chunk-RGYCLCLK.mjs → chunk-GM3YBSM3.mjs} +21 -135
  33. package/lib/node/{chunk-OL7OR4RL.mjs → chunk-OMRBBJT7.mjs} +20 -14
  34. package/lib/node/{chunk-VS3GJPUE.mjs → chunk-UBEFEZXT.mjs} +22 -3
  35. package/lib/node/{chunk-MVPEJK4V.js → chunk-UF7QIAQ5.js} +23 -4
  36. package/lib/node/chunk-YQGTMMOZ.mjs +58 -0
  37. package/lib/node/index.d.ts +3 -2
  38. package/lib/node/index.js +3 -3
  39. package/lib/node/index.mjs +2 -2
  40. package/lib/node/interceptors/ClientRequest/index.d.ts +4 -3
  41. package/lib/node/interceptors/ClientRequest/index.js +4 -4
  42. package/lib/node/interceptors/ClientRequest/index.mjs +3 -3
  43. package/lib/node/interceptors/XMLHttpRequest/index.d.ts +4 -3
  44. package/lib/node/interceptors/XMLHttpRequest/index.js +4 -4
  45. package/lib/node/interceptors/XMLHttpRequest/index.mjs +3 -3
  46. package/lib/node/interceptors/fetch/index.d.ts +2 -1
  47. package/lib/node/interceptors/fetch/index.js +27 -17
  48. package/lib/node/interceptors/fetch/index.mjs +25 -15
  49. package/lib/node/presets/node.d.ts +3 -2
  50. package/lib/node/presets/node.js +6 -6
  51. package/lib/node/presets/node.mjs +4 -4
  52. package/package.json +2 -2
  53. package/src/BatchInterceptor.test.ts +141 -0
  54. package/src/BatchInterceptor.ts +38 -4
  55. package/src/Interceptor.test.ts +46 -0
  56. package/src/Interceptor.ts +35 -16
  57. package/src/RemoteHttpInterceptor.ts +11 -9
  58. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +10 -10
  59. package/src/interceptors/ClientRequest/NodeClientRequest.ts +35 -18
  60. package/src/interceptors/ClientRequest/index.test.ts +2 -3
  61. package/src/interceptors/ClientRequest/index.ts +2 -2
  62. package/src/interceptors/ClientRequest/utils/createRequest.test.ts +2 -2
  63. package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +29 -25
  64. package/src/interceptors/XMLHttpRequest/index.ts +2 -2
  65. package/src/interceptors/fetch/index.ts +26 -13
  66. package/src/utils/RequestController.ts +21 -0
  67. package/src/utils/emitAsync.ts +25 -0
  68. package/src/utils/toInteractiveRequest.ts +17 -23
  69. package/lib/browser/Interceptor-0a020bc4.d.ts +0 -116
  70. package/lib/browser/chunk-PCFJD76X.js +0 -64
  71. package/lib/browser/chunk-RT3ATOJH.mjs +0 -64
  72. package/lib/node/Interceptor-738f79c5.d.ts +0 -116
  73. package/lib/node/chunk-STA6QBYM.mjs +0 -64
  74. package/lib/node/chunk-ZJOF5MEZ.js +0 -64
  75. package/src/utils/AsyncEventEmitter.test.ts +0 -102
  76. package/src/utils/AsyncEventEmitter.ts +0 -193
  77. package/src/utils/createLazyCallback.ts +0 -49
@@ -1,6 +1,5 @@
1
1
  import { Logger } from '@open-draft/logger'
2
- import { Listener } from 'strict-event-emitter'
3
- import { AsyncEventEmitter } from './utils/AsyncEventEmitter'
2
+ import { Emitter, EventMap, Listener } from 'strict-event-emitter'
4
3
 
5
4
  export type InterceptorEventMap = Record<string, any>
6
5
  export type InterceptorSubscription = () => void
@@ -34,7 +33,7 @@ export type ExtractEventNames<Events extends Record<string, any>> =
34
33
  Events extends Record<infer EventName, any> ? EventName : never
35
34
 
36
35
  export class Interceptor<Events extends InterceptorEventMap> {
37
- protected emitter: AsyncEventEmitter<Events>
36
+ protected emitter: Emitter<Events>
38
37
  protected subscriptions: Array<InterceptorSubscription>
39
38
  protected logger: Logger
40
39
 
@@ -43,7 +42,7 @@ export class Interceptor<Events extends InterceptorEventMap> {
43
42
  constructor(private readonly symbol: symbol) {
44
43
  this.readyState = InterceptorReadyState.INACTIVE
45
44
 
46
- this.emitter = new AsyncEventEmitter()
45
+ this.emitter = new Emitter()
47
46
  this.subscriptions = []
48
47
  this.logger = new Logger(symbol.description!)
49
48
 
@@ -84,12 +83,6 @@ export class Interceptor<Events extends InterceptorEventMap> {
84
83
 
85
84
  this.readyState = InterceptorReadyState.APPLYING
86
85
 
87
- // Always activate the emitter when applying the interceptor.
88
- // This will ensure the interceptor can process events after it's
89
- // been disposed and re-applied again (it may be a singleton).
90
- this.emitter.activate()
91
- logger.info('activated the emiter!', this.emitter.readyState)
92
-
93
86
  // Whenever applying a new interceptor, check if it hasn't been applied already.
94
87
  // This enables to apply the same interceptor multiple times, for example from a different
95
88
  // interceptor, only proxying events but keeping the stubs in a single place.
@@ -112,6 +105,8 @@ export class Interceptor<Events extends InterceptorEventMap> {
112
105
  runningInstance.emitter.removeListener(event, listener)
113
106
  logger.info('removed proxied "%s" listener!', event)
114
107
  })
108
+
109
+ return this
115
110
  }
116
111
 
117
112
  this.readyState = InterceptorReadyState.APPLIED
@@ -141,9 +136,9 @@ export class Interceptor<Events extends InterceptorEventMap> {
141
136
  * Listen to the interceptor's public events.
142
137
  */
143
138
  public on<EventName extends ExtractEventNames<Events>>(
144
- eventName: EventName,
139
+ event: EventName,
145
140
  listener: Listener<Events[EventName]>
146
- ): void {
141
+ ): this {
147
142
  const logger = this.logger.extend('on')
148
143
 
149
144
  if (
@@ -151,12 +146,36 @@ export class Interceptor<Events extends InterceptorEventMap> {
151
146
  this.readyState === InterceptorReadyState.DISPOSED
152
147
  ) {
153
148
  logger.info('cannot listen to events, already disposed!')
154
- return
149
+ return this
155
150
  }
156
151
 
157
- logger.info('adding "%s" event listener:', eventName, listener.name)
152
+ logger.info('adding "%s" event listener:', event, listener.name)
153
+
154
+ this.emitter.on(event, listener)
155
+ return this
156
+ }
157
+
158
+ public once<EventName extends ExtractEventNames<Events>>(
159
+ event: EventName,
160
+ listener: Listener<Events[EventName]>
161
+ ): this {
162
+ this.emitter.once(event, listener)
163
+ return this
164
+ }
165
+
166
+ public off<EventName extends ExtractEventNames<Events>>(
167
+ event: EventName,
168
+ listener: Listener<Events[EventName]>
169
+ ): this {
170
+ this.emitter.off(event, listener)
171
+ return this
172
+ }
158
173
 
159
- this.emitter.on(eventName, listener)
174
+ public removeAllListeners<EventName extends ExtractEventNames<Events>>(
175
+ event?: EventName
176
+ ): this {
177
+ this.emitter.removeAllListeners(event)
178
+ return this
160
179
  }
161
180
 
162
181
  /**
@@ -196,7 +215,7 @@ export class Interceptor<Events extends InterceptorEventMap> {
196
215
  logger.info('disposed of all subscriptions!', this.subscriptions.length)
197
216
  }
198
217
 
199
- this.emitter.deactivate()
218
+ this.emitter.removeAllListeners()
200
219
  logger.info('destroyed the listener!')
201
220
 
202
221
  this.readyState = InterceptorReadyState.DISPOSED
@@ -5,6 +5,7 @@ import { BatchInterceptor } from './BatchInterceptor'
5
5
  import { ClientRequestInterceptor } from './interceptors/ClientRequest'
6
6
  import { XMLHttpRequestInterceptor } from './interceptors/XMLHttpRequest'
7
7
  import { toInteractiveRequest } from './utils/toInteractiveRequest'
8
+ import { emitAsync } from './utils/emitAsync'
8
9
 
9
10
  export interface SerializedRequest {
10
11
  id: string
@@ -166,20 +167,21 @@ export class RemoteHttpResolver extends Interceptor<HttpRequestEventMap> {
166
167
  body: requestJson.body,
167
168
  })
168
169
 
169
- const interactiveRequest = toInteractiveRequest(capturedRequest)
170
+ const { interactiveRequest, requestController } =
171
+ toInteractiveRequest(capturedRequest)
170
172
 
171
- this.emitter.emit('request', {
173
+ this.emitter.once('request', () => {
174
+ if (requestController.responsePromise.state === 'pending') {
175
+ requestController.respondWith(undefined)
176
+ }
177
+ })
178
+
179
+ await emitAsync(this.emitter, 'request', {
172
180
  request: interactiveRequest,
173
181
  requestId: requestJson.id,
174
182
  })
175
183
 
176
- await this.emitter.untilIdle(
177
- 'request',
178
- ({ args: [{ requestId: pendingRequestId }] }) => {
179
- return pendingRequestId === requestJson.id
180
- }
181
- )
182
- const [mockedResponse] = await interactiveRequest.respondWith.invoked()
184
+ const mockedResponse = await requestController.responsePromise
183
185
 
184
186
  if (!mockedResponse) {
185
187
  return
@@ -1,13 +1,13 @@
1
1
  import { vi, it, expect, beforeAll, afterAll } from 'vitest'
2
2
  import express from 'express'
3
3
  import { IncomingMessage } from 'http'
4
+ import { Emitter } from 'strict-event-emitter'
4
5
  import { Logger } from '@open-draft/logger'
5
6
  import { HttpServer } from '@open-draft/test-server/http'
6
7
  import { DeferredPromise } from '@open-draft/deferred-promise'
7
8
  import { NodeClientRequest } from './NodeClientRequest'
8
9
  import { getIncomingMessageBody } from './utils/getIncomingMessageBody'
9
10
  import { normalizeClientRequestArgs } from './utils/normalizeClientRequestArgs'
10
- import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
11
11
  import { sleep } from '../../../test/helpers'
12
12
  import { HttpRequestEventMap } from '../../glossary'
13
13
 
@@ -37,7 +37,7 @@ afterAll(async () => {
37
37
  })
38
38
 
39
39
  it('gracefully finishes the request when it has a mocked response', async () => {
40
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
40
+ const emitter = new Emitter<HttpRequestEventMap>()
41
41
  const request = new NodeClientRequest(
42
42
  normalizeClientRequestArgs('http:', 'http://any.thing', {
43
43
  method: 'PUT',
@@ -88,7 +88,7 @@ it('gracefully finishes the request when it has a mocked response', async () =>
88
88
  })
89
89
 
90
90
  it('responds with a mocked response when requesting an existing hostname', async () => {
91
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
91
+ const emitter = new Emitter<HttpRequestEventMap>()
92
92
  const request = new NodeClientRequest(
93
93
  normalizeClientRequestArgs('http:', httpServer.http.url('/comment')),
94
94
  {
@@ -116,7 +116,7 @@ it('responds with a mocked response when requesting an existing hostname', async
116
116
  })
117
117
 
118
118
  it('performs the request as-is given resolver returned no mocked response', async () => {
119
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
119
+ const emitter = new Emitter<HttpRequestEventMap>()
120
120
  const request = new NodeClientRequest(
121
121
  normalizeClientRequestArgs('http:', httpServer.http.url('/comment'), {
122
122
  method: 'POST',
@@ -147,7 +147,7 @@ it('performs the request as-is given resolver returned no mocked response', asyn
147
147
  })
148
148
 
149
149
  it('emits the ENOTFOUND error connecting to a non-existing hostname given no mocked response', async () => {
150
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
150
+ const emitter = new Emitter<HttpRequestEventMap>()
151
151
  const request = new NodeClientRequest(
152
152
  normalizeClientRequestArgs('http:', 'http://non-existing-url.com'),
153
153
  { emitter, logger }
@@ -165,7 +165,7 @@ it('emits the ENOTFOUND error connecting to a non-existing hostname given no moc
165
165
  })
166
166
 
167
167
  it('emits the ECONNREFUSED error connecting to an inactive server given no mocked response', async () => {
168
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
168
+ const emitter = new Emitter<HttpRequestEventMap>()
169
169
  const request = new NodeClientRequest(
170
170
  normalizeClientRequestArgs('http:', 'http://127.0.0.1:12345'),
171
171
  {
@@ -191,7 +191,7 @@ it('emits the ECONNREFUSED error connecting to an inactive server given no mocke
191
191
  })
192
192
 
193
193
  it('does not emit ENOTFOUND error connecting to an inactive server given mocked response', async () => {
194
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
194
+ const emitter = new Emitter<HttpRequestEventMap>()
195
195
  const handleError = vi.fn()
196
196
  const request = new NodeClientRequest(
197
197
  normalizeClientRequestArgs('http:', 'http://non-existing-url.com'),
@@ -221,7 +221,7 @@ it('does not emit ENOTFOUND error connecting to an inactive server given mocked
221
221
  })
222
222
 
223
223
  it('does not emit ECONNREFUSED error connecting to an inactive server given mocked response', async () => {
224
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
224
+ const emitter = new Emitter<HttpRequestEventMap>()
225
225
  const handleError = vi.fn()
226
226
  const request = new NodeClientRequest(
227
227
  normalizeClientRequestArgs('http:', 'http://localhost:9876'),
@@ -253,7 +253,7 @@ it('does not emit ECONNREFUSED error connecting to an inactive server given mock
253
253
  })
254
254
 
255
255
  it('sends the request body to the server given no mocked response', async () => {
256
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
256
+ const emitter = new Emitter<HttpRequestEventMap>()
257
257
  const request = new NodeClientRequest(
258
258
  normalizeClientRequestArgs('http:', httpServer.http.url('/write'), {
259
259
  method: 'POST',
@@ -284,7 +284,7 @@ it('sends the request body to the server given no mocked response', async () =>
284
284
  })
285
285
 
286
286
  it('does not send request body to the original server given mocked response', async () => {
287
- const emitter = new AsyncEventEmitter<HttpRequestEventMap>()
287
+ const emitter = new Emitter<HttpRequestEventMap>()
288
288
  const request = new NodeClientRequest(
289
289
  normalizeClientRequestArgs('http:', httpServer.http.url('/write'), {
290
290
  method: 'POST',
@@ -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).
@@ -155,28 +166,34 @@ export class NodeClientRequest extends ClientRequest {
155
166
  'emitting the "request" event for %d listener(s)...',
156
167
  this.emitter.listenerCount('request')
157
168
  )
158
- this.emitter.emit('request', {
159
- request: interactiveRequest,
160
- requestId,
169
+
170
+ // Add the last "request" listener that always resolves
171
+ // the pending response Promise. This way if the consumer
172
+ // hasn't handled the request themselves, we will prevent
173
+ // the response Promise from pending indefinitely.
174
+ this.emitter.once('request', ({ requestId: pendingRequestId }) => {
175
+ /**
176
+ * @note Ignore request events emitted by irrelevant
177
+ * requests. This happens when response patching.
178
+ */
179
+ if (pendingRequestId !== requestId) {
180
+ return
181
+ }
182
+
183
+ if (requestController.responsePromise.state === 'pending') {
184
+ requestController.responsePromise.resolve(undefined)
185
+ }
161
186
  })
162
187
 
163
188
  // Execute the resolver Promise like a side-effect.
164
189
  // Node.js 16 forces "ClientRequest.end" to be synchronous and return "this".
165
190
  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
- }
177
- )
191
+ await emitAsync(this.emitter, 'request', {
192
+ request: interactiveRequest,
193
+ requestId,
194
+ })
178
195
 
179
- const [mockedResponse] = await interactiveRequest.respondWith.invoked()
196
+ const mockedResponse = await requestController.responsePromise
180
197
  this.logger.info('event.respondWith called with:', mockedResponse)
181
198
 
182
199
  return mockedResponse
@@ -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 { 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,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
+ }