msw 3.0.3 → 3.0.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.
@@ -7,9 +7,13 @@
7
7
  * - Please do NOT modify this file.
8
8
  */
9
9
 
10
- const PACKAGE_VERSION = '3.0.3'
11
- const INTEGRITY_CHECKSUM = '5cd5cf8b54c3a90f82960cedcd637772'
10
+ const PACKAGE_VERSION = '3.0.4'
11
+ const INTEGRITY_CHECKSUM = '0bd5af64c8dc112df5e0c5e9c8a2b15a'
12
12
  const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
13
+ // Cache Storage is shared by the whole origin. Scope the cache name
14
+ // to this registration so multiple workers do not overwrite each other.
15
+ const ACTIVE_CLIENTS_CACHE_NAME = `msw:active-clients:${self.registration.scope}`
16
+ const ACTIVE_CLIENTS_CACHE_KEY = '/__msw__/active-clients'
13
17
 
14
18
  const activeClientIds = new Set()
15
19
  /**
@@ -17,6 +21,23 @@ const activeClientIds = new Set()
17
21
  */
18
22
  const pendingRequests = new Map()
19
23
 
24
+ /**
25
+ * The browser may terminate this worker at any point (idle timeout,
26
+ * hidden page throttling, event timeout) and evaluate it again on the
27
+ * next event, losing all in-memory state. Restore the active client IDs
28
+ * from the storage so the worker keeps handling requests after that.
29
+ */
30
+ let hasRestoredActiveClientIds = false
31
+ let isStoredActiveClientIdsOutdated = false
32
+ let activeClientIdsStorePromise = Promise.resolve()
33
+ const activeClientIdsRestorePromise = restoreActiveClientIds()
34
+ .catch((error) => {
35
+ console.error('[MSW] Failed to restore the active clients:', error)
36
+ })
37
+ .finally(() => {
38
+ hasRestoredActiveClientIds = true
39
+ })
40
+
20
41
  addEventListener('install', function () {
21
42
  self.skipWaiting()
22
43
  })
@@ -34,12 +55,17 @@ addEventListener('message', function (event) {
34
55
 
35
56
  event.waitUntil(
36
57
  (async () => {
58
+ // Never mutate the active clients before they are restored,
59
+ // otherwise storing them would persist an incomplete set.
60
+ await activeClientIdsRestorePromise
61
+
37
62
  if (event.data === 'CLIENT_CLOSE') {
38
63
  const allClients = await self.clients.matchAll({
39
64
  type: 'window',
40
65
  })
41
66
 
42
67
  activeClientIds.delete(clientId)
68
+ await storeActiveClientIds()
43
69
 
44
70
  // Await any pending requests from the closing client.
45
71
  // This makes sure that those requests are handled and not passthrough.
@@ -55,13 +81,14 @@ addEventListener('message', function (event) {
55
81
 
56
82
  // Unregister itself when there are no more clients
57
83
  if (remainingClients.length === 0) {
84
+ await clearStoredActiveClientIds()
58
85
  await self.registration.unregister()
59
86
  }
60
87
 
61
88
  const client = await self.clients.get(clientId)
62
89
 
63
90
  if (client != null) {
64
- await sendToClient(client, {
91
+ notifyClient(client, {
65
92
  type: 'CLIENT_CLOSED',
66
93
  })
67
94
  }
@@ -83,14 +110,20 @@ addEventListener('message', function (event) {
83
110
 
84
111
  switch (event.data) {
85
112
  case 'KEEPALIVE_REQUEST': {
86
- await sendToClient(client, {
113
+ // Retry a failed store on the next keepalive so the storage
114
+ // catches up with the in-memory state before the worker restarts.
115
+ if (isStoredActiveClientIdsOutdated) {
116
+ await storeActiveClientIds()
117
+ }
118
+
119
+ notifyClient(client, {
87
120
  type: 'KEEPALIVE_RESPONSE',
88
121
  })
89
122
  break
90
123
  }
91
124
 
92
125
  case 'INTEGRITY_CHECK_REQUEST': {
93
- await sendToClient(client, {
126
+ notifyClient(client, {
94
127
  type: 'INTEGRITY_CHECK_RESPONSE',
95
128
  payload: {
96
129
  packageVersion: PACKAGE_VERSION,
@@ -102,8 +135,9 @@ addEventListener('message', function (event) {
102
135
 
103
136
  case 'MOCK_ACTIVATE': {
104
137
  activeClientIds.add(clientId)
138
+ await storeActiveClientIds()
105
139
 
106
- await sendToClient(client, {
140
+ notifyClient(client, {
107
141
  type: 'MOCKING_ENABLED',
108
142
  payload: {
109
143
  client: {
@@ -129,6 +163,22 @@ addEventListener('fetch', function (event) {
129
163
  return
130
164
  }
131
165
 
166
+ // This request may be the one waking the worker up after the browser
167
+ // has terminated it. Wait for the active clients to be restored
168
+ // before deciding whether to handle the request.
169
+ if (!hasRestoredActiveClientIds) {
170
+ event.respondWith(
171
+ activeClientIdsRestorePromise.then(() => {
172
+ if (activeClientIds.size === 0) {
173
+ return fetch(event.request)
174
+ }
175
+
176
+ return handleRequest(event)
177
+ }),
178
+ )
179
+ return
180
+ }
181
+
132
182
  // Bypass all requests when there are no active clients.
133
183
  // Prevents the self-unregistered worked from handling requests
134
184
  // after it's been terminated (still remains active until the next reload).
@@ -136,15 +186,85 @@ addEventListener('fetch', function (event) {
136
186
  return
137
187
  }
138
188
 
139
- const requestId = crypto.randomUUID()
140
- event.respondWith(handleRequest(event, requestId))
189
+ event.respondWith(handleRequest(event))
141
190
  })
142
191
 
192
+ /**
193
+ * Populate the active client IDs from the storage,
194
+ * ignoring the clients that no longer exist.
195
+ */
196
+ async function restoreActiveClientIds() {
197
+ const storedClientIds = await readStoredActiveClientIds()
198
+
199
+ if (storedClientIds.length === 0) {
200
+ return
201
+ }
202
+
203
+ const windowClients = await self.clients.matchAll({
204
+ type: 'window',
205
+ })
206
+ const windowClientIds = new Set(windowClients.map((client) => client.id))
207
+
208
+ for (const clientId of storedClientIds) {
209
+ if (windowClientIds.has(clientId)) {
210
+ activeClientIds.add(clientId)
211
+ }
212
+ }
213
+ }
214
+
215
+ /**
216
+ * @returns {Promise<Array<string>>}
217
+ */
218
+ async function readStoredActiveClientIds() {
219
+ const cache = await caches.open(ACTIVE_CLIENTS_CACHE_NAME)
220
+ const response = await cache.match(ACTIVE_CLIENTS_CACHE_KEY)
221
+
222
+ if (!response) {
223
+ return []
224
+ }
225
+
226
+ return response.json()
227
+ }
228
+
229
+ /**
230
+ * Storing is best-effort: a failure must never prevent the client
231
+ * from being activated or closed, in-memory state is still correct.
232
+ * Stores are serialized so each write persists the latest state
233
+ * and an earlier success cannot clear the flag of a later failure.
234
+ */
235
+ function storeActiveClientIds() {
236
+ activeClientIdsStorePromise =
237
+ activeClientIdsStorePromise.then(writeActiveClientIds)
238
+ return activeClientIdsStorePromise
239
+ }
240
+
241
+ async function writeActiveClientIds() {
242
+ try {
243
+ const cache = await caches.open(ACTIVE_CLIENTS_CACHE_NAME)
244
+ await cache.put(
245
+ ACTIVE_CLIENTS_CACHE_KEY,
246
+ new Response(JSON.stringify(Array.from(activeClientIds))),
247
+ )
248
+ isStoredActiveClientIdsOutdated = false
249
+ } catch (error) {
250
+ isStoredActiveClientIdsOutdated = true
251
+ console.error('[MSW] Failed to store the active clients:', error)
252
+ }
253
+ }
254
+
255
+ async function clearStoredActiveClientIds() {
256
+ try {
257
+ await caches.delete(ACTIVE_CLIENTS_CACHE_NAME)
258
+ } catch (error) {
259
+ console.error('[MSW] Failed to clear the stored active clients:', error)
260
+ }
261
+ }
262
+
143
263
  /**
144
264
  * @param {FetchEvent} event
145
- * @param {string} requestId
146
265
  */
147
- async function handleRequest(event, requestId) {
266
+ async function handleRequest(event) {
267
+ const requestId = crypto.randomUUID()
148
268
  const client = await resolveMainClient(event)
149
269
  const requestCloneForEvents = event.request.clone()
150
270
 
@@ -172,7 +292,7 @@ async function handleRequest(event, requestId) {
172
292
  // request failed with a network error). Notify the client so it
173
293
  // can release the resources associated with this request.
174
294
  if (client && activeClientIds.has(client.id)) {
175
- sendToClient(client, {
295
+ notifyClient(client, {
176
296
  type: 'REQUEST_ERROR',
177
297
  payload: {
178
298
  request: {
@@ -205,7 +325,7 @@ async function handleRequest(event, requestId) {
205
325
  // Clone the response so both the client and the library could consume it.
206
326
  const responseClone = isEventStreamResponse ? null : response.clone()
207
327
 
208
- sendToClient(
328
+ notifyClient(
209
329
  client,
210
330
  {
211
331
  type: 'RESPONSE',
@@ -402,6 +522,26 @@ function sendToClient(client, message, transferrables = []) {
402
522
  })
403
523
  }
404
524
 
525
+ /**
526
+ * Post a one-way message to the client.
527
+ * Unlike `sendToClient`, this does not open a MessageChannel or wait
528
+ * for a reply. The client never acknowledges these messages, so awaiting
529
+ * them keeps the extended event pending until the browser terminates
530
+ * the worker on its event timeout (5 minutes in Chromium), wiping its state.
531
+ * @param {Client} client
532
+ * @param {any} message
533
+ * @param {Array<Transferable>} transferrables
534
+ */
535
+ function notifyClient(client, message, transferrables = []) {
536
+ try {
537
+ client.postMessage(message, transferrables.filter(Boolean))
538
+ } catch (error) {
539
+ // A one-way message must never fail its caller (e.g. a transferable
540
+ // that has already been detached must not reject a resolved request).
541
+ console.error('[MSW] Failed to notify the client:', error)
542
+ }
543
+ }
544
+
405
545
  /**
406
546
  * @param {Response} response
407
547
  * @param {FetchEvent} event
@@ -23,7 +23,7 @@ function isCommonAssetRequest(request) {
23
23
  if (/(fonts\.googleapis\.com)/.test(url.hostname)) return true;
24
24
  if (/node_modules/.test(url.pathname)) return true;
25
25
  if (url.pathname.includes("@vite")) return true;
26
- return /\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff|woff2|eot|gif|jpe?g|png|avif|webp|svg|mp4|webm|ogg|mov|mp3|wav|ogg|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test(url.pathname);
26
+ return /\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff|woff2|eot|gif|jpe?g|png|avif|webp|ico|svg|mp4|webm|ogg|mov|mp3|wav|ogg|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test(url.pathname);
27
27
  }
28
28
  //#endregion
29
29
  export { isCommonAssetRequest };
@@ -1 +1 @@
1
- {"version":3,"file":"is-common-asset-request.js","names":[],"sources":["../../src/utils/is-common-asset-request.ts"],"sourcesContent":["/**\n * Determines if the given request is a static asset request.\n * Useful when deciding which unhandled requests to ignore.\n * @note Despite being ignored, you can still intercept and mock\n * static assets by creating request handlers for them.\n *\n * @example\n * import { isCommonAssetRequest } from 'msw'\n * import { HttpNetworkFrame } from 'msw/experimental'\n *\n * await worker.start({\n * onUnhandledFrame({ frame, defaults }) {\n * if (frame instanceof HttpNetworkFrame && !isCommonAssetRequest(frame.data.request)) {\n * defaults.warn()\n * }\n * }\n * })\n */\nexport function isCommonAssetRequest(request: Request): boolean {\n const url = new URL(request.url)\n\n // Ignore certain protocols.\n if (url.protocol === 'file:') {\n return true\n }\n\n // Ignore static assets hosts.\n if (/(fonts\\.googleapis\\.com)/.test(url.hostname)) {\n return true\n }\n\n // Ignore node modules served over HTTP.\n if (/node_modules/.test(url.pathname)) {\n return true\n }\n\n // Ignore internal Vite requests, like \"/@vite/client\".\n if (url.pathname.includes('@vite')) {\n return true\n }\n\n // Ignore common static assets.\n return /\\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff|woff2|eot|gif|jpe?g|png|avif|webp|svg|mp4|webm|ogg|mov|mp3|wav|ogg|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test(\n url.pathname,\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,SAAgB,qBAAqB,SAA2B;CAC9D,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;CAG/B,IAAI,IAAI,aAAa,SACnB,OAAO;CAIT,IAAI,2BAA2B,KAAK,IAAI,QAAQ,GAC9C,OAAO;CAIT,IAAI,eAAe,KAAK,IAAI,QAAQ,GAClC,OAAO;CAIT,IAAI,IAAI,SAAS,SAAS,OAAO,GAC/B,OAAO;CAIT,OAAO,yKAAyK,KAC9K,IAAI,QACN;AACF"}
1
+ {"version":3,"file":"is-common-asset-request.js","names":[],"sources":["../../src/utils/is-common-asset-request.ts"],"sourcesContent":["/**\n * Determines if the given request is a static asset request.\n * Useful when deciding which unhandled requests to ignore.\n * @note Despite being ignored, you can still intercept and mock\n * static assets by creating request handlers for them.\n *\n * @example\n * import { isCommonAssetRequest } from 'msw'\n * import { HttpNetworkFrame } from 'msw/experimental'\n *\n * await worker.start({\n * onUnhandledFrame({ frame, defaults }) {\n * if (frame instanceof HttpNetworkFrame && !isCommonAssetRequest(frame.data.request)) {\n * defaults.warn()\n * }\n * }\n * })\n */\nexport function isCommonAssetRequest(request: Request): boolean {\n const url = new URL(request.url)\n\n // Ignore certain protocols.\n if (url.protocol === 'file:') {\n return true\n }\n\n // Ignore static assets hosts.\n if (/(fonts\\.googleapis\\.com)/.test(url.hostname)) {\n return true\n }\n\n // Ignore node modules served over HTTP.\n if (/node_modules/.test(url.pathname)) {\n return true\n }\n\n // Ignore internal Vite requests, like \"/@vite/client\".\n if (url.pathname.includes('@vite')) {\n return true\n }\n\n // Ignore common static assets.\n return /\\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff|woff2|eot|gif|jpe?g|png|avif|webp|ico|svg|mp4|webm|ogg|mov|mp3|wav|ogg|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test(\n url.pathname,\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,SAAgB,qBAAqB,SAA2B;CAC9D,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;CAG/B,IAAI,IAAI,aAAa,SACnB,OAAO;CAIT,IAAI,2BAA2B,KAAK,IAAI,QAAQ,GAC9C,OAAO;CAIT,IAAI,eAAe,KAAK,IAAI,QAAQ,GAClC,OAAO;CAIT,IAAI,IAAI,SAAS,SAAS,OAAO,GAC/B,OAAO;CAIT,OAAO,6KAA6K,KAClL,IAAI,QACN;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msw",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "The industry standard for API mocking in JavaScript.",
5
5
  "type": "module",
6
6
  "main": "./lib/core/index.js",
@@ -159,7 +159,7 @@
159
159
  "json-bigint": "^1.0.0",
160
160
  "knip": "^6.34.0",
161
161
  "lint-staged": "^17.1.1",
162
- "msw": "^3.0.3",
162
+ "msw": "^3.0.4",
163
163
  "oxlint": "^1.75.0",
164
164
  "page-with": "^0.6.1",
165
165
  "prettier": "^3.9.6",
@@ -10,6 +10,10 @@
10
10
  const PACKAGE_VERSION = '<PACKAGE_VERSION>'
11
11
  const INTEGRITY_CHECKSUM = '<INTEGRITY_CHECKSUM>'
12
12
  const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
13
+ // Cache Storage is shared by the whole origin. Scope the cache name
14
+ // to this registration so multiple workers do not overwrite each other.
15
+ const ACTIVE_CLIENTS_CACHE_NAME = `msw:active-clients:${self.registration.scope}`
16
+ const ACTIVE_CLIENTS_CACHE_KEY = '/__msw__/active-clients'
13
17
 
14
18
  const activeClientIds = new Set()
15
19
  /**
@@ -17,6 +21,23 @@ const activeClientIds = new Set()
17
21
  */
18
22
  const pendingRequests = new Map()
19
23
 
24
+ /**
25
+ * The browser may terminate this worker at any point (idle timeout,
26
+ * hidden page throttling, event timeout) and evaluate it again on the
27
+ * next event, losing all in-memory state. Restore the active client IDs
28
+ * from the storage so the worker keeps handling requests after that.
29
+ */
30
+ let hasRestoredActiveClientIds = false
31
+ let isStoredActiveClientIdsOutdated = false
32
+ let activeClientIdsStorePromise = Promise.resolve()
33
+ const activeClientIdsRestorePromise = restoreActiveClientIds()
34
+ .catch((error) => {
35
+ console.error('[MSW] Failed to restore the active clients:', error)
36
+ })
37
+ .finally(() => {
38
+ hasRestoredActiveClientIds = true
39
+ })
40
+
20
41
  addEventListener('install', function () {
21
42
  self.skipWaiting()
22
43
  })
@@ -34,12 +55,17 @@ addEventListener('message', function (event) {
34
55
 
35
56
  event.waitUntil(
36
57
  (async () => {
58
+ // Never mutate the active clients before they are restored,
59
+ // otherwise storing them would persist an incomplete set.
60
+ await activeClientIdsRestorePromise
61
+
37
62
  if (event.data === 'CLIENT_CLOSE') {
38
63
  const allClients = await self.clients.matchAll({
39
64
  type: 'window',
40
65
  })
41
66
 
42
67
  activeClientIds.delete(clientId)
68
+ await storeActiveClientIds()
43
69
 
44
70
  // Await any pending requests from the closing client.
45
71
  // This makes sure that those requests are handled and not passthrough.
@@ -55,13 +81,14 @@ addEventListener('message', function (event) {
55
81
 
56
82
  // Unregister itself when there are no more clients
57
83
  if (remainingClients.length === 0) {
84
+ await clearStoredActiveClientIds()
58
85
  await self.registration.unregister()
59
86
  }
60
87
 
61
88
  const client = await self.clients.get(clientId)
62
89
 
63
90
  if (client != null) {
64
- await sendToClient(client, {
91
+ notifyClient(client, {
65
92
  type: 'CLIENT_CLOSED',
66
93
  })
67
94
  }
@@ -83,14 +110,20 @@ addEventListener('message', function (event) {
83
110
 
84
111
  switch (event.data) {
85
112
  case 'KEEPALIVE_REQUEST': {
86
- await sendToClient(client, {
113
+ // Retry a failed store on the next keepalive so the storage
114
+ // catches up with the in-memory state before the worker restarts.
115
+ if (isStoredActiveClientIdsOutdated) {
116
+ await storeActiveClientIds()
117
+ }
118
+
119
+ notifyClient(client, {
87
120
  type: 'KEEPALIVE_RESPONSE',
88
121
  })
89
122
  break
90
123
  }
91
124
 
92
125
  case 'INTEGRITY_CHECK_REQUEST': {
93
- await sendToClient(client, {
126
+ notifyClient(client, {
94
127
  type: 'INTEGRITY_CHECK_RESPONSE',
95
128
  payload: {
96
129
  packageVersion: PACKAGE_VERSION,
@@ -102,8 +135,9 @@ addEventListener('message', function (event) {
102
135
 
103
136
  case 'MOCK_ACTIVATE': {
104
137
  activeClientIds.add(clientId)
138
+ await storeActiveClientIds()
105
139
 
106
- await sendToClient(client, {
140
+ notifyClient(client, {
107
141
  type: 'MOCKING_ENABLED',
108
142
  payload: {
109
143
  client: {
@@ -129,6 +163,22 @@ addEventListener('fetch', function (event) {
129
163
  return
130
164
  }
131
165
 
166
+ // This request may be the one waking the worker up after the browser
167
+ // has terminated it. Wait for the active clients to be restored
168
+ // before deciding whether to handle the request.
169
+ if (!hasRestoredActiveClientIds) {
170
+ event.respondWith(
171
+ activeClientIdsRestorePromise.then(() => {
172
+ if (activeClientIds.size === 0) {
173
+ return fetch(event.request)
174
+ }
175
+
176
+ return handleRequest(event)
177
+ }),
178
+ )
179
+ return
180
+ }
181
+
132
182
  // Bypass all requests when there are no active clients.
133
183
  // Prevents the self-unregistered worked from handling requests
134
184
  // after it's been terminated (still remains active until the next reload).
@@ -136,15 +186,85 @@ addEventListener('fetch', function (event) {
136
186
  return
137
187
  }
138
188
 
139
- const requestId = crypto.randomUUID()
140
- event.respondWith(handleRequest(event, requestId))
189
+ event.respondWith(handleRequest(event))
141
190
  })
142
191
 
192
+ /**
193
+ * Populate the active client IDs from the storage,
194
+ * ignoring the clients that no longer exist.
195
+ */
196
+ async function restoreActiveClientIds() {
197
+ const storedClientIds = await readStoredActiveClientIds()
198
+
199
+ if (storedClientIds.length === 0) {
200
+ return
201
+ }
202
+
203
+ const windowClients = await self.clients.matchAll({
204
+ type: 'window',
205
+ })
206
+ const windowClientIds = new Set(windowClients.map((client) => client.id))
207
+
208
+ for (const clientId of storedClientIds) {
209
+ if (windowClientIds.has(clientId)) {
210
+ activeClientIds.add(clientId)
211
+ }
212
+ }
213
+ }
214
+
215
+ /**
216
+ * @returns {Promise<Array<string>>}
217
+ */
218
+ async function readStoredActiveClientIds() {
219
+ const cache = await caches.open(ACTIVE_CLIENTS_CACHE_NAME)
220
+ const response = await cache.match(ACTIVE_CLIENTS_CACHE_KEY)
221
+
222
+ if (!response) {
223
+ return []
224
+ }
225
+
226
+ return response.json()
227
+ }
228
+
229
+ /**
230
+ * Storing is best-effort: a failure must never prevent the client
231
+ * from being activated or closed, in-memory state is still correct.
232
+ * Stores are serialized so each write persists the latest state
233
+ * and an earlier success cannot clear the flag of a later failure.
234
+ */
235
+ function storeActiveClientIds() {
236
+ activeClientIdsStorePromise =
237
+ activeClientIdsStorePromise.then(writeActiveClientIds)
238
+ return activeClientIdsStorePromise
239
+ }
240
+
241
+ async function writeActiveClientIds() {
242
+ try {
243
+ const cache = await caches.open(ACTIVE_CLIENTS_CACHE_NAME)
244
+ await cache.put(
245
+ ACTIVE_CLIENTS_CACHE_KEY,
246
+ new Response(JSON.stringify(Array.from(activeClientIds))),
247
+ )
248
+ isStoredActiveClientIdsOutdated = false
249
+ } catch (error) {
250
+ isStoredActiveClientIdsOutdated = true
251
+ console.error('[MSW] Failed to store the active clients:', error)
252
+ }
253
+ }
254
+
255
+ async function clearStoredActiveClientIds() {
256
+ try {
257
+ await caches.delete(ACTIVE_CLIENTS_CACHE_NAME)
258
+ } catch (error) {
259
+ console.error('[MSW] Failed to clear the stored active clients:', error)
260
+ }
261
+ }
262
+
143
263
  /**
144
264
  * @param {FetchEvent} event
145
- * @param {string} requestId
146
265
  */
147
- async function handleRequest(event, requestId) {
266
+ async function handleRequest(event) {
267
+ const requestId = crypto.randomUUID()
148
268
  const client = await resolveMainClient(event)
149
269
  const requestCloneForEvents = event.request.clone()
150
270
 
@@ -172,7 +292,7 @@ async function handleRequest(event, requestId) {
172
292
  // request failed with a network error). Notify the client so it
173
293
  // can release the resources associated with this request.
174
294
  if (client && activeClientIds.has(client.id)) {
175
- sendToClient(client, {
295
+ notifyClient(client, {
176
296
  type: 'REQUEST_ERROR',
177
297
  payload: {
178
298
  request: {
@@ -205,7 +325,7 @@ async function handleRequest(event, requestId) {
205
325
  // Clone the response so both the client and the library could consume it.
206
326
  const responseClone = isEventStreamResponse ? null : response.clone()
207
327
 
208
- sendToClient(
328
+ notifyClient(
209
329
  client,
210
330
  {
211
331
  type: 'RESPONSE',
@@ -402,6 +522,26 @@ function sendToClient(client, message, transferrables = []) {
402
522
  })
403
523
  }
404
524
 
525
+ /**
526
+ * Post a one-way message to the client.
527
+ * Unlike `sendToClient`, this does not open a MessageChannel or wait
528
+ * for a reply. The client never acknowledges these messages, so awaiting
529
+ * them keeps the extended event pending until the browser terminates
530
+ * the worker on its event timeout (5 minutes in Chromium), wiping its state.
531
+ * @param {Client} client
532
+ * @param {any} message
533
+ * @param {Array<Transferable>} transferrables
534
+ */
535
+ function notifyClient(client, message, transferrables = []) {
536
+ try {
537
+ client.postMessage(message, transferrables.filter(Boolean))
538
+ } catch (error) {
539
+ // A one-way message must never fail its caller (e.g. a transferable
540
+ // that has already been detached must not reject a resolved request).
541
+ console.error('[MSW] Failed to notify the client:', error)
542
+ }
543
+ }
544
+
405
545
  /**
406
546
  * @param {Response} response
407
547
  * @param {FetchEvent} event