msw 3.0.2 → 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.
- package/README.md +51 -35
- package/lib/_chunks/cookie-store.js +3 -2
- package/lib/_chunks/cookie-store.js.map +1 -1
- package/lib/_chunks/interceptor-source.d.ts +5 -1
- package/lib/_chunks/interceptor-source.js +18 -5
- package/lib/_chunks/interceptor-source.js.map +1 -1
- package/lib/_chunks/request-handler.d.ts +3 -2
- package/lib/browser/index.d.ts +4 -9
- package/lib/browser/index.js +22 -15
- package/lib/browser/index.js.map +1 -1
- package/lib/graphql/index.d.ts +19 -4
- package/lib/graphql/index.js +24 -12
- package/lib/graphql/index.js.map +1 -1
- package/lib/iife/index.js +68 -35
- package/lib/iife/index.js.map +1 -1
- package/lib/mockServiceWorker.js +152 -12
- package/lib/utils/is-common-asset-request.js +1 -1
- package/lib/utils/is-common-asset-request.js.map +1 -1
- package/package.json +2 -3
- package/src/browser/default-network-options.ts +1 -1
- package/src/browser/setup-worker.ts +1 -4
- package/src/browser/sources/fallback-http-source.ts +11 -8
- package/src/browser/sources/service-worker-source.ts +12 -5
- package/src/browser/utils/should-invalidate-worker.test.ts +0 -16
- package/src/core/experimental/define-network.ts +1 -1
- package/src/core/experimental/handlers-controller.test.ts +61 -0
- package/src/core/experimental/handlers-controller.ts +39 -6
- package/src/core/experimental/sources/network-source.ts +8 -1
- package/src/core/handlers/handler.ts +3 -2
- package/src/graphql/graphql-subscription.ts +30 -12
- package/src/mockServiceWorker.js +150 -10
- package/src/utils/is-common-asset-request.test.ts +371 -0
- package/src/utils/is-common-asset-request.ts +1 -1
package/lib/mockServiceWorker.js
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
* - Please do NOT modify this file.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const PACKAGE_VERSION = '3.0.
|
|
11
|
-
const INTEGRITY_CHECKSUM = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
"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",
|
|
@@ -94,7 +94,6 @@
|
|
|
94
94
|
"funding": "https://github.com/sponsors/mswjs",
|
|
95
95
|
"files": [
|
|
96
96
|
"config/constants.js",
|
|
97
|
-
"config/scripts/postinstall.js",
|
|
98
97
|
"cli",
|
|
99
98
|
"lib",
|
|
100
99
|
"src",
|
|
@@ -160,7 +159,7 @@
|
|
|
160
159
|
"json-bigint": "^1.0.0",
|
|
161
160
|
"knip": "^6.34.0",
|
|
162
161
|
"lint-staged": "^17.1.1",
|
|
163
|
-
"msw": "^3.0.
|
|
162
|
+
"msw": "^3.0.4",
|
|
164
163
|
"oxlint": "^1.75.0",
|
|
165
164
|
"page-with": "^0.6.1",
|
|
166
165
|
"prettier": "^3.9.6",
|
|
@@ -17,7 +17,7 @@ export function createDefaultNetworkOptions(
|
|
|
17
17
|
sources: [
|
|
18
18
|
supportsServiceWorker()
|
|
19
19
|
? new ServiceWorkerSource({ serviceWorker: { url: workerUrl } })
|
|
20
|
-
: new FallbackHttpSource(
|
|
20
|
+
: new FallbackHttpSource(),
|
|
21
21
|
new InterceptorSource({
|
|
22
22
|
interceptors: [
|
|
23
23
|
new WebSocketInterceptor() as InterceptorSourceOptions['interceptors'][number],
|
|
@@ -56,11 +56,8 @@ export function setupWorker(...handlers: Array<AnyHandler>): SetupWorker {
|
|
|
56
56
|
options: options?.serviceWorker?.options,
|
|
57
57
|
},
|
|
58
58
|
findWorker: options?.findWorker,
|
|
59
|
-
quiet: options?.quiet,
|
|
60
|
-
})
|
|
61
|
-
: new FallbackHttpSource({
|
|
62
|
-
quiet: options?.quiet,
|
|
63
59
|
})
|
|
60
|
+
: new FallbackHttpSource()
|
|
64
61
|
|
|
65
62
|
network.configure({
|
|
66
63
|
sources: [
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { FetchInterceptor } from '@mswjs/interceptors/fetch'
|
|
2
2
|
import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'
|
|
3
3
|
import { InterceptorSource } from '#core/experimental/sources/interceptor-source'
|
|
4
|
+
import type { NetworkFrameResolutionContext } from '#core/experimental/frames/network-frame'
|
|
4
5
|
import { devUtils } from '#core/utils/internal/dev-utils'
|
|
5
6
|
|
|
6
|
-
interface FallbackHttpSourceOptions {
|
|
7
|
-
quiet?: boolean
|
|
8
|
-
}
|
|
9
|
-
|
|
10
7
|
export class FallbackHttpSource extends InterceptorSource {
|
|
11
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The resolved context of the network this source was last enabled with.
|
|
10
|
+
*/
|
|
11
|
+
#context?: NetworkFrameResolutionContext
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
12
14
|
super({
|
|
13
15
|
interceptors: [new XMLHttpRequestInterceptor(), new FetchInterceptor()],
|
|
14
16
|
})
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
public enable(): void {
|
|
19
|
+
public enable(context?: NetworkFrameResolutionContext): void {
|
|
20
|
+
this.#context = context
|
|
18
21
|
super.enable()
|
|
19
22
|
|
|
20
|
-
if (!this
|
|
23
|
+
if (!this.#context?.quiet) {
|
|
21
24
|
this.#printStartMessage()
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -25,7 +28,7 @@ export class FallbackHttpSource extends InterceptorSource {
|
|
|
25
28
|
public disable(): void {
|
|
26
29
|
super.disable()
|
|
27
30
|
|
|
28
|
-
if (!this
|
|
31
|
+
if (!this.#context?.quiet) {
|
|
29
32
|
this.#printStopMessage()
|
|
30
33
|
}
|
|
31
34
|
}
|
|
@@ -2,6 +2,7 @@ import { invariant } from 'outvariant'
|
|
|
2
2
|
import type { Emitter } from 'rettime'
|
|
3
3
|
import { FetchResponse } from '@mswjs/interceptors'
|
|
4
4
|
import { NetworkSource } from '#core/experimental/sources/network-source'
|
|
5
|
+
import type { NetworkFrameResolutionContext } from '#core/experimental/frames/network-frame'
|
|
5
6
|
import { RequestHandler } from '#core/handlers/request-handler'
|
|
6
7
|
import {
|
|
7
8
|
HttpNetworkFrame,
|
|
@@ -23,7 +24,6 @@ import { validateWorkerScope } from '../utils/validate-worker-scope'
|
|
|
23
24
|
import { shouldInvalidateWorker } from '../utils/should-invalidate-worker'
|
|
24
25
|
|
|
25
26
|
export interface ServiceWorkerSourceOptions {
|
|
26
|
-
quiet?: boolean
|
|
27
27
|
serviceWorker: {
|
|
28
28
|
url: string
|
|
29
29
|
options?: RegistrationOptions
|
|
@@ -75,6 +75,10 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
#options: ServiceWorkerSourceOptions
|
|
78
|
+
/**
|
|
79
|
+
* The resolved context of the network this source was last enabled with.
|
|
80
|
+
*/
|
|
81
|
+
#context?: NetworkFrameResolutionContext
|
|
78
82
|
/**
|
|
79
83
|
* @note We cannot use `WeakMap` here as request/response
|
|
80
84
|
* identity cannot be preserved through the client-worker channel.
|
|
@@ -109,7 +113,10 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
|
|
|
109
113
|
})
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
public async enable(
|
|
116
|
+
public async enable(
|
|
117
|
+
context?: NetworkFrameResolutionContext,
|
|
118
|
+
): Promise<ServiceWorkerRegistration> {
|
|
119
|
+
this.#context = context
|
|
113
120
|
this.#channel.removeAllListeners()
|
|
114
121
|
this.#frames.clear()
|
|
115
122
|
|
|
@@ -147,7 +154,7 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
|
|
|
147
154
|
})
|
|
148
155
|
await clientConfirmationPromise.promise
|
|
149
156
|
|
|
150
|
-
if (!this.#
|
|
157
|
+
if (!this.#context?.quiet) {
|
|
151
158
|
this.#printStartMessage()
|
|
152
159
|
}
|
|
153
160
|
|
|
@@ -178,7 +185,7 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
|
|
|
178
185
|
* `#handleRequest`. `#startWorker` swaps in a fresh deferred on re-enable.
|
|
179
186
|
*/
|
|
180
187
|
|
|
181
|
-
if (!this.#
|
|
188
|
+
if (!this.#context?.quiet) {
|
|
182
189
|
this.#printStopMessage()
|
|
183
190
|
}
|
|
184
191
|
}
|
|
@@ -294,7 +301,7 @@ Please consider using a custom "serviceWorker.url" option to point to the actual
|
|
|
294
301
|
this.#channel.postMessage('KEEPALIVE_REQUEST')
|
|
295
302
|
}, 5000)
|
|
296
303
|
|
|
297
|
-
if (!this.#
|
|
304
|
+
if (!this.#context?.quiet) {
|
|
298
305
|
validateWorkerScope(registration)
|
|
299
306
|
}
|
|
300
307
|
|
|
@@ -104,19 +104,3 @@ test('returns false when findWorker is the same reference', () => {
|
|
|
104
104
|
),
|
|
105
105
|
).toBe(false)
|
|
106
106
|
})
|
|
107
|
-
|
|
108
|
-
test('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
|
-
})
|
|
@@ -308,6 +308,67 @@ describe(InMemoryHandlersController.prototype.reset, () => {
|
|
|
308
308
|
expect(controller.getHandlersByKind('websocket')).toEqual([wsOne, wsTwo])
|
|
309
309
|
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
|
|
310
310
|
})
|
|
311
|
+
|
|
312
|
+
test('resets the kept handlers and disposes of the removed ones', () => {
|
|
313
|
+
const initialHandler = http.get('/initial', () => {})
|
|
314
|
+
initialHandler.reset = vi.fn()
|
|
315
|
+
initialHandler.dispose = vi.fn()
|
|
316
|
+
const runtimeHandler = http.get('/runtime', () => {})
|
|
317
|
+
runtimeHandler.reset = vi.fn()
|
|
318
|
+
runtimeHandler.dispose = vi.fn()
|
|
319
|
+
|
|
320
|
+
const controller = new InMemoryHandlersController([initialHandler])
|
|
321
|
+
controller.use([runtimeHandler])
|
|
322
|
+
controller.reset([])
|
|
323
|
+
|
|
324
|
+
expect.soft(initialHandler.reset).toHaveBeenCalledOnce()
|
|
325
|
+
expect.soft(initialHandler.dispose).not.toHaveBeenCalled()
|
|
326
|
+
expect.soft(runtimeHandler.reset).not.toHaveBeenCalled()
|
|
327
|
+
expect(runtimeHandler.dispose).toHaveBeenCalledOnce()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('reports a rejected disposal of a removed handler', async () => {
|
|
331
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
332
|
+
onTestFinished(() => vi.restoreAllMocks())
|
|
333
|
+
|
|
334
|
+
const runtimeHandler = http.get('/runtime', () => {})
|
|
335
|
+
runtimeHandler.dispose = () =>
|
|
336
|
+
Promise.reject(new Error('Handler disposal error'))
|
|
337
|
+
|
|
338
|
+
const controller = new InMemoryHandlersController([])
|
|
339
|
+
controller.use([runtimeHandler])
|
|
340
|
+
controller.reset([])
|
|
341
|
+
|
|
342
|
+
await expect
|
|
343
|
+
.poll(() => console.error)
|
|
344
|
+
.toHaveBeenCalledWith(
|
|
345
|
+
`[MSW] Failed to dispose of "request" handler removed during reset. Please see the original error below.\nError: Handler disposal error`,
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('removes the remaining handlers when a disposal throws', () => {
|
|
350
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
351
|
+
onTestFinished(() => vi.restoreAllMocks())
|
|
352
|
+
|
|
353
|
+
const throwingHandler = http.get('/throwing', () => {})
|
|
354
|
+
throwingHandler.dispose = () => {
|
|
355
|
+
throw new Error('Handler disposal error')
|
|
356
|
+
}
|
|
357
|
+
const runtimeHandler = http.get('/runtime', () => {})
|
|
358
|
+
runtimeHandler.dispose = vi.fn()
|
|
359
|
+
|
|
360
|
+
const controller = new InMemoryHandlersController([])
|
|
361
|
+
controller.use([throwingHandler, runtimeHandler])
|
|
362
|
+
controller.reset([])
|
|
363
|
+
|
|
364
|
+
expect
|
|
365
|
+
.soft(console.error)
|
|
366
|
+
.toHaveBeenCalledWith(
|
|
367
|
+
`[MSW] Failed to dispose of "request" handler removed during reset. Please see the original error below.\nError: Handler disposal error`,
|
|
368
|
+
)
|
|
369
|
+
expect.soft(runtimeHandler.dispose).toHaveBeenCalledOnce()
|
|
370
|
+
expect(controller.currentHandlers()).toEqual([])
|
|
371
|
+
})
|
|
311
372
|
})
|
|
312
373
|
|
|
313
374
|
describe(InMemoryHandlersController.prototype.listHandlers, () => {
|
|
@@ -168,11 +168,38 @@ export abstract class HandlersController {
|
|
|
168
168
|
),
|
|
169
169
|
)
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
const { initialHandlers } = this.getState()
|
|
172
|
+
const nextInitialHandlers =
|
|
173
|
+
nextHandlers.length > 0
|
|
174
|
+
? groupHandlersByKind(nextHandlers)
|
|
175
|
+
: initialHandlers
|
|
176
|
+
const currentHandlers = new Set(this.currentHandlers())
|
|
177
|
+
const nextHandlersSet = new Set(
|
|
178
|
+
Object.values(nextInitialHandlers).flat().filter(Boolean),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
for (const handler of currentHandlers.intersection(nextHandlersSet)) {
|
|
172
182
|
handler.reset()
|
|
173
183
|
}
|
|
174
184
|
|
|
175
|
-
|
|
185
|
+
// The handlers that leave the network with this reset release
|
|
186
|
+
// whatever they hold (e.g. live connection bindings).
|
|
187
|
+
// Resetting is synchronous, so their disposal is not awaited.
|
|
188
|
+
for (const handler of currentHandlers.difference(nextHandlersSet)) {
|
|
189
|
+
try {
|
|
190
|
+
const disposal = handler.dispose()
|
|
191
|
+
|
|
192
|
+
if (disposal instanceof Promise) {
|
|
193
|
+
disposal.catch((error) => {
|
|
194
|
+
this.#printDisposalError(handler, error)
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
// A failing disposal must not prevent the remaining handlers
|
|
199
|
+
// from being disposed of, nor the removed handlers from being cleared.
|
|
200
|
+
this.#printDisposalError(handler, error)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
176
203
|
|
|
177
204
|
if (nextHandlers.length === 0) {
|
|
178
205
|
this.setState({
|
|
@@ -182,11 +209,9 @@ export abstract class HandlersController {
|
|
|
182
209
|
return
|
|
183
210
|
}
|
|
184
211
|
|
|
185
|
-
const normalizedNextHandlers = groupHandlersByKind(nextHandlers)
|
|
186
|
-
|
|
187
212
|
this.setState({
|
|
188
|
-
initialHandlers:
|
|
189
|
-
handlers: { ...
|
|
213
|
+
initialHandlers: nextInitialHandlers,
|
|
214
|
+
handlers: { ...nextInitialHandlers },
|
|
190
215
|
})
|
|
191
216
|
}
|
|
192
217
|
|
|
@@ -217,6 +242,14 @@ export abstract class HandlersController {
|
|
|
217
242
|
#validateHandlers(handlers: Array<AnyHandler>): boolean {
|
|
218
243
|
return handlers.every((handler) => !Array.isArray(handler))
|
|
219
244
|
}
|
|
245
|
+
|
|
246
|
+
#printDisposalError(handler: AnyHandler, error: unknown): void {
|
|
247
|
+
devUtils.error(
|
|
248
|
+
'Failed to dispose of "%s" handler removed during reset. Please see the original error below.\n%s',
|
|
249
|
+
handler.kind,
|
|
250
|
+
error,
|
|
251
|
+
)
|
|
252
|
+
}
|
|
220
253
|
}
|
|
221
254
|
|
|
222
255
|
export class InMemoryHandlersController extends HandlersController {
|
|
@@ -2,6 +2,7 @@ import { Emitter, TypedEvent, type TypedListenerOptions } from 'rettime'
|
|
|
2
2
|
import type {
|
|
3
3
|
AnyNetworkFrame,
|
|
4
4
|
ExtractFrameEvents,
|
|
5
|
+
NetworkFrameResolutionContext,
|
|
5
6
|
} from '../frames/network-frame'
|
|
6
7
|
|
|
7
8
|
class NetworkFrameEvent<
|
|
@@ -33,7 +34,13 @@ export abstract class NetworkSource<
|
|
|
33
34
|
this.emitter = new Emitter()
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Enable this network source.
|
|
39
|
+
* @param context The resolved context of the network this source belongs to.
|
|
40
|
+
*/
|
|
41
|
+
public abstract enable(
|
|
42
|
+
context?: NetworkFrameResolutionContext,
|
|
43
|
+
): unknown | Promise<unknown>
|
|
37
44
|
|
|
38
45
|
public async queue(frame: Frame): Promise<void> {
|
|
39
46
|
await this.emitter.emitAsPromise(
|
|
@@ -32,8 +32,9 @@ export abstract class Handler {
|
|
|
32
32
|
/**
|
|
33
33
|
* Release the resources held by this handler.
|
|
34
34
|
*
|
|
35
|
-
* @note This method is invoked automatically when the
|
|
36
|
-
* disabled (e.g. `server.close()`)
|
|
35
|
+
* @note This method is invoked automatically when the handler leaves
|
|
36
|
+
* the network: when the network is disabled (e.g. `server.close()`)
|
|
37
|
+
* or when a reset removes it (e.g. `server.resetHandlers()`). Override it in the handlers that
|
|
37
38
|
* hold onto anything beyond a single frame, like timers, connections,
|
|
38
39
|
* or event listeners. Returning a promise makes the network await
|
|
39
40
|
* this handler's disposal before it tears itself down.
|