msw 2.13.3 → 2.13.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 (216) hide show
  1. package/cli/init.js +1 -1
  2. package/lib/browser/index.js +200 -56
  3. package/lib/browser/index.js.map +1 -1
  4. package/lib/browser/index.mjs +200 -56
  5. package/lib/browser/index.mjs.map +1 -1
  6. package/lib/core/{HttpResponse-DlRR1D-f.d.mts → HttpResponse-BF4NGRsf.d.mts} +1 -1
  7. package/lib/core/{HttpResponse-CksOMVAa.d.ts → HttpResponse-yukpQS4a.d.ts} +1 -1
  8. package/lib/core/HttpResponse.d.mts +1 -1
  9. package/lib/core/HttpResponse.d.ts +1 -1
  10. package/lib/core/experimental/compat.d.mts +2 -2
  11. package/lib/core/experimental/compat.d.ts +2 -2
  12. package/lib/core/experimental/compat.js +1 -0
  13. package/lib/core/experimental/compat.js.map +1 -1
  14. package/lib/core/experimental/compat.mjs +1 -0
  15. package/lib/core/experimental/compat.mjs.map +1 -1
  16. package/lib/core/experimental/define-network.d.mts +2 -2
  17. package/lib/core/experimental/define-network.d.ts +2 -2
  18. package/lib/core/experimental/define-network.js +4 -0
  19. package/lib/core/experimental/define-network.js.map +1 -1
  20. package/lib/core/experimental/define-network.mjs +6 -0
  21. package/lib/core/experimental/define-network.mjs.map +1 -1
  22. package/lib/core/experimental/frames/http-frame.d.mts +2 -2
  23. package/lib/core/experimental/frames/http-frame.d.ts +2 -2
  24. package/lib/core/experimental/frames/http-frame.js +2 -0
  25. package/lib/core/experimental/frames/http-frame.js.map +1 -1
  26. package/lib/core/experimental/frames/http-frame.mjs +5 -1
  27. package/lib/core/experimental/frames/http-frame.mjs.map +1 -1
  28. package/lib/core/experimental/frames/network-frame.d.mts +2 -2
  29. package/lib/core/experimental/frames/network-frame.d.ts +2 -2
  30. package/lib/core/experimental/frames/websocket-frame.d.mts +2 -2
  31. package/lib/core/experimental/frames/websocket-frame.d.ts +2 -2
  32. package/lib/core/experimental/frames/websocket-frame.js +2 -0
  33. package/lib/core/experimental/frames/websocket-frame.js.map +1 -1
  34. package/lib/core/experimental/frames/websocket-frame.mjs +2 -0
  35. package/lib/core/experimental/frames/websocket-frame.mjs.map +1 -1
  36. package/lib/core/experimental/handlers-controller.d.mts +1 -1
  37. package/lib/core/experimental/handlers-controller.d.ts +1 -1
  38. package/lib/core/experimental/handlers-controller.js +3 -1
  39. package/lib/core/experimental/handlers-controller.js.map +1 -1
  40. package/lib/core/experimental/handlers-controller.mjs +3 -1
  41. package/lib/core/experimental/handlers-controller.mjs.map +1 -1
  42. package/lib/core/experimental/index.d.mts +2 -2
  43. package/lib/core/experimental/index.d.ts +2 -2
  44. package/lib/core/experimental/index.js +0 -1
  45. package/lib/core/experimental/index.js.map +1 -1
  46. package/lib/core/experimental/index.mjs +1 -2
  47. package/lib/core/experimental/index.mjs.map +1 -1
  48. package/lib/core/experimental/on-unhandled-frame.d.mts +2 -2
  49. package/lib/core/experimental/on-unhandled-frame.d.ts +2 -2
  50. package/lib/core/experimental/on-unhandled-frame.js +1 -0
  51. package/lib/core/experimental/on-unhandled-frame.js.map +1 -1
  52. package/lib/core/experimental/on-unhandled-frame.mjs +1 -0
  53. package/lib/core/experimental/on-unhandled-frame.mjs.map +1 -1
  54. package/lib/core/experimental/setup-api.d.mts +1 -1
  55. package/lib/core/experimental/setup-api.d.ts +1 -1
  56. package/lib/core/experimental/setup-api.js +1 -0
  57. package/lib/core/experimental/setup-api.js.map +1 -1
  58. package/lib/core/experimental/setup-api.mjs +1 -0
  59. package/lib/core/experimental/setup-api.mjs.map +1 -1
  60. package/lib/core/experimental/sources/interceptor-source.d.mts +2 -2
  61. package/lib/core/experimental/sources/interceptor-source.d.ts +2 -2
  62. package/lib/core/experimental/sources/interceptor-source.js.map +1 -1
  63. package/lib/core/experimental/sources/interceptor-source.mjs +1 -3
  64. package/lib/core/experimental/sources/interceptor-source.mjs.map +1 -1
  65. package/lib/core/experimental/sources/network-source.d.mts +2 -2
  66. package/lib/core/experimental/sources/network-source.d.ts +2 -2
  67. package/lib/core/experimental/sources/network-source.js +1 -0
  68. package/lib/core/experimental/sources/network-source.js.map +1 -1
  69. package/lib/core/experimental/sources/network-source.mjs +2 -0
  70. package/lib/core/experimental/sources/network-source.mjs.map +1 -1
  71. package/lib/core/getResponse.d.mts +1 -1
  72. package/lib/core/getResponse.d.ts +1 -1
  73. package/lib/core/graphql.d.mts +1 -1
  74. package/lib/core/graphql.d.ts +1 -1
  75. package/lib/core/graphql.js +1 -0
  76. package/lib/core/graphql.js.map +1 -1
  77. package/lib/core/graphql.mjs +2 -0
  78. package/lib/core/graphql.mjs.map +1 -1
  79. package/lib/core/handlers/GraphQLHandler.d.mts +1 -1
  80. package/lib/core/handlers/GraphQLHandler.d.ts +1 -1
  81. package/lib/core/handlers/GraphQLHandler.js +1 -0
  82. package/lib/core/handlers/GraphQLHandler.js.map +1 -1
  83. package/lib/core/handlers/GraphQLHandler.mjs +4 -1
  84. package/lib/core/handlers/GraphQLHandler.mjs.map +1 -1
  85. package/lib/core/handlers/HttpHandler.d.mts +1 -1
  86. package/lib/core/handlers/HttpHandler.d.ts +1 -1
  87. package/lib/core/handlers/HttpHandler.js +1 -0
  88. package/lib/core/handlers/HttpHandler.js.map +1 -1
  89. package/lib/core/handlers/HttpHandler.mjs +1 -0
  90. package/lib/core/handlers/HttpHandler.mjs.map +1 -1
  91. package/lib/core/handlers/RequestHandler.d.mts +1 -1
  92. package/lib/core/handlers/RequestHandler.d.ts +1 -1
  93. package/lib/core/handlers/RequestHandler.js +1 -0
  94. package/lib/core/handlers/RequestHandler.js.map +1 -1
  95. package/lib/core/handlers/RequestHandler.mjs +2 -0
  96. package/lib/core/handlers/RequestHandler.mjs.map +1 -1
  97. package/lib/core/http.d.mts +1 -1
  98. package/lib/core/http.d.ts +1 -1
  99. package/lib/core/http.js +1 -0
  100. package/lib/core/http.js.map +1 -1
  101. package/lib/core/http.mjs +2 -0
  102. package/lib/core/http.mjs.map +1 -1
  103. package/lib/core/index.d.mts +1 -1
  104. package/lib/core/index.d.ts +1 -1
  105. package/lib/core/passthrough.d.mts +1 -1
  106. package/lib/core/passthrough.d.ts +1 -1
  107. package/lib/core/sse.d.mts +4 -18
  108. package/lib/core/sse.d.ts +4 -18
  109. package/lib/core/sse.js +105 -45
  110. package/lib/core/sse.js.map +1 -1
  111. package/lib/core/sse.mjs +105 -45
  112. package/lib/core/sse.mjs.map +1 -1
  113. package/lib/core/utils/HttpResponse/decorators.d.mts +1 -1
  114. package/lib/core/utils/HttpResponse/decorators.d.ts +1 -1
  115. package/lib/core/utils/cookieStore.js.map +1 -1
  116. package/lib/core/utils/cookieStore.mjs.map +1 -1
  117. package/lib/core/utils/executeHandlers.d.mts +1 -1
  118. package/lib/core/utils/executeHandlers.d.ts +1 -1
  119. package/lib/core/utils/executeHandlers.js +1 -0
  120. package/lib/core/utils/executeHandlers.js.map +1 -1
  121. package/lib/core/utils/executeHandlers.mjs +1 -0
  122. package/lib/core/utils/executeHandlers.mjs.map +1 -1
  123. package/lib/core/utils/handleRequest.d.mts +1 -1
  124. package/lib/core/utils/handleRequest.d.ts +1 -1
  125. package/lib/core/utils/handleRequest.js.map +1 -1
  126. package/lib/core/utils/handleRequest.mjs.map +1 -1
  127. package/lib/core/utils/internal/isHandlerKind.d.mts +1 -1
  128. package/lib/core/utils/internal/isHandlerKind.d.ts +1 -1
  129. package/lib/core/utils/internal/parseGraphQLRequest.d.mts +1 -1
  130. package/lib/core/utils/internal/parseGraphQLRequest.d.ts +1 -1
  131. package/lib/core/utils/internal/parseMultipartData.d.mts +1 -1
  132. package/lib/core/utils/internal/parseMultipartData.d.ts +1 -1
  133. package/lib/core/utils/internal/parseMultipartData.js +1 -0
  134. package/lib/core/utils/internal/parseMultipartData.js.map +1 -1
  135. package/lib/core/utils/internal/parseMultipartData.mjs +1 -0
  136. package/lib/core/utils/internal/parseMultipartData.mjs.map +1 -1
  137. package/lib/core/utils/internal/pipeEvents.js +1 -0
  138. package/lib/core/utils/internal/pipeEvents.js.map +1 -1
  139. package/lib/core/utils/internal/pipeEvents.mjs +1 -0
  140. package/lib/core/utils/internal/pipeEvents.mjs.map +1 -1
  141. package/lib/core/utils/internal/requestHandlerUtils.d.mts +1 -1
  142. package/lib/core/utils/internal/requestHandlerUtils.d.ts +1 -1
  143. package/lib/core/utils/internal/requestHandlerUtils.js.map +1 -1
  144. package/lib/core/utils/internal/requestHandlerUtils.mjs.map +1 -1
  145. package/lib/core/ws/WebSocketClientManager.js.map +1 -1
  146. package/lib/core/ws/WebSocketClientManager.mjs.map +1 -1
  147. package/lib/core/ws/WebSocketIndexedDBClientStore.js +1 -0
  148. package/lib/core/ws/WebSocketIndexedDBClientStore.js.map +1 -1
  149. package/lib/core/ws/WebSocketIndexedDBClientStore.mjs +1 -0
  150. package/lib/core/ws/WebSocketIndexedDBClientStore.mjs.map +1 -1
  151. package/lib/core/ws/WebSocketMemoryClientStore.js +1 -0
  152. package/lib/core/ws/WebSocketMemoryClientStore.js.map +1 -1
  153. package/lib/core/ws/WebSocketMemoryClientStore.mjs +1 -0
  154. package/lib/core/ws/WebSocketMemoryClientStore.mjs.map +1 -1
  155. package/lib/core/ws/handleWebSocketEvent.d.mts +1 -1
  156. package/lib/core/ws/handleWebSocketEvent.d.ts +1 -1
  157. package/lib/core/ws/handleWebSocketEvent.js.map +1 -1
  158. package/lib/core/ws/handleWebSocketEvent.mjs.map +1 -1
  159. package/lib/core/ws.js.map +1 -1
  160. package/lib/core/ws.mjs.map +1 -1
  161. package/lib/iife/index.js +6300 -6076
  162. package/lib/iife/index.js.map +1 -1
  163. package/lib/mockServiceWorker.js +1 -1
  164. package/lib/native/index.js.map +1 -1
  165. package/lib/native/index.mjs.map +1 -1
  166. package/lib/node/index.js.map +1 -1
  167. package/lib/node/index.mjs.map +1 -1
  168. package/lib/shims/cookie.js +152 -62
  169. package/lib/shims/cookie.mjs +152 -62
  170. package/package.json +33 -40
  171. package/src/browser/glossary.ts +1 -1
  172. package/src/browser/setup-worker.ts +2 -2
  173. package/src/browser/sources/service-worker-source.ts +125 -28
  174. package/src/browser/utils/deserializeRequest.ts +0 -1
  175. package/src/browser/utils/should-invalidate-worker.test.ts +122 -0
  176. package/src/browser/utils/should-invalidate-worker.ts +13 -0
  177. package/src/browser/utils/workerChannel.ts +43 -21
  178. package/src/core/experimental/define-network.ts +10 -2
  179. package/src/core/experimental/frames/http-frame.test.ts +2 -1
  180. package/src/core/experimental/frames/http-frame.ts +6 -2
  181. package/src/core/experimental/frames/websocket-frame.test.ts +2 -4
  182. package/src/core/experimental/frames/websocket-frame.ts +3 -2
  183. package/src/core/experimental/handlers-controller.ts +1 -1
  184. package/src/core/experimental/index.ts +1 -1
  185. package/src/core/experimental/on-unhandled-frame.test.ts +2 -4
  186. package/src/core/experimental/setup-api.ts +3 -3
  187. package/src/core/experimental/sources/interceptor-source.ts +2 -6
  188. package/src/core/graphql.ts +8 -8
  189. package/src/core/handlers/GraphQLHandler.test.ts +3 -4
  190. package/src/core/handlers/GraphQLHandler.ts +15 -11
  191. package/src/core/handlers/HttpHandler.test.ts +3 -2
  192. package/src/core/handlers/HttpHandler.ts +7 -7
  193. package/src/core/handlers/RequestHandler.ts +5 -5
  194. package/src/core/http.ts +5 -5
  195. package/src/core/sse.ts +157 -56
  196. package/src/core/utils/cookieStore.ts +1 -1
  197. package/src/core/utils/executeHandlers.ts +2 -4
  198. package/src/core/utils/handleRequest.test.ts +5 -4
  199. package/src/core/utils/handleRequest.ts +3 -3
  200. package/src/core/utils/internal/parseGraphQLRequest.test.ts +2 -4
  201. package/src/core/utils/internal/parseMultipartData.ts +1 -1
  202. package/src/core/utils/internal/pipeEvents.ts +2 -1
  203. package/src/core/utils/internal/requestHandlerUtils.ts +1 -1
  204. package/src/core/utils/request/onUnhandledRequest.test.ts +2 -4
  205. package/src/core/ws/WebSocketClientManager.test.ts +2 -4
  206. package/src/core/ws/WebSocketClientManager.ts +1 -1
  207. package/src/core/ws/WebSocketIndexedDBClientStore.ts +3 -5
  208. package/src/core/ws/WebSocketMemoryClientStore.ts +3 -5
  209. package/src/core/ws/handleWebSocketEvent.ts +3 -3
  210. package/src/core/ws.ts +1 -1
  211. package/src/native/index.ts +2 -2
  212. package/src/node/async-handlers-controller.ts +2 -2
  213. package/src/node/setup-server-common.ts +4 -4
  214. package/src/node/setup-server.ts +2 -2
  215. package/lib/core/{network-frame-usYiHS0K.d.ts → on-unhandled-frame-BBR-P3kV.d.ts} +12 -12
  216. package/lib/core/{network-frame-B7A0ggXE.d.mts → on-unhandled-frame-Cr1KOZ0I.d.mts} +12 -12
@@ -1,5 +1,5 @@
1
1
  import { invariant } from 'outvariant'
2
- import { Emitter } from 'rettime'
2
+ import type { Emitter } from 'rettime'
3
3
  import { DeferredPromise } from '@open-draft/deferred-promise'
4
4
  import { FetchResponse } from '@mswjs/interceptors'
5
5
  import { NetworkSource } from '#core/experimental/sources/network-source'
@@ -16,10 +16,12 @@ import {
16
16
  supportsServiceWorker,
17
17
  } from '../utils/supports'
18
18
  import { getWorkerInstance } from '../utils/get-worker-instance'
19
- import { WorkerChannel, WorkerChannelEventMap } from '../utils/workerChannel'
20
- import { FindWorker } from '../glossary'
19
+ import type { WorkerChannelEventMap } from '../utils/workerChannel'
20
+ import { WorkerChannel } from '../utils/workerChannel'
21
+ import type { FindWorker } from '../glossary'
21
22
  import { deserializeRequest } from '../utils/deserializeRequest'
22
23
  import { validateWorkerScope } from '../utils/validate-worker-scope'
24
+ import { shouldInvalidateWorker } from '../utils/should-invalidate-worker'
23
25
 
24
26
  export interface ServiceWorkerSourceOptions {
25
27
  quiet?: boolean
@@ -30,13 +32,13 @@ export interface ServiceWorkerSourceOptions {
30
32
  findWorker?: FindWorker
31
33
  }
32
34
 
33
- type WorkerChannelRequestEvent = Emitter.EventType<
35
+ type WorkerChannelRequestEvent = Emitter.Event<
34
36
  WorkerChannel,
35
37
  'REQUEST',
36
38
  WorkerChannelEventMap
37
39
  >
38
40
 
39
- type WorkerChannelResponseEvent = Emitter.EventType<
41
+ type WorkerChannelResponseEvent = Emitter.Event<
40
42
  WorkerChannel,
41
43
  'RESPONSE',
42
44
  WorkerChannelEventMap
@@ -46,8 +48,31 @@ type WorkerChannelClient =
46
48
  WorkerChannelEventMap['MOCKING_ENABLED']['data']['client']
47
49
 
48
50
  export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkFrame> {
51
+ static #current?: ServiceWorkerSource
52
+
53
+ /**
54
+ * Create a new Service Worker source or reuse an existing one.
55
+ * These sources act as a singleton and only get recreated if the options change.
56
+ */
57
+ public static async from(
58
+ options: ServiceWorkerSourceOptions,
59
+ ): Promise<ServiceWorkerSource> {
60
+ if (ServiceWorkerSource.#current == null) {
61
+ ServiceWorkerSource.#current = new ServiceWorkerSource(options)
62
+ } else if (
63
+ shouldInvalidateWorker(ServiceWorkerSource.#current.#options, options)
64
+ ) {
65
+ await ServiceWorkerSource.#current.terminate()
66
+ ServiceWorkerSource.#current = new ServiceWorkerSource(options)
67
+ }
68
+
69
+ return ServiceWorkerSource.#current
70
+ }
71
+
72
+ #options: ServiceWorkerSourceOptions
49
73
  #frames: Map<string, ServiceWorkerHttpNetworkFrame>
50
74
  #channel: WorkerChannel
75
+ #listenerController?: AbortController
51
76
  #clientPromise?: Promise<WorkerChannelClient>
52
77
  #keepAliveInterval?: number
53
78
  #stoppedAt?: number
@@ -56,7 +81,7 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
56
81
  [ServiceWorker, ServiceWorkerRegistration]
57
82
  >
58
83
 
59
- constructor(private readonly options: ServiceWorkerSourceOptions) {
84
+ constructor(options: ServiceWorkerSourceOptions) {
60
85
  super()
61
86
 
62
87
  invariant(
@@ -64,17 +89,25 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
64
89
  'Failed to use Service Worker as the network source: the Service Worker API is not supported in this environment',
65
90
  )
66
91
 
92
+ this.#options = options
67
93
  this.#frames = new Map()
68
94
  this.workerPromise = new DeferredPromise()
69
95
  this.#channel = new WorkerChannel({
70
- worker: this.workerPromise.then(([worker]) => worker),
96
+ getWorker: () => this.workerPromise.then(([worker]) => worker),
71
97
  })
72
98
  }
73
99
 
74
100
  public async enable(): Promise<ServiceWorkerRegistration> {
75
- this.#stoppedAt = undefined
76
-
77
- if (this.workerPromise.state !== 'pending') {
101
+ /**
102
+ * @note The source is considered already running if the worker has been
103
+ * resolved AND `stop()` has not been called since. `workerPromise` is NOT
104
+ * reset on `disable()` so that the channel's `getWorker()` can keep
105
+ * resolving to the registered SW for post-stop passthrough replies.
106
+ */
107
+ if (
108
+ this.workerPromise.state === 'fulfilled' &&
109
+ typeof this.#stoppedAt == 'undefined'
110
+ ) {
78
111
  devUtils.warn(
79
112
  'Found a redundant "worker.start()" call. Note that starting the worker while mocking is already enabled will have no effect. Consider removing this "worker.start()" call.',
80
113
  )
@@ -82,7 +115,11 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
82
115
  return this.workerPromise.then(([, registration]) => registration)
83
116
  }
84
117
 
118
+ this.#stoppedAt = undefined
85
119
  this.#channel.removeAllListeners()
120
+ this.#frames.clear()
121
+
122
+ this.#listenerController = new AbortController()
86
123
  const [worker, registration] = await this.#startWorker()
87
124
 
88
125
  if (worker.state !== 'activated') {
@@ -97,7 +134,9 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
97
134
  activationPromise.resolve()
98
135
  }
99
136
  },
100
- { signal: controller.signal },
137
+ {
138
+ signal: controller.signal,
139
+ },
101
140
  )
102
141
 
103
142
  await activationPromise
@@ -113,7 +152,7 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
113
152
  })
114
153
  await clientConfirmationPromise
115
154
 
116
- if (!this.options.quiet) {
155
+ if (!this.#options.quiet) {
117
156
  this.#printStartMessage()
118
157
  }
119
158
 
@@ -137,29 +176,70 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
137
176
  }
138
177
 
139
178
  this.#stoppedAt = Date.now()
140
- this.#frames.clear()
141
- this.workerPromise = new DeferredPromise()
142
179
 
143
- if (!this.options.quiet) {
180
+ this.#listenerController?.abort()
181
+ this.#listenerController = undefined
182
+
183
+ /**
184
+ * @note Tell the Service Worker to drop this client from its active set
185
+ * so it stops forwarding REQUEST events here. `stoppedAt` still guards
186
+ * any requests the SW already forwarded before this message arrived.
187
+ */
188
+ this.#channel.postMessage('CLIENT_CLOSED')
189
+
190
+ /**
191
+ * @note Do NOT reset `workerPromise` here. The channel must continue to
192
+ * resolve the currently registered SW so that any in-flight requests the
193
+ * worker forwards after `stop()` can be answered with `PASSTHROUGH` by
194
+ * `#handleRequest`. `#startWorker` swaps in a fresh deferred on re-enable.
195
+ */
196
+
197
+ if (!this.#options.quiet) {
144
198
  this.#printStopMessage()
145
199
  }
146
200
  }
147
201
 
202
+ /**
203
+ * Terminal teardown. Unregisters the Service Worker, tears down the channel,
204
+ * and clears timers. Called when the singleton is being replaced with one
205
+ * that has different options. The instance is not usable afterwards.
206
+ */
207
+ public async terminate(): Promise<void> {
208
+ if (this.#keepAliveInterval != null) {
209
+ clearInterval(this.#keepAliveInterval)
210
+ this.#keepAliveInterval = undefined
211
+ }
212
+
213
+ this.#frames.clear()
214
+ this.#channel.terminate()
215
+ this.#listenerController?.abort()
216
+ this.#listenerController = undefined
217
+
218
+ if (this.workerPromise.state === 'fulfilled') {
219
+ const [, registration] = await this.workerPromise
220
+ await registration.unregister()
221
+ }
222
+
223
+ if (ServiceWorkerSource.#current === this) {
224
+ ServiceWorkerSource.#current = undefined
225
+ }
226
+ }
227
+
148
228
  async #startWorker() {
149
229
  if (this.#keepAliveInterval) {
150
230
  clearInterval(this.#keepAliveInterval)
151
231
  }
152
232
 
153
- const workerUrl = this.options.serviceWorker.url
233
+ const workerUrl = this.#options.serviceWorker.url
154
234
 
155
235
  const [worker, registration] = await getWorkerInstance(
156
236
  workerUrl,
157
- this.options.serviceWorker.options,
158
- this.options.findWorker || this.#defaultFindWorker,
237
+ this.#options.serviceWorker.options,
238
+ this.#options.findWorker || this.#defaultFindWorker,
159
239
  )
160
240
 
161
241
  if (worker == null) {
162
- const missingWorkerMessage = this.options?.findWorker
242
+ const missingWorkerMessage = this.#options?.findWorker
163
243
  ? devUtils.formatMessage(
164
244
  `Failed to locate the Service Worker registration using a custom "findWorker" predicate.
165
245
 
@@ -181,20 +261,37 @@ Please consider using a custom "serviceWorker.url" option to point to the actual
181
261
  throw new Error(missingWorkerMessage)
182
262
  }
183
263
 
184
- this.workerPromise.resolve([worker, registration])
264
+ if (this.workerPromise.state === 'pending') {
265
+ this.workerPromise.resolve([worker, registration])
266
+ } else {
267
+ /**
268
+ * @note Re-enable after `stop()`: the previous `workerPromise` is already
269
+ * fulfilled and cannot be resolved again. Swap in a pre-resolved one so
270
+ * `getWorker()` sees the new worker instance immediately.
271
+ */
272
+ this.workerPromise = new DeferredPromise((resolve) => {
273
+ resolve([worker, registration])
274
+ })
275
+ }
185
276
 
186
277
  this.#channel.on('REQUEST', this.#handleRequest.bind(this))
187
278
  this.#channel.on('RESPONSE', this.#handleResponse.bind(this))
188
279
 
189
- window.addEventListener('beforeunload', () => {
190
- if (worker.state !== 'redundant') {
191
- this.#channel.postMessage('CLIENT_CLOSED')
192
- }
280
+ window.addEventListener(
281
+ 'beforeunload',
282
+ () => {
283
+ if (worker.state !== 'redundant') {
284
+ this.#channel.postMessage('CLIENT_CLOSED')
285
+ }
193
286
 
194
- clearInterval(this.#keepAliveInterval)
287
+ clearInterval(this.#keepAliveInterval)
195
288
 
196
- window.postMessage({ type: 'msw/worker:stop' })
197
- })
289
+ window.postMessage({ type: 'msw/worker:stop' })
290
+ },
291
+ {
292
+ signal: this.#listenerController?.signal,
293
+ },
294
+ )
198
295
 
199
296
  await this.#checkWorkerIntegrity().catch((error) => {
200
297
  devUtils.error(
@@ -207,7 +304,7 @@ Please consider using a custom "serviceWorker.url" option to point to the actual
207
304
  this.#channel.postMessage('KEEPALIVE_REQUEST')
208
305
  }, 5000)
209
306
 
210
- if (!this.options.quiet) {
307
+ if (!this.#options.quiet) {
211
308
  validateWorkerScope(registration)
212
309
  }
213
310
 
@@ -1,5 +1,4 @@
1
1
  import { pruneGetRequestBody } from './pruneGetRequestBody'
2
- import type { ServiceWorkerIncomingRequest } from '../glossary'
3
2
 
4
3
  /**
5
4
  * Converts a given request received from the Service Worker
@@ -0,0 +1,122 @@
1
+ import { type ServiceWorkerSourceOptions } from '../sources/service-worker-source'
2
+ import { shouldInvalidateWorker } from './should-invalidate-worker'
3
+
4
+ function createOptions(
5
+ overrides: Partial<ServiceWorkerSourceOptions> = {},
6
+ ): ServiceWorkerSourceOptions {
7
+ return {
8
+ serviceWorker: {
9
+ url: '/mockServiceWorker.js',
10
+ options: { scope: '/' },
11
+ },
12
+ ...overrides,
13
+ }
14
+ }
15
+
16
+ it('returns true when the worker url differs', () => {
17
+ expect(
18
+ shouldInvalidateWorker(
19
+ createOptions({
20
+ serviceWorker: { url: '/a.js', options: { scope: '/' } },
21
+ }),
22
+ createOptions({
23
+ serviceWorker: { url: '/b.js', options: { scope: '/' } },
24
+ }),
25
+ ),
26
+ ).toBe(true)
27
+ })
28
+
29
+ it('returns true when the registration options differ', () => {
30
+ expect(
31
+ shouldInvalidateWorker(
32
+ createOptions({
33
+ serviceWorker: { url: '/sw.js', options: { scope: '/' } },
34
+ }),
35
+ createOptions({
36
+ serviceWorker: { url: '/sw.js', options: { scope: '/app' } },
37
+ }),
38
+ ),
39
+ ).toBe(true)
40
+ })
41
+
42
+ it('returns true when only one side has registration options', () => {
43
+ expect(
44
+ shouldInvalidateWorker(
45
+ createOptions({ serviceWorker: { url: '/sw.js' } }),
46
+ createOptions({
47
+ serviceWorker: { url: '/sw.js', options: { scope: '/' } },
48
+ }),
49
+ ),
50
+ ).toBe(true)
51
+ })
52
+
53
+ it('returns true when findWorker differs by reference', () => {
54
+ expect(
55
+ shouldInvalidateWorker(
56
+ createOptions({ findWorker: () => true }),
57
+ createOptions({ findWorker: () => true }),
58
+ ),
59
+ ).toBe(true)
60
+ })
61
+
62
+ it('returns true when findWorker is added on one side', () => {
63
+ expect(
64
+ shouldInvalidateWorker(
65
+ createOptions(),
66
+ createOptions({ findWorker: () => true }),
67
+ ),
68
+ ).toBe(true)
69
+ })
70
+
71
+ it('returns false for the same options reference', () => {
72
+ const options = createOptions()
73
+ expect(shouldInvalidateWorker(options, options)).toBe(false)
74
+ })
75
+
76
+ it('returns false for deeply equal options', () => {
77
+ expect(
78
+ shouldInvalidateWorker(
79
+ createOptions({
80
+ serviceWorker: { url: '/sw.js', options: { scope: '/' } },
81
+ }),
82
+ createOptions({
83
+ serviceWorker: { url: '/sw.js', options: { scope: '/' } },
84
+ }),
85
+ ),
86
+ ).toBe(false)
87
+ })
88
+
89
+ it('returns false for the same worker url without options', () => {
90
+ expect(
91
+ shouldInvalidateWorker(
92
+ createOptions({ serviceWorker: { url: '/sw.js' } }),
93
+ createOptions({ serviceWorker: { url: '/sw.js' } }),
94
+ ),
95
+ ).toBe(false)
96
+ })
97
+
98
+ it('returns false when findWorker is the same reference', () => {
99
+ const findWorker = () => true
100
+ expect(
101
+ shouldInvalidateWorker(
102
+ createOptions({ findWorker }),
103
+ createOptions({ findWorker }),
104
+ ),
105
+ ).toBe(false)
106
+ })
107
+
108
+ it('returns false regardless of the "quiet" option', () => {
109
+ expect(
110
+ shouldInvalidateWorker(
111
+ createOptions({ quiet: true }),
112
+ createOptions({ quiet: true }),
113
+ ),
114
+ ).toBe(false)
115
+
116
+ expect(
117
+ shouldInvalidateWorker(
118
+ createOptions({ quiet: false }),
119
+ createOptions({ quiet: true }),
120
+ ),
121
+ ).toBe(false)
122
+ })
@@ -0,0 +1,13 @@
1
+ import { type ServiceWorkerSourceOptions } from '../sources/service-worker-source'
2
+
3
+ export function shouldInvalidateWorker(
4
+ prevOptions: ServiceWorkerSourceOptions,
5
+ nextOptions: ServiceWorkerSourceOptions,
6
+ ): boolean {
7
+ return (
8
+ prevOptions.findWorker !== nextOptions.findWorker ||
9
+ prevOptions.serviceWorker.url !== nextOptions.serviceWorker.url ||
10
+ JSON.stringify(prevOptions.serviceWorker.options) !==
11
+ JSON.stringify(nextOptions.serviceWorker.options)
12
+ )
13
+ }
@@ -4,10 +4,6 @@ import { isObject } from '#core/utils/internal/isObject'
4
4
  import type { StringifiedResponse } from '../glossary'
5
5
  import { supportsServiceWorker } from '../utils/supports'
6
6
 
7
- export interface WorkerChannelOptions {
8
- worker: Promise<ServiceWorker>
9
- }
10
-
11
7
  export type WorkerChannelEventMap = {
12
8
  REQUEST: WorkerEvent<IncomingWorkerRequest>
13
9
  RESPONSE: WorkerEvent<IncomingWorkerResponse>
@@ -121,25 +117,42 @@ type OutgoingWorkerEvents =
121
117
  | 'KEEPALIVE_REQUEST'
122
118
  | 'CLIENT_CLOSED'
123
119
 
124
- export class WorkerChannel extends Emitter<WorkerChannelEventMap> {
125
- constructor(protected readonly options: WorkerChannelOptions) {
126
- super()
120
+ export interface WorkerChannelOptions {
121
+ getWorker: () => Promise<ServiceWorker>
122
+ }
127
123
 
128
- if (!SUPPORTS_SERVICE_WORKER) {
129
- return
130
- }
124
+ export class WorkerChannel extends Emitter<WorkerChannelEventMap> {
125
+ #getWorker: WorkerChannelOptions['getWorker']
126
+ #controller: AbortController
131
127
 
132
- navigator.serviceWorker.addEventListener('message', async (event) => {
133
- const worker = await this.options.worker
128
+ constructor(options: WorkerChannelOptions) {
129
+ super()
134
130
 
135
- if (event.source != null && event.source !== worker) {
136
- return
137
- }
131
+ invariant(
132
+ SUPPORTS_SERVICE_WORKER,
133
+ 'Failed to open a WorkerChannel: Service Worker is not supported in this environment.',
134
+ )
138
135
 
139
- if (event.data && isObject(event.data) && 'type' in event.data) {
140
- this.emit(new WorkerEvent<any, any, any>(event))
141
- }
142
- })
136
+ this.#getWorker = options.getWorker
137
+ this.#controller = new AbortController()
138
+
139
+ navigator.serviceWorker.addEventListener(
140
+ 'message',
141
+ async (event) => {
142
+ const worker = await this.#getWorker()
143
+
144
+ if (event.source != null && event.source !== worker) {
145
+ return
146
+ }
147
+
148
+ if (event.data && isObject(event.data) && 'type' in event.data) {
149
+ this.emit(new WorkerEvent<any, any, any>(event))
150
+ }
151
+ },
152
+ {
153
+ signal: this.#controller.signal,
154
+ },
155
+ )
143
156
  }
144
157
 
145
158
  /**
@@ -149,11 +162,20 @@ export class WorkerChannel extends Emitter<WorkerChannelEventMap> {
149
162
  public postMessage(type: OutgoingWorkerEvents): void {
150
163
  invariant(
151
164
  SUPPORTS_SERVICE_WORKER,
152
- 'Failed to post message on a WorkerChannel: the Service Worker API is unavailable in this context. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues',
165
+ 'Failed to post message on a WorkerChannel: the Service Worker API is unavailable in this environment. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues',
153
166
  )
154
167
 
155
- this.options.worker.then((worker) => {
168
+ this.#getWorker().then((worker) => {
156
169
  worker.postMessage(type)
157
170
  })
158
171
  }
172
+
173
+ /**
174
+ * Terminal teardown. Removes the `navigator.serviceWorker` message listener
175
+ * and all emitter subscriptions. The channel is not usable afterwards.
176
+ */
177
+ public terminate(): void {
178
+ this.#controller.abort()
179
+ this.removeAllListeners()
180
+ }
159
181
  }
@@ -1,15 +1,15 @@
1
1
  import { invariant } from 'outvariant'
2
2
  import { Emitter, type DefaultEventMap } from 'rettime'
3
3
  import {
4
- type NetworkSource,
4
+ NetworkSource,
5
5
  type ExtractSourceEvents,
6
6
  } from './sources/network-source'
7
7
  import { type NetworkFrameResolutionContext } from './frames/network-frame'
8
8
  import { type UnhandledFrameHandle } from './on-unhandled-frame'
9
9
  import {
10
- AnyHandler,
11
10
  HandlersController,
12
11
  InMemoryHandlersController,
12
+ type AnyHandler,
13
13
  } from './handlers-controller'
14
14
  import { toReadonlyArray } from '../utils/internal/toReadonlyArray'
15
15
 
@@ -161,6 +161,14 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
161
161
  readyState = NetworkReadyState.ENABLED
162
162
 
163
163
  const result = resolvedOptions.sources.map((source) => {
164
+ /**
165
+ * @note Preemptively disable the network source before enabling.
166
+ * This intentionally calls only the prototype method that clears the
167
+ * event listeners and nothing else. This prevents the "frame" listeners
168
+ * from accumulating across enable/disable in case the source is a singleton.
169
+ */
170
+ NetworkSource.prototype.disable.call(source)
171
+
164
172
  source.on('frame', async ({ frame }) => {
165
173
  frame.events.on('*', (event) => events.emit(event), {
166
174
  signal: listenersController.signal,
@@ -2,7 +2,8 @@ import { http } from '../../http'
2
2
  import { graphql } from '../../graphql'
3
3
  import { ws } from '../../ws'
4
4
  import { bypass } from '../../bypass'
5
- import { HttpNetworkFrame, HttpNetworkFrameEventMap } from './http-frame'
5
+ import type { HttpNetworkFrameEventMap } from './http-frame'
6
+ import { HttpNetworkFrame } from './http-frame'
6
7
  import { InMemoryHandlersController } from '#core/experimental/handlers-controller'
7
8
 
8
9
  beforeAll(() => {
@@ -1,7 +1,10 @@
1
1
  import { TypedEvent } from 'rettime'
2
2
  import { until } from 'until-async'
3
3
  import { createRequestId } from '@mswjs/interceptors'
4
- import { NetworkFrame, NetworkFrameResolutionContext } from './network-frame'
4
+ import {
5
+ NetworkFrame,
6
+ type NetworkFrameResolutionContext,
7
+ } from './network-frame'
5
8
  import { toPublicUrl } from '../../utils/request/toPublicUrl'
6
9
  import { executeHandlers } from '../../utils/executeHandlers'
7
10
  import { storeResponseCookies } from '../../utils/request/storeResponseCookies'
@@ -11,7 +14,8 @@ import {
11
14
  executeUnhandledFrameHandle,
12
15
  type UnhandledFrameHandle,
13
16
  } from '../on-unhandled-frame'
14
- import { HandlersController, AnyHandler } from '../handlers-controller'
17
+ import type { HandlersController } from '../handlers-controller'
18
+ import { type AnyHandler } from '../handlers-controller'
15
19
  import { type RequestHandler } from '../../handlers/RequestHandler'
16
20
 
17
21
  interface HttpNetworkFrameOptions {
@@ -1,10 +1,8 @@
1
1
  import { http } from '../../http'
2
2
  import { graphql } from '../../graphql'
3
3
  import { ws } from '../../ws'
4
- import {
5
- WebSocketNetworkFrame,
6
- WebSocketNetworkFrameEventMap,
7
- } from './websocket-frame'
4
+ import type { WebSocketNetworkFrameEventMap } from './websocket-frame'
5
+ import { WebSocketNetworkFrame } from './websocket-frame'
8
6
  import { createTestWebSocketConnection } from '../../../../test/support/ws-test-utils'
9
7
  import { InMemoryHandlersController } from '#core/experimental/handlers-controller'
10
8
 
@@ -11,10 +11,11 @@ import {
11
11
  } from './network-frame'
12
12
  import {
13
13
  executeUnhandledFrameHandle,
14
- UnhandledFrameHandle,
14
+ type UnhandledFrameHandle,
15
15
  } from '../on-unhandled-frame'
16
16
  import { devUtils } from '../../utils/internal/devUtils'
17
- import { HandlersController, AnyHandler } from '../handlers-controller'
17
+ import type { HandlersController } from '../handlers-controller'
18
+ import { type AnyHandler } from '../handlers-controller'
18
19
 
19
20
  export interface WebSocketNetworkFrameOptions {
20
21
  connection: WebSocketConnectionData
@@ -31,7 +31,7 @@ export abstract class HandlersController {
31
31
  invariant(
32
32
  this.#validateHandlers(initialHandlers),
33
33
  devUtils.formatMessage(
34
- '[MSW] Failed to apply given request handlers: invalid input. Did you forget to spread the request handlers Array?',
34
+ 'Failed to apply given request handlers: invalid input. Did you forget to spread the request handlers Array?',
35
35
  ),
36
36
  )
37
37
 
@@ -1,4 +1,4 @@
1
- export { defineNetwork, DefineNetworkOptions } from './define-network'
1
+ export { defineNetwork, type DefineNetworkOptions } from './define-network'
2
2
 
3
3
  /* Network sources */
4
4
  export { NetworkSource } from './sources/network-source'
@@ -5,10 +5,8 @@ import type {
5
5
  } from '@mswjs/interceptors/WebSocket'
6
6
  import { HttpNetworkFrame } from './frames/http-frame'
7
7
  import { WebSocketNetworkFrame } from './frames/websocket-frame'
8
- import {
9
- executeUnhandledFrameHandle,
10
- UnhandledFrameCallback,
11
- } from './on-unhandled-frame'
8
+ import type { UnhandledFrameCallback } from './on-unhandled-frame'
9
+ import { executeUnhandledFrameHandle } from './on-unhandled-frame'
12
10
 
13
11
  beforeAll(() => {
14
12
  vi.spyOn(console, 'warn').mockImplementation(() => void 0)
@@ -1,9 +1,9 @@
1
1
  import { type DefaultEventMap, Emitter } from 'rettime'
2
- import { LifeCycleEventEmitter } from '../sharedOptions'
2
+ import { type LifeCycleEventEmitter } from '../sharedOptions'
3
+ import type { HandlersController } from './handlers-controller'
3
4
  import {
4
- AnyHandler,
5
- HandlersController,
6
5
  InMemoryHandlersController,
6
+ type AnyHandler,
7
7
  } from './handlers-controller'
8
8
  import { Disposable } from '../utils/internal/Disposable'
9
9
  import { toReadonlyArray } from '../utils/internal/toReadonlyArray'
@@ -1,9 +1,5 @@
1
- import {
2
- BatchInterceptor,
3
- Interceptor,
4
- RequestController,
5
- type HttpRequestEventMap,
6
- } from '@mswjs/interceptors'
1
+ import type { Interceptor, RequestController } from '@mswjs/interceptors'
2
+ import { BatchInterceptor, type HttpRequestEventMap } from '@mswjs/interceptors'
7
3
  import type {
8
4
  WebSocketConnectionData,
9
5
  WebSocketEventMap,