msw 2.14.2 → 2.14.4

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 (79) hide show
  1. package/lib/browser/index.js +88 -38
  2. package/lib/browser/index.js.map +1 -1
  3. package/lib/browser/index.mjs +88 -38
  4. package/lib/browser/index.mjs.map +1 -1
  5. package/lib/core/{HttpResponse-aGiIzO91.d.ts → HttpResponse-BFS34nkx.d.ts} +16 -0
  6. package/lib/core/{HttpResponse-CxHR1nNN.d.mts → HttpResponse-CQwYpuKo.d.mts} +16 -0
  7. package/lib/core/HttpResponse.d.mts +1 -1
  8. package/lib/core/HttpResponse.d.ts +1 -1
  9. package/lib/core/experimental/compat.d.mts +1 -1
  10. package/lib/core/experimental/compat.d.ts +1 -1
  11. package/lib/core/experimental/define-network.d.mts +1 -1
  12. package/lib/core/experimental/define-network.d.ts +1 -1
  13. package/lib/core/experimental/frames/http-frame.d.mts +1 -1
  14. package/lib/core/experimental/frames/http-frame.d.ts +1 -1
  15. package/lib/core/experimental/frames/network-frame.d.mts +1 -1
  16. package/lib/core/experimental/frames/network-frame.d.ts +1 -1
  17. package/lib/core/experimental/frames/websocket-frame.d.mts +1 -1
  18. package/lib/core/experimental/frames/websocket-frame.d.ts +1 -1
  19. package/lib/core/experimental/handlers-controller.d.mts +1 -1
  20. package/lib/core/experimental/handlers-controller.d.ts +1 -1
  21. package/lib/core/experimental/index.d.mts +1 -1
  22. package/lib/core/experimental/index.d.ts +1 -1
  23. package/lib/core/experimental/on-unhandled-frame.d.mts +1 -1
  24. package/lib/core/experimental/on-unhandled-frame.d.ts +1 -1
  25. package/lib/core/experimental/setup-api.d.mts +1 -1
  26. package/lib/core/experimental/setup-api.d.ts +1 -1
  27. package/lib/core/experimental/sources/interceptor-source.d.mts +1 -1
  28. package/lib/core/experimental/sources/interceptor-source.d.ts +1 -1
  29. package/lib/core/experimental/sources/interceptor-source.js +14 -10
  30. package/lib/core/experimental/sources/interceptor-source.js.map +1 -1
  31. package/lib/core/experimental/sources/interceptor-source.mjs +14 -10
  32. package/lib/core/experimental/sources/interceptor-source.mjs.map +1 -1
  33. package/lib/core/experimental/sources/network-source.d.mts +1 -1
  34. package/lib/core/experimental/sources/network-source.d.ts +1 -1
  35. package/lib/core/getResponse.d.mts +1 -1
  36. package/lib/core/getResponse.d.ts +1 -1
  37. package/lib/core/graphql.d.mts +1 -1
  38. package/lib/core/graphql.d.ts +1 -1
  39. package/lib/core/handlers/GraphQLHandler.d.mts +1 -1
  40. package/lib/core/handlers/GraphQLHandler.d.ts +1 -1
  41. package/lib/core/handlers/HttpHandler.d.mts +1 -1
  42. package/lib/core/handlers/HttpHandler.d.ts +1 -1
  43. package/lib/core/handlers/RequestHandler.d.mts +1 -1
  44. package/lib/core/handlers/RequestHandler.d.ts +1 -1
  45. package/lib/core/handlers/RequestHandler.js +82 -1
  46. package/lib/core/handlers/RequestHandler.js.map +1 -1
  47. package/lib/core/handlers/RequestHandler.mjs +82 -1
  48. package/lib/core/handlers/RequestHandler.mjs.map +1 -1
  49. package/lib/core/http.d.mts +1 -1
  50. package/lib/core/http.d.ts +1 -1
  51. package/lib/core/index.d.mts +1 -1
  52. package/lib/core/index.d.ts +1 -1
  53. package/lib/core/passthrough.d.mts +1 -1
  54. package/lib/core/passthrough.d.ts +1 -1
  55. package/lib/core/sse.d.mts +1 -1
  56. package/lib/core/sse.d.ts +1 -1
  57. package/lib/core/utils/HttpResponse/decorators.d.mts +1 -1
  58. package/lib/core/utils/HttpResponse/decorators.d.ts +1 -1
  59. package/lib/core/utils/executeHandlers.d.mts +1 -1
  60. package/lib/core/utils/executeHandlers.d.ts +1 -1
  61. package/lib/core/utils/handleRequest.d.mts +1 -1
  62. package/lib/core/utils/handleRequest.d.ts +1 -1
  63. package/lib/core/utils/internal/attachSiblingHandlers.d.mts +1 -1
  64. package/lib/core/utils/internal/attachSiblingHandlers.d.ts +1 -1
  65. package/lib/core/utils/internal/isHandlerKind.d.mts +1 -1
  66. package/lib/core/utils/internal/isHandlerKind.d.ts +1 -1
  67. package/lib/core/utils/internal/parseGraphQLRequest.d.mts +1 -1
  68. package/lib/core/utils/internal/parseGraphQLRequest.d.ts +1 -1
  69. package/lib/core/utils/internal/parseMultipartData.d.mts +1 -1
  70. package/lib/core/utils/internal/parseMultipartData.d.ts +1 -1
  71. package/lib/core/ws/handleWebSocketEvent.d.mts +1 -1
  72. package/lib/core/ws/handleWebSocketEvent.d.ts +1 -1
  73. package/lib/iife/index.js +183 -49
  74. package/lib/iife/index.js.map +1 -1
  75. package/lib/mockServiceWorker.js +1 -1
  76. package/package.json +5 -4
  77. package/src/browser/sources/service-worker-source.ts +17 -12
  78. package/src/core/experimental/sources/interceptor-source.ts +20 -10
  79. package/src/core/handlers/RequestHandler.ts +144 -10
@@ -7,7 +7,7 @@
7
7
  * - Please do NOT modify this file.
8
8
  */
9
9
 
10
- const PACKAGE_VERSION = '2.14.2'
10
+ const PACKAGE_VERSION = '2.14.4'
11
11
  const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
12
12
  const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
13
13
  const activeClientIds = new Set()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msw",
3
- "version": "2.14.2",
3
+ "version": "2.14.4",
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",
@@ -233,7 +233,7 @@
233
233
  "outvariant": "^1.4.3",
234
234
  "path-to-regexp": "^6.3.0",
235
235
  "picocolors": "^1.1.1",
236
- "rettime": "^0.11.7",
236
+ "rettime": "^0.11.11",
237
237
  "statuses": "^2.0.2",
238
238
  "strict-event-emitter": "^0.5.1",
239
239
  "tough-cookie": "^6.0.1",
@@ -286,7 +286,7 @@
286
286
  "vitest-environment-miniflare": "^2.14.4",
287
287
  "webpack": "^5.106.2",
288
288
  "webpack-http-server": "^0.5.0",
289
- "msw": "^2.14.2"
289
+ "msw": "^2.14.4"
290
290
  },
291
291
  "peerDependencies": {
292
292
  "typescript": ">= 4.8.x"
@@ -313,7 +313,7 @@
313
313
  "build": "pnpm clean && cross-env NODE_ENV=production tsup && pnpm patch:dts",
314
314
  "patch:dts": "node \"./config/scripts/patch-ts.js\"",
315
315
  "publint": "publint",
316
- "test": "pnpm test:unit && pnpm test:node && pnpm test:browser && pnpm test:native",
316
+ "test": "pnpm test:unit && pnpm test:node && pnpm test:browser && pnpm test:native && pnpm test:memory",
317
317
  "test:unit": "vitest",
318
318
  "test:node": "vitest --config=./test/node/vitest.config.ts",
319
319
  "test:native": "vitest --config=./test/native/vitest.config.ts",
@@ -322,6 +322,7 @@
322
322
  "test:modules:browser": "playwright test -c ./test/modules/browser/playwright.config.ts",
323
323
  "test:e2e": "vitest run --config=./test/e2e/vitest.config.ts",
324
324
  "test:ts": "vitest --config=./test/typings/vitest.config.ts",
325
+ "test:memory": "vitest --config=./test/memory/vitest.config.ts",
325
326
  "release": "release publish",
326
327
  "postinstall": "node -e \"import('./config/scripts/postinstall.js').catch(() => void 0)\"",
327
328
  "knip": "knip"
@@ -330,6 +330,7 @@ Please consider using a custom "serviceWorker.url" option to point to the actual
330
330
 
331
331
  async #handleResponse(event: WorkerChannelResponseEvent): Promise<void> {
332
332
  const { request, response, isMockedResponse } = event.data
333
+ const frame = this.#frames.get(request.id)
333
334
 
334
335
  /**
335
336
  * CORS requests with `mode: "no-cors"` result in "opaque" responses.
@@ -340,10 +341,10 @@ Please consider using a custom "serviceWorker.url" option to point to the actual
340
341
  */
341
342
  if (response.type?.includes('opaque')) {
342
343
  this.#frames.delete(request.id)
344
+ frame?.events.removeAllListeners()
343
345
  return
344
346
  }
345
347
 
346
- const frame = this.#frames.get(request.id)
347
348
  this.#frames.delete(request.id)
348
349
 
349
350
  /**
@@ -379,17 +380,21 @@ Please consider using a custom "serviceWorker.url" option to point to the actual
379
380
  },
380
381
  )
381
382
 
382
- frame.events.emit(
383
- new ResponseEvent(
384
- isMockedResponse ? 'response:mocked' : 'response:bypass',
385
- {
386
- requestId: frame.data.id,
387
- request: fetchRequest,
388
- response: fetchResponse,
389
- isMockedResponse,
390
- },
391
- ),
392
- )
383
+ try {
384
+ frame.events.emit(
385
+ new ResponseEvent(
386
+ isMockedResponse ? 'response:mocked' : 'response:bypass',
387
+ {
388
+ requestId: frame.data.id,
389
+ request: fetchRequest,
390
+ response: fetchResponse,
391
+ isMockedResponse,
392
+ },
393
+ ),
394
+ )
395
+ } finally {
396
+ frame.events.removeAllListeners()
397
+ }
393
398
  }
394
399
 
395
400
  #defaultFindWorker: FindWorker = (workerUrl, mockServiceWorkerUrl) => {
@@ -87,16 +87,26 @@ export class InterceptorSource extends NetworkSource {
87
87
  }
88
88
 
89
89
  queueMicrotask(() => {
90
- httpFrame.events.emit(
91
- new ResponseEvent(
92
- isMockedResponse ? 'response:mocked' : 'response:bypass',
93
- {
94
- requestId,
95
- request,
96
- response,
97
- },
98
- ),
99
- )
90
+ try {
91
+ httpFrame.events.emit(
92
+ new ResponseEvent(
93
+ isMockedResponse ? 'response:mocked' : 'response:bypass',
94
+ {
95
+ requestId,
96
+ request,
97
+ response,
98
+ },
99
+ ),
100
+ )
101
+ } finally {
102
+ /**
103
+ * @note Remove any listeners from this frame.
104
+ * Past this point, it won't emit anything. The removal is crucial
105
+ * to prevent "rettime" from keeping the abort cleanup listeners internally.
106
+ * @see https://github.com/mswjs/msw/issues/2735
107
+ */
108
+ httpFrame.events.removeAllListeners()
109
+ }
100
110
  })
101
111
  }
102
112
 
@@ -13,6 +13,7 @@ import {
13
13
  type DefaultUnsafeFetchResponse,
14
14
  } from '../HttpResponse'
15
15
  import type { GraphQLRequestBody } from './GraphQLHandler'
16
+ import { devUtils } from '../utils/internal/devUtils'
16
17
  import { getRawSetCookie } from '../utils/HttpResponse/decorators'
17
18
 
18
19
  export type DefaultRequestMultipartBody = Record<
@@ -90,8 +91,22 @@ export type ResponseResolverInfo<
90
91
  > = {
91
92
  request: StrictRequest<RequestBodyType>
92
93
  requestId: string
94
+ /**
95
+ * Schedule a callback to run after this response resolver completes.
96
+ * Handy for cleaning up the side effects introduced in the resolver.
97
+ * @example
98
+ * sse('/', ({ client, finalize }) => {
99
+ * const interval = setInterval(() => client.send({ data: 'ping' }))
100
+ * finalize(() => clearInterval(interval))
101
+ * })
102
+ */
103
+ finalize: ResponseResolverFinalizeFunction
93
104
  } & ResolverExtraInfo
94
105
 
106
+ type ResponseResolverFinalizeFunction = (
107
+ callback: () => MaybePromise<void>,
108
+ ) => void
109
+
95
110
  export type ResponseResolver<
96
111
  ResolverExtraInfo extends Record<string, unknown> = Record<string, unknown>,
97
112
  RequestBodyType extends DefaultBodyType = DefaultBodyType,
@@ -149,7 +164,9 @@ export abstract class RequestHandler<
149
164
  MaybeAsyncResponseResolverReturnType<any>
150
165
  >
151
166
  private resolverIteratorResult?: Response | HttpResponse<any>
167
+ private resolverIteratorCleanups?: Array<() => MaybePromise<void>>
152
168
  private options?: HandlerOptions
169
+ private scheduledCleanups: Map<string, Array<() => MaybePromise<void>>>
153
170
 
154
171
  public info: HandlerInfo & RequestHandlerInternalInfo
155
172
 
@@ -162,6 +179,7 @@ export abstract class RequestHandler<
162
179
  constructor(args: RequestHandlerArgs<HandlerInfo, HandlerOptions>) {
163
180
  this.resolver = args.resolver
164
181
  this.options = args.options
182
+ this.scheduledCleanups = new Map()
165
183
 
166
184
  const callFrame = getCallFrame(new Error())
167
185
 
@@ -180,9 +198,12 @@ export abstract class RequestHandler<
180
198
  * from a clean state.
181
199
  */
182
200
  protected reset(): void {
201
+ this.scheduledCleanups.clear()
202
+
183
203
  const iterator = this.resolverIterator
184
204
  this.resolverIterator = undefined
185
205
  this.resolverIteratorResult = undefined
206
+ this.resolverIteratorCleanups = undefined
186
207
 
187
208
  if (typeof iterator?.return === 'function') {
188
209
  void Promise.resolve(iterator.return())
@@ -332,21 +353,39 @@ export abstract class RequestHandler<
332
353
  parsedResult,
333
354
  })
334
355
 
356
+ const listenerController = new AbortController()
357
+
358
+ args.request.signal.addEventListener(
359
+ 'abort',
360
+ () => this.runScheduledCleanups(args.requestId),
361
+ {
362
+ once: true,
363
+ signal: listenerController.signal,
364
+ },
365
+ )
366
+
335
367
  const mockedResponsePromise = (
336
368
  executeResolver({
337
369
  ...resolverExtras,
370
+ finalize: (callback) => {
371
+ this.scheduleCleanup(args.requestId, callback)
372
+ },
338
373
  requestId: args.requestId,
339
374
  request: args.request,
340
- }) as Promise<Response | undefined>
341
- ).catch((errorOrResponse) => {
342
- // Allow throwing a Response instance in a response resolver.
343
- if (errorOrResponse instanceof Response) {
344
- return errorOrResponse
345
- }
375
+ }) as Promise<Response>
376
+ )
377
+ .catch((errorOrResponse) => {
378
+ // Allow throwing a Response instance in a response resolver.
379
+ if (errorOrResponse instanceof Response) {
380
+ return errorOrResponse
381
+ }
346
382
 
347
- // Otherwise, throw the error as-is.
348
- throw errorOrResponse
349
- })
383
+ // Otherwise, throw the error as-is.
384
+ throw errorOrResponse
385
+ })
386
+ .finally(() => {
387
+ listenerController.abort()
388
+ })
350
389
 
351
390
  const mockedResponse = await mockedResponsePromise
352
391
 
@@ -371,12 +410,40 @@ export abstract class RequestHandler<
371
410
  ): ResponseResolver<ResolverExtras> {
372
411
  return async (info): Promise<ResponseResolverReturnType<any>> => {
373
412
  if (!this.resolverIterator) {
374
- const result = await resolver(info)
413
+ let result
414
+
415
+ try {
416
+ result = await resolver(info)
417
+ } catch (error) {
418
+ // Ensure cleanup if the resolver throws, regardless of the returned type.
419
+ await this.runScheduledCleanups(info.requestId)
420
+
421
+ // Re-throw the error for the "run()" method to handle (e.g. thrown responses).
422
+ throw error
423
+ }
375
424
 
376
425
  if (!isIterable(result)) {
426
+ // Otherwise, run the cleanup immediately if it's a plain response resolver.
427
+ await this.runScheduledCleanups(info.requestId)
377
428
  return result
378
429
  }
379
430
 
431
+ /**
432
+ * @note Carry over any previously registered cleanups onto the iterator cleanups.
433
+ * This is only relevant if "finalize()" is called in a regular resolver that
434
+ * returns an iterator.
435
+ * @example
436
+ * http.get('/', async ({ finalize }) => {
437
+ * finalize(cleanup)
438
+ * return (async function*() {})()
439
+ * })
440
+ */
441
+ const existingCleanups = this.scheduledCleanups.get(info.requestId)
442
+ if (existingCleanups != null && existingCleanups.length > 0) {
443
+ this.resolverIteratorCleanups = existingCleanups
444
+ this.scheduledCleanups.delete(info.requestId)
445
+ }
446
+
380
447
  this.resolverIterator =
381
448
  Symbol.iterator in result
382
449
  ? result[Symbol.iterator]()
@@ -398,6 +465,8 @@ export abstract class RequestHandler<
398
465
  // only after it's been completely exhausted.
399
466
  this.isUsed = true
400
467
 
468
+ await this.runScheduledCleanups(info.requestId)
469
+
401
470
  // Clone the previously stored response so it can be read
402
471
  // when receiving it repeatedly from the "done" generator.
403
472
  return this.resolverIteratorResult?.clone()
@@ -421,6 +490,71 @@ export abstract class RequestHandler<
421
490
  parsedResult: args.parsedResult,
422
491
  }
423
492
  }
493
+
494
+ private scheduleCleanup(
495
+ requestId: string,
496
+ callback: () => MaybePromise<void>,
497
+ ): void {
498
+ if (this.resolverIterator) {
499
+ ;(this.resolverIteratorCleanups ||= []).unshift(callback)
500
+ return
501
+ }
502
+
503
+ const cleanups = this.scheduledCleanups.get(requestId) || []
504
+ cleanups.unshift(callback)
505
+ this.scheduledCleanups.set(requestId, cleanups)
506
+ }
507
+
508
+ private async exhaustCleanups(
509
+ cleanups: Array<() => MaybePromise<void>>,
510
+ ): Promise<void> {
511
+ const errors: Array<Error> = []
512
+
513
+ for (const cleanup of cleanups) {
514
+ try {
515
+ await cleanup()
516
+ } catch (error) {
517
+ if (error instanceof Error) {
518
+ errors.push(error)
519
+ }
520
+ }
521
+ }
522
+
523
+ if (errors.length > 0) {
524
+ devUtils.error(
525
+ 'Failed to execute cleanup for request handler "%s"',
526
+ this.info.header,
527
+ new AggregateError(
528
+ errors,
529
+ `Failed to execute cleanup for request handler "${this.info.header}"`,
530
+ ),
531
+ )
532
+ }
533
+ }
534
+
535
+ private async runScheduledCleanups(requestId: string): Promise<void> {
536
+ if (
537
+ this.resolverIterator &&
538
+ this.resolverIteratorCleanups != null &&
539
+ this.resolverIteratorCleanups.length > 0
540
+ ) {
541
+ try {
542
+ await this.exhaustCleanups(this.resolverIteratorCleanups)
543
+ } finally {
544
+ this.resolverIteratorCleanups = undefined
545
+ }
546
+ return
547
+ }
548
+
549
+ const cleanups = this.scheduledCleanups.get(requestId)
550
+
551
+ if (!cleanups || cleanups.length == 0) {
552
+ return
553
+ }
554
+
555
+ await this.exhaustCleanups(cleanups)
556
+ this.scheduledCleanups.delete(requestId)
557
+ }
424
558
  }
425
559
 
426
560
  /**