msw 2.14.6 → 2.15.0
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 +5 -5
- package/lib/browser/index.js +233 -93
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/index.mjs +233 -93
- package/lib/browser/index.mjs.map +1 -1
- package/lib/core/{HttpResponse-CQwYpuKo.d.mts → HttpResponse-BE_Y2Ak9.d.mts} +19 -1
- package/lib/core/{HttpResponse-BFS34nkx.d.ts → HttpResponse-DL-P1EeG.d.ts} +19 -1
- package/lib/core/HttpResponse.d.mts +1 -1
- package/lib/core/HttpResponse.d.ts +1 -1
- package/lib/core/HttpResponse.js +2 -2
- package/lib/core/HttpResponse.js.map +1 -1
- package/lib/core/HttpResponse.mjs +2 -2
- package/lib/core/HttpResponse.mjs.map +1 -1
- package/lib/core/experimental/compat.d.mts +2 -2
- package/lib/core/experimental/compat.d.ts +2 -2
- package/lib/core/experimental/define-network.d.mts +2 -2
- package/lib/core/experimental/define-network.d.ts +2 -2
- package/lib/core/experimental/frames/http-frame.d.mts +2 -2
- package/lib/core/experimental/frames/http-frame.d.ts +2 -2
- package/lib/core/experimental/frames/network-frame.d.mts +2 -2
- package/lib/core/experimental/frames/network-frame.d.ts +2 -2
- package/lib/core/experimental/frames/network-frame.js +2 -0
- package/lib/core/experimental/frames/network-frame.js.map +1 -1
- package/lib/core/experimental/frames/network-frame.mjs +2 -0
- package/lib/core/experimental/frames/network-frame.mjs.map +1 -1
- package/lib/core/experimental/frames/websocket-frame.d.mts +2 -2
- package/lib/core/experimental/frames/websocket-frame.d.ts +2 -2
- package/lib/core/experimental/handlers-controller.d.mts +1 -1
- package/lib/core/experimental/handlers-controller.d.ts +1 -1
- package/lib/core/experimental/index.d.mts +2 -2
- package/lib/core/experimental/index.d.ts +2 -2
- package/lib/core/experimental/on-unhandled-frame.d.mts +2 -2
- package/lib/core/experimental/on-unhandled-frame.d.ts +2 -2
- package/lib/core/experimental/setup-api.d.mts +1 -1
- package/lib/core/experimental/setup-api.d.ts +1 -1
- package/lib/core/experimental/sources/interceptor-source.d.mts +2 -2
- package/lib/core/experimental/sources/interceptor-source.d.ts +2 -2
- package/lib/core/experimental/sources/network-source.d.mts +2 -2
- package/lib/core/experimental/sources/network-source.d.ts +2 -2
- package/lib/core/getResponse.d.mts +1 -1
- package/lib/core/getResponse.d.ts +1 -1
- package/lib/core/graphql.d.mts +1 -1
- package/lib/core/graphql.d.ts +1 -1
- package/lib/core/handlers/GraphQLHandler.d.mts +1 -1
- package/lib/core/handlers/GraphQLHandler.d.ts +1 -1
- package/lib/core/handlers/HttpHandler.d.mts +1 -1
- package/lib/core/handlers/HttpHandler.d.ts +1 -1
- package/lib/core/handlers/RequestHandler.d.mts +1 -1
- package/lib/core/handlers/RequestHandler.d.ts +1 -1
- package/lib/core/handlers/RequestHandler.js +88 -23
- package/lib/core/handlers/RequestHandler.js.map +1 -1
- package/lib/core/handlers/RequestHandler.mjs +88 -23
- package/lib/core/handlers/RequestHandler.mjs.map +1 -1
- package/lib/core/handlers/WebSocketHandler.js +7 -6
- package/lib/core/handlers/WebSocketHandler.js.map +1 -1
- package/lib/core/handlers/WebSocketHandler.mjs +7 -6
- package/lib/core/handlers/WebSocketHandler.mjs.map +1 -1
- package/lib/core/http.d.mts +1 -1
- package/lib/core/http.d.ts +1 -1
- package/lib/core/index.d.mts +1 -1
- package/lib/core/index.d.ts +1 -1
- package/lib/core/{on-unhandled-frame-BBR-P3kV.d.ts → on-unhandled-frame-0KmwUmeV.d.ts} +1 -1
- package/lib/core/{on-unhandled-frame-Cr1KOZ0I.d.mts → on-unhandled-frame-gkokW4Rr.d.mts} +1 -1
- package/lib/core/passthrough.d.mts +1 -1
- package/lib/core/passthrough.d.ts +1 -1
- package/lib/core/sse.d.mts +2 -2
- package/lib/core/sse.d.ts +2 -2
- package/lib/core/sse.js +83 -60
- package/lib/core/sse.js.map +1 -1
- package/lib/core/sse.mjs +83 -60
- package/lib/core/sse.mjs.map +1 -1
- package/lib/core/utils/HttpResponse/decorators.d.mts +8 -3
- package/lib/core/utils/HttpResponse/decorators.d.ts +8 -3
- package/lib/core/utils/HttpResponse/decorators.js +14 -1
- package/lib/core/utils/HttpResponse/decorators.js.map +1 -1
- package/lib/core/utils/HttpResponse/decorators.mjs +14 -1
- package/lib/core/utils/HttpResponse/decorators.mjs.map +1 -1
- package/lib/core/utils/executeHandlers.d.mts +1 -1
- package/lib/core/utils/executeHandlers.d.ts +1 -1
- package/lib/core/utils/handleRequest.d.mts +1 -1
- package/lib/core/utils/handleRequest.d.ts +1 -1
- package/lib/core/utils/internal/attachSiblingHandlers.d.mts +1 -1
- package/lib/core/utils/internal/attachSiblingHandlers.d.ts +1 -1
- package/lib/core/utils/internal/attachSiblingHandlers.js +1 -1
- package/lib/core/utils/internal/attachSiblingHandlers.js.map +1 -1
- package/lib/core/utils/internal/attachSiblingHandlers.mjs +1 -1
- package/lib/core/utils/internal/attachSiblingHandlers.mjs.map +1 -1
- package/lib/core/utils/internal/isHandlerKind.d.mts +1 -1
- package/lib/core/utils/internal/isHandlerKind.d.ts +1 -1
- package/lib/core/utils/internal/observe-response-body-stream.d.mts +18 -0
- package/lib/core/utils/internal/observe-response-body-stream.d.ts +18 -0
- package/lib/core/utils/internal/observe-response-body-stream.js +64 -0
- package/lib/core/utils/internal/observe-response-body-stream.js.map +1 -0
- package/lib/core/utils/internal/observe-response-body-stream.mjs +44 -0
- package/lib/core/utils/internal/observe-response-body-stream.mjs.map +1 -0
- package/lib/core/utils/internal/parseGraphQLRequest.d.mts +1 -1
- package/lib/core/utils/internal/parseGraphQLRequest.d.ts +1 -1
- package/lib/core/utils/internal/parseMultipartData.d.mts +1 -1
- package/lib/core/utils/internal/parseMultipartData.d.ts +1 -1
- package/lib/core/ws/WebSocketClientManager.js +4 -0
- package/lib/core/ws/WebSocketClientManager.js.map +1 -1
- package/lib/core/ws/WebSocketClientManager.mjs +4 -0
- package/lib/core/ws/WebSocketClientManager.mjs.map +1 -1
- package/lib/core/ws/handleWebSocketEvent.d.mts +1 -1
- package/lib/core/ws/handleWebSocketEvent.d.ts +1 -1
- package/lib/iife/index.js +4668 -1105
- package/lib/iife/index.js.map +1 -1
- package/lib/mockServiceWorker.js +21 -9
- package/package.json +4 -5
- package/src/core/handlers/HttpHandler.test.ts +89 -0
- package/src/core/handlers/RequestHandler.ts +142 -32
- package/src/core/sse.ts +100 -57
- package/src/core/utils/HttpResponse/decorators.ts +23 -2
- package/src/core/utils/internal/observe-response-body-stream.test.ts +64 -0
- package/src/core/utils/internal/observe-response-body-stream.ts +77 -0
- package/src/mockServiceWorker.js +19 -7
package/lib/mockServiceWorker.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* - Please do NOT modify this file.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const PACKAGE_VERSION = '2.
|
|
11
|
-
const INTEGRITY_CHECKSUM = '
|
|
10
|
+
const PACKAGE_VERSION = '2.15.0'
|
|
11
|
+
const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e'
|
|
12
12
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
|
13
13
|
const activeClientIds = new Set()
|
|
14
14
|
|
|
@@ -137,8 +137,18 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
|
|
137
137
|
if (client && activeClientIds.has(client.id)) {
|
|
138
138
|
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
|
139
139
|
|
|
140
|
+
// Omit the body of server-sent event stream responses.
|
|
141
|
+
// Cloning such responses would prevent client-side stream cancelations
|
|
142
|
+
// from reaching the original stream (a teed stream only cancels its
|
|
143
|
+
// source once both of its branches cancel) and would buffer the
|
|
144
|
+
// entire stream into the unconsumed clone indefinitely.
|
|
145
|
+
const isEventStreamResponse = response.headers
|
|
146
|
+
.get('content-type')
|
|
147
|
+
?.toLowerCase()
|
|
148
|
+
.startsWith('text/event-stream')
|
|
149
|
+
|
|
140
150
|
// Clone the response so both the client and the library could consume it.
|
|
141
|
-
const responseClone = response.clone()
|
|
151
|
+
const responseClone = isEventStreamResponse ? null : response.clone()
|
|
142
152
|
|
|
143
153
|
sendToClient(
|
|
144
154
|
client,
|
|
@@ -151,15 +161,17 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
|
|
151
161
|
...serializedRequest,
|
|
152
162
|
},
|
|
153
163
|
response: {
|
|
154
|
-
type:
|
|
155
|
-
status:
|
|
156
|
-
statusText:
|
|
157
|
-
headers: Object.fromEntries(
|
|
158
|
-
body: responseClone.body,
|
|
164
|
+
type: response.type,
|
|
165
|
+
status: response.status,
|
|
166
|
+
statusText: response.statusText,
|
|
167
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
168
|
+
body: responseClone ? responseClone.body : null,
|
|
159
169
|
},
|
|
160
170
|
},
|
|
161
171
|
},
|
|
162
|
-
responseClone
|
|
172
|
+
responseClone && responseClone.body
|
|
173
|
+
? [serializedRequest.body, responseClone.body]
|
|
174
|
+
: [],
|
|
163
175
|
)
|
|
164
176
|
}
|
|
165
177
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "msw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "Seamless REST/GraphQL API mocking library for browser and Node.js.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./lib/core/index.js",
|
|
@@ -248,7 +248,7 @@
|
|
|
248
248
|
"@fastify/websocket": "^11.2.0",
|
|
249
249
|
"@graphql-typed-document-node/core": "^3.2.0",
|
|
250
250
|
"@open-draft/test-server": "^0.4.2",
|
|
251
|
-
"@ossjs/release": "^0.
|
|
251
|
+
"@ossjs/release": "^0.11.1",
|
|
252
252
|
"@playwright/test": "^1.59.1",
|
|
253
253
|
"@types/express": "^5.0.6",
|
|
254
254
|
"@types/json-bigint": "^1.0.4",
|
|
@@ -280,13 +280,12 @@
|
|
|
280
280
|
"simple-git-hooks": "^2.13.1",
|
|
281
281
|
"tsup": "^8.5.1",
|
|
282
282
|
"typescript": "^5.9.3",
|
|
283
|
-
"undici": "^
|
|
283
|
+
"undici": "^7.28.0",
|
|
284
284
|
"url-loader": "^4.1.1",
|
|
285
285
|
"vitest": "^4.1.4",
|
|
286
|
-
"vitest-environment-miniflare": "^2.14.4",
|
|
287
286
|
"webpack": "^5.106.2",
|
|
288
287
|
"webpack-http-server": "^0.5.0",
|
|
289
|
-
"msw": "^2.
|
|
288
|
+
"msw": "^2.15.0"
|
|
290
289
|
},
|
|
291
290
|
"peerDependencies": {
|
|
292
291
|
"typescript": ">= 4.8.x"
|
|
@@ -252,3 +252,92 @@ describe('run', () => {
|
|
|
252
252
|
await expect(run()).resolves.toBe('complete')
|
|
253
253
|
})
|
|
254
254
|
})
|
|
255
|
+
|
|
256
|
+
describe('finalize', () => {
|
|
257
|
+
it('returns the exact response instance if the resolver never accesses finalize', async () => {
|
|
258
|
+
const response = new HttpResponse(
|
|
259
|
+
new ReadableStream({
|
|
260
|
+
start(controller) {
|
|
261
|
+
controller.enqueue(new TextEncoder().encode('hello'))
|
|
262
|
+
controller.close()
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
)
|
|
266
|
+
const handler = new HttpHandler('GET', '/resource', () => {
|
|
267
|
+
return response
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const result = await handler.run({
|
|
271
|
+
request: new Request(new URL('/resource', location.href)),
|
|
272
|
+
requestId: createRequestId(),
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
expect(result?.response).toBe(response)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('returns the exact response instance if finalize is accessed but never called', async () => {
|
|
279
|
+
const response = new HttpResponse(
|
|
280
|
+
new ReadableStream({
|
|
281
|
+
start(controller) {
|
|
282
|
+
controller.close()
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
)
|
|
286
|
+
const handler = new HttpHandler('GET', '/resource', ({ finalize }) => {
|
|
287
|
+
expect(finalize).toBeInstanceOf(Function)
|
|
288
|
+
return response
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const result = await handler.run({
|
|
292
|
+
request: new Request(new URL('/resource', location.href)),
|
|
293
|
+
requestId: createRequestId(),
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
expect(result?.response).toBe(response)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('defers the cleanup until the response stream settles', async () => {
|
|
300
|
+
const cleanup = vi.fn()
|
|
301
|
+
const response = new HttpResponse(
|
|
302
|
+
new ReadableStream({
|
|
303
|
+
start(controller) {
|
|
304
|
+
controller.enqueue(new TextEncoder().encode('hello'))
|
|
305
|
+
controller.close()
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
)
|
|
309
|
+
const handler = new HttpHandler('GET', '/resource', ({ finalize }) => {
|
|
310
|
+
finalize(cleanup)
|
|
311
|
+
return response
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const result = await handler.run({
|
|
315
|
+
request: new Request(new URL('/resource', location.href)),
|
|
316
|
+
requestId: createRequestId(),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// The observed response is a copy of the original response.
|
|
320
|
+
expect(result?.response).not.toBe(response)
|
|
321
|
+
expect(cleanup).not.toHaveBeenCalled()
|
|
322
|
+
|
|
323
|
+
await expect(result?.response?.text()).resolves.toBe('hello')
|
|
324
|
+
await vi.waitFor(() => {
|
|
325
|
+
expect(cleanup).toHaveBeenCalledOnce()
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('runs the cleanup immediately for responses without a body', async () => {
|
|
330
|
+
const cleanup = vi.fn()
|
|
331
|
+
const handler = new HttpHandler('GET', '/resource', ({ finalize }) => {
|
|
332
|
+
finalize(cleanup)
|
|
333
|
+
return new HttpResponse(null)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
await handler.run({
|
|
337
|
+
request: new Request(new URL('/resource', location.href)),
|
|
338
|
+
requestId: createRequestId(),
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
expect(cleanup).toHaveBeenCalledOnce()
|
|
342
|
+
})
|
|
343
|
+
})
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import type { GraphQLRequestBody } from './GraphQLHandler'
|
|
16
16
|
import { devUtils } from '../utils/internal/devUtils'
|
|
17
17
|
import { getRawSetCookie } from '../utils/HttpResponse/decorators'
|
|
18
|
+
import { observeResponseBodyStream } from '../utils/internal/observe-response-body-stream'
|
|
18
19
|
|
|
19
20
|
export type DefaultRequestMultipartBody = Record<
|
|
20
21
|
string,
|
|
@@ -31,12 +32,7 @@ export type DefaultBodyType =
|
|
|
31
32
|
| undefined
|
|
32
33
|
|
|
33
34
|
export type JsonBodyType =
|
|
34
|
-
|
|
35
|
-
| string
|
|
36
|
-
| number
|
|
37
|
-
| boolean
|
|
38
|
-
| null
|
|
39
|
-
| undefined
|
|
35
|
+
Record<string, any> | string | number | boolean | null | undefined
|
|
40
36
|
|
|
41
37
|
export interface RequestHandlerDefaultInfo {
|
|
42
38
|
header: string
|
|
@@ -94,6 +90,10 @@ export type ResponseResolverInfo<
|
|
|
94
90
|
/**
|
|
95
91
|
* Schedule a callback to run after this response resolver completes.
|
|
96
92
|
* Handy for cleaning up the side effects introduced in the resolver.
|
|
93
|
+
*
|
|
94
|
+
* For responses with a `ReadableStream` body (including `sse()` handlers),
|
|
95
|
+
* the callback runs once the response stream settles: it is read to
|
|
96
|
+
* completion, errored, or canceled, or the request is aborted.
|
|
97
97
|
* @example
|
|
98
98
|
* sse('/', ({ client, finalize }) => {
|
|
99
99
|
* const interval = setInterval(() => client.send({ data: 'ping' }))
|
|
@@ -355,20 +355,54 @@ export abstract class RequestHandler<
|
|
|
355
355
|
|
|
356
356
|
const listenerController = new AbortController()
|
|
357
357
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
358
|
+
/**
|
|
359
|
+
* @note Initialize the `finalize` machinery lazily, on the first
|
|
360
|
+
* access of the `finalize` property by the resolver. If the resolver
|
|
361
|
+
* never accesses it, the handler behaves as if `finalize` never
|
|
362
|
+
* existed: no abort listeners, no scheduled cleanups, and no response
|
|
363
|
+
* body stream observation (see `this.complete()`).
|
|
364
|
+
*/
|
|
365
|
+
let finalizeFunction: ResponseResolverFinalizeFunction | undefined
|
|
366
|
+
|
|
367
|
+
const getFinalize = (): ResponseResolverFinalizeFunction => {
|
|
368
|
+
if (finalizeFunction == null) {
|
|
369
|
+
// Run any scheduled cleanups if the request gets aborted
|
|
370
|
+
// while the resolver is still executing.
|
|
371
|
+
if (!args.request.signal.aborted) {
|
|
372
|
+
args.request.signal.addEventListener(
|
|
373
|
+
'abort',
|
|
374
|
+
() => this.runScheduledCleanups(args.requestId),
|
|
375
|
+
{
|
|
376
|
+
once: true,
|
|
377
|
+
signal: listenerController.signal,
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
finalizeFunction = (callback) => {
|
|
383
|
+
this.scheduleCleanup(args.requestId, callback)
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @note Run the cleanup immediately if the request has already
|
|
387
|
+
* been aborted. The "abort" listener above never fires for an
|
|
388
|
+
* already-aborted signal (and fires at most once), while
|
|
389
|
+
* long-lived resolvers (streams, generators) may never settle
|
|
390
|
+
* to run the cleanups on completion.
|
|
391
|
+
*/
|
|
392
|
+
if (args.request.signal.aborted) {
|
|
393
|
+
void this.runScheduledCleanups(args.requestId)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return finalizeFunction
|
|
399
|
+
}
|
|
366
400
|
|
|
367
401
|
const mockedResponsePromise = (
|
|
368
402
|
executeResolver({
|
|
369
403
|
...resolverExtras,
|
|
370
|
-
finalize
|
|
371
|
-
|
|
404
|
+
get finalize(): ResponseResolverFinalizeFunction {
|
|
405
|
+
return getFinalize()
|
|
372
406
|
},
|
|
373
407
|
requestId: args.requestId,
|
|
374
408
|
request: args.request,
|
|
@@ -423,9 +457,13 @@ export abstract class RequestHandler<
|
|
|
423
457
|
}
|
|
424
458
|
|
|
425
459
|
if (!isIterable(result)) {
|
|
426
|
-
// Otherwise, run the
|
|
427
|
-
|
|
428
|
-
return
|
|
460
|
+
// Otherwise, run the cleanups if it's a plain response resolver
|
|
461
|
+
// (deferred until the response stream settles for streamed responses).
|
|
462
|
+
return this.complete({
|
|
463
|
+
request: info.request,
|
|
464
|
+
requestId: info.requestId,
|
|
465
|
+
response: result,
|
|
466
|
+
})
|
|
429
467
|
}
|
|
430
468
|
|
|
431
469
|
/**
|
|
@@ -465,11 +503,13 @@ export abstract class RequestHandler<
|
|
|
465
503
|
// only after it's been completely exhausted.
|
|
466
504
|
this.isUsed = true
|
|
467
505
|
|
|
468
|
-
await this.runScheduledCleanups(info.requestId)
|
|
469
|
-
|
|
470
506
|
// Clone the previously stored response so it can be read
|
|
471
507
|
// when receiving it repeatedly from the "done" generator.
|
|
472
|
-
return this.
|
|
508
|
+
return this.complete({
|
|
509
|
+
request: info.request,
|
|
510
|
+
requestId: info.requestId,
|
|
511
|
+
response: this.resolverIteratorResult?.clone(),
|
|
512
|
+
})
|
|
473
513
|
}
|
|
474
514
|
|
|
475
515
|
return nextResponse
|
|
@@ -532,28 +572,98 @@ export abstract class RequestHandler<
|
|
|
532
572
|
}
|
|
533
573
|
}
|
|
534
574
|
|
|
535
|
-
|
|
575
|
+
/**
|
|
576
|
+
* Remove and return the cleanups scheduled for the given request
|
|
577
|
+
* (or the pending iterator cleanups for generator resolvers).
|
|
578
|
+
*/
|
|
579
|
+
private takeScheduledCleanups(
|
|
580
|
+
requestId: string,
|
|
581
|
+
): Array<() => MaybePromise<void>> | undefined {
|
|
536
582
|
if (
|
|
537
583
|
this.resolverIterator &&
|
|
538
584
|
this.resolverIteratorCleanups != null &&
|
|
539
585
|
this.resolverIteratorCleanups.length > 0
|
|
540
586
|
) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
this.resolverIteratorCleanups = undefined
|
|
545
|
-
}
|
|
546
|
-
return
|
|
587
|
+
const cleanups = this.resolverIteratorCleanups
|
|
588
|
+
this.resolverIteratorCleanups = undefined
|
|
589
|
+
return cleanups
|
|
547
590
|
}
|
|
548
591
|
|
|
549
592
|
const cleanups = this.scheduledCleanups.get(requestId)
|
|
550
593
|
|
|
551
|
-
if (!cleanups || cleanups.length
|
|
552
|
-
return
|
|
594
|
+
if (!cleanups || cleanups.length === 0) {
|
|
595
|
+
return undefined
|
|
553
596
|
}
|
|
554
597
|
|
|
555
|
-
await this.exhaustCleanups(cleanups)
|
|
556
598
|
this.scheduledCleanups.delete(requestId)
|
|
599
|
+
return cleanups
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private async runScheduledCleanups(requestId: string): Promise<void> {
|
|
603
|
+
const cleanups = this.takeScheduledCleanups(requestId)
|
|
604
|
+
|
|
605
|
+
if (cleanups) {
|
|
606
|
+
await this.exhaustCleanups(cleanups)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Conclude the response resolution for the given request.
|
|
612
|
+
* Runs the scheduled cleanups immediately for responses without a
|
|
613
|
+
* `ReadableStream` body. For streamed responses, returns an observed
|
|
614
|
+
* copy of the response and defers the cleanups until its body settles
|
|
615
|
+
* (is read to completion, errored, or canceled) or the request is
|
|
616
|
+
* aborted, whichever comes first.
|
|
617
|
+
*/
|
|
618
|
+
private async complete(args: {
|
|
619
|
+
request: StrictRequest<any>
|
|
620
|
+
requestId: string
|
|
621
|
+
response: Response | undefined | void
|
|
622
|
+
}): Promise<Response | undefined | void> {
|
|
623
|
+
const cleanups = this.takeScheduledCleanups(args.requestId)
|
|
624
|
+
|
|
625
|
+
if (!cleanups) {
|
|
626
|
+
return args.response
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const observedResponse = args.response
|
|
630
|
+
? observeResponseBodyStream(args.response)
|
|
631
|
+
: null
|
|
632
|
+
|
|
633
|
+
if (!observedResponse) {
|
|
634
|
+
await this.exhaustCleanups(cleanups)
|
|
635
|
+
return args.response
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const listenerController = new AbortController()
|
|
639
|
+
|
|
640
|
+
const runCleanupsOnce = (): void => {
|
|
641
|
+
if (listenerController.signal.aborted) {
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
listenerController.abort()
|
|
646
|
+
void this.exhaustCleanups(cleanups)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
void observedResponse.settled.then(runCleanupsOnce)
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* @note Also run the cleanups when the request is aborted.
|
|
653
|
+
* Stream cancellation does not always propagate to the observed
|
|
654
|
+
* response (e.g. if an unconsumed clone of the response exists),
|
|
655
|
+
* while the request abort reliably means the response is unused.
|
|
656
|
+
*/
|
|
657
|
+
if (args.request.signal.aborted) {
|
|
658
|
+
runCleanupsOnce()
|
|
659
|
+
} else {
|
|
660
|
+
args.request.signal.addEventListener('abort', runCleanupsOnce, {
|
|
661
|
+
once: true,
|
|
662
|
+
signal: listenerController.signal,
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return observedResponse.response
|
|
557
667
|
}
|
|
558
668
|
}
|
|
559
669
|
|
package/src/core/sse.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { invariant } from 'outvariant'
|
|
2
2
|
import { DeferredPromise } from '@open-draft/deferred-promise'
|
|
3
|
-
import { Emitter } from '
|
|
3
|
+
import { Emitter, TypedEvent } from 'rettime'
|
|
4
4
|
import type { ResponseResolver } from './handlers/RequestHandler'
|
|
5
5
|
import {
|
|
6
6
|
HttpHandler,
|
|
@@ -80,7 +80,7 @@ const SSE_RESPONSE_INIT: ResponseInit = {
|
|
|
80
80
|
class ServerSentEventHandler<
|
|
81
81
|
EventMap extends EventMapConstraint,
|
|
82
82
|
> extends HttpHandler {
|
|
83
|
-
#
|
|
83
|
+
#loggableRequests: WeakSet<Request>
|
|
84
84
|
|
|
85
85
|
constructor(path: Path, resolver: ServerSentEventResolver<EventMap, any>) {
|
|
86
86
|
invariant(
|
|
@@ -94,18 +94,33 @@ class ServerSentEventHandler<
|
|
|
94
94
|
info.request,
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
/**
|
|
98
|
+
* @note Scope the emitter to this connection so its logging
|
|
99
|
+
* listeners don't accumulate on the handler across connections.
|
|
100
|
+
*/
|
|
101
|
+
const emitter = new Emitter<ServerSentEventClientEventMap>()
|
|
102
|
+
client[kClientEmitter] = emitter
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
if (this.#loggableRequests.has(info.request)) {
|
|
105
|
+
this.#attachClientLogger(info.request, emitter)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @note Extend the resolver info via assignment instead of a spread.
|
|
110
|
+
* Spreading the info would invoke its lazy `finalize` getter,
|
|
111
|
+
* initializing the finalize machinery for resolvers that never use it.
|
|
112
|
+
*/
|
|
113
|
+
await resolver(
|
|
114
|
+
Object.assign(info, {
|
|
115
|
+
client,
|
|
116
|
+
server,
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
104
119
|
|
|
105
120
|
return response
|
|
106
121
|
})
|
|
107
122
|
|
|
108
|
-
this.#
|
|
123
|
+
this.#loggableRequests = new WeakSet()
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
async predicate(args: {
|
|
@@ -134,13 +149,22 @@ class ServerSentEventHandler<
|
|
|
134
149
|
response: new Response('[streaming]', SSE_RESPONSE_INIT),
|
|
135
150
|
})
|
|
136
151
|
|
|
137
|
-
|
|
152
|
+
// Defer attaching the client logger until the resolver runs
|
|
153
|
+
// and creates a connection-scoped emitter for this request.
|
|
154
|
+
this.#loggableRequests.add(args.request)
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
return matches
|
|
141
158
|
}
|
|
142
159
|
|
|
143
|
-
async log(
|
|
160
|
+
async log(args: { request: Request; response: Response }): Promise<void> {
|
|
161
|
+
/**
|
|
162
|
+
* @note Cancel the response stream because it's not needed for logging.
|
|
163
|
+
* Otherwise, this cloned response remains unconsumed and its original
|
|
164
|
+
* doesn't propagate stream cancelations at all.
|
|
165
|
+
*/
|
|
166
|
+
args.response.body?.cancel()
|
|
167
|
+
|
|
144
168
|
/**
|
|
145
169
|
* @note Skip the default `this.log()` logic so that when this handler is logged
|
|
146
170
|
* upon handling the request, nothing is printed (we log SSE requests early).
|
|
@@ -155,16 +179,14 @@ class ServerSentEventHandler<
|
|
|
155
179
|
const publicUrl = toPublicUrl(request.url)
|
|
156
180
|
|
|
157
181
|
/* eslint-disable no-console */
|
|
158
|
-
emitter.on('message', (
|
|
182
|
+
emitter.on('message', ({ data }) => {
|
|
159
183
|
console.groupCollapsed(
|
|
160
|
-
devUtils.formatMessage(
|
|
161
|
-
`${getTimestamp()} SSE %s %c⇣%c ${payload.event}`,
|
|
162
|
-
),
|
|
184
|
+
devUtils.formatMessage(`${getTimestamp()} SSE %s %c⇣%c ${data.event}`),
|
|
163
185
|
publicUrl,
|
|
164
186
|
`color:${colors.mocked}`,
|
|
165
187
|
'color:inherit',
|
|
166
188
|
)
|
|
167
|
-
console.log(
|
|
189
|
+
console.log(data.frames)
|
|
168
190
|
console.groupEnd()
|
|
169
191
|
})
|
|
170
192
|
|
|
@@ -216,9 +238,9 @@ type ToEventDiscriminatedUnion<T> = Values<{
|
|
|
216
238
|
}>
|
|
217
239
|
|
|
218
240
|
type ServerSentEventClientEventMap = {
|
|
219
|
-
message:
|
|
220
|
-
error:
|
|
221
|
-
close:
|
|
241
|
+
message: TypedEvent<EventStreamMessage>
|
|
242
|
+
error: TypedEvent
|
|
243
|
+
close: TypedEvent
|
|
222
244
|
}
|
|
223
245
|
|
|
224
246
|
const kClientEmitter = Symbol.for('kClientEmitter')
|
|
@@ -229,11 +251,13 @@ class ServerSentEventClient<
|
|
|
229
251
|
private [kClientEmitter]?: Emitter<ServerSentEventClientEventMap>
|
|
230
252
|
|
|
231
253
|
#encoder: TextEncoder
|
|
232
|
-
#
|
|
254
|
+
#controller: ReadableStreamDefaultController<Uint8Array>
|
|
255
|
+
#closed: DeferredPromise<void>
|
|
233
256
|
|
|
234
|
-
constructor(
|
|
257
|
+
constructor(controller: ReadableStreamDefaultController<Uint8Array>) {
|
|
235
258
|
this.#encoder = new TextEncoder()
|
|
236
|
-
this.#
|
|
259
|
+
this.#controller = controller
|
|
260
|
+
this.#closed = new DeferredPromise()
|
|
237
261
|
}
|
|
238
262
|
|
|
239
263
|
/**
|
|
@@ -289,37 +313,50 @@ class ServerSentEventClient<
|
|
|
289
313
|
* error.
|
|
290
314
|
*/
|
|
291
315
|
public error(): void {
|
|
292
|
-
this.#
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this[kClientEmitter]?.emit('error')
|
|
316
|
+
if (this.#closed.state !== 'pending') {
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this.#controller.error()
|
|
321
|
+
this.#closed.resolve()
|
|
322
|
+
this[kClientEmitter]?.emit(new TypedEvent('error'))
|
|
299
323
|
}
|
|
300
324
|
|
|
301
325
|
/**
|
|
302
326
|
* Closes the underlying `EventSource`, closing the connection.
|
|
303
327
|
*/
|
|
304
328
|
public close(): void {
|
|
305
|
-
this.#
|
|
329
|
+
if (this.#closed.state !== 'pending') {
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
this.#controller.close()
|
|
335
|
+
this.#closed.resolve()
|
|
336
|
+
} catch {
|
|
337
|
+
//
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this[kClientEmitter]?.emit(new TypedEvent('close'))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#enqueue(chunk: Uint8Array): void {
|
|
344
|
+
if (this.#closed.state !== 'pending') {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
this.#controller.enqueue(chunk)
|
|
350
|
+
} catch (error) {
|
|
306
351
|
console.error(error)
|
|
307
352
|
devUtils.error(
|
|
308
|
-
'Failed to
|
|
353
|
+
'Failed to write to server-side EventSource. Please see the original error above.',
|
|
309
354
|
)
|
|
310
|
-
}
|
|
311
|
-
this[kClientEmitter]?.emit('close')
|
|
355
|
+
}
|
|
312
356
|
}
|
|
313
357
|
|
|
314
358
|
#sendRetry(retry: number): void {
|
|
315
|
-
this.#
|
|
316
|
-
.write(this.#encoder.encode(`retry:${retry}\n\n`))
|
|
317
|
-
.catch((error) => {
|
|
318
|
-
console.error(error)
|
|
319
|
-
devUtils.error(
|
|
320
|
-
'Failed to send a retry packet to server-side EventSource. Please see the original error above.',
|
|
321
|
-
)
|
|
322
|
-
})
|
|
359
|
+
this.#enqueue(this.#encoder.encode(`retry:${retry}\n\n`))
|
|
323
360
|
}
|
|
324
361
|
|
|
325
362
|
#sendMessage(message: {
|
|
@@ -350,21 +387,18 @@ class ServerSentEventClient<
|
|
|
350
387
|
|
|
351
388
|
frames.push('', '')
|
|
352
389
|
|
|
353
|
-
this.#
|
|
354
|
-
.write(this.#encoder.encode(frames.join('\n')))
|
|
355
|
-
.catch((error) => {
|
|
356
|
-
console.error(error)
|
|
357
|
-
devUtils.error(
|
|
358
|
-
'Failed to send a message to server-side EventSource. Please see the original error above.',
|
|
359
|
-
)
|
|
360
|
-
})
|
|
390
|
+
this.#enqueue(this.#encoder.encode(frames.join('\n')))
|
|
361
391
|
|
|
362
|
-
this[kClientEmitter]?.emit(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
392
|
+
this[kClientEmitter]?.emit(
|
|
393
|
+
new TypedEvent('message', {
|
|
394
|
+
data: {
|
|
395
|
+
id: message.id,
|
|
396
|
+
event: message.event?.toString() || 'message',
|
|
397
|
+
data: message.data,
|
|
398
|
+
frames,
|
|
399
|
+
},
|
|
400
|
+
}),
|
|
401
|
+
)
|
|
368
402
|
}
|
|
369
403
|
}
|
|
370
404
|
|
|
@@ -991,9 +1025,18 @@ function createEventStream<EventMap extends EventMapConstraint>(
|
|
|
991
1025
|
request.url,
|
|
992
1026
|
)
|
|
993
1027
|
|
|
994
|
-
|
|
1028
|
+
let controller!: ReadableStreamDefaultController<Uint8Array>
|
|
1029
|
+
|
|
1030
|
+
const readable = new ReadableStream<Uint8Array>({
|
|
1031
|
+
start(defaultController) {
|
|
1032
|
+
controller = defaultController
|
|
1033
|
+
},
|
|
1034
|
+
cancel() {
|
|
1035
|
+
client.close()
|
|
1036
|
+
},
|
|
1037
|
+
})
|
|
995
1038
|
|
|
996
|
-
const client = new ServerSentEventClient<EventMap>(
|
|
1039
|
+
const client = new ServerSentEventClient<EventMap>(controller)
|
|
997
1040
|
const server = new ServerSentEventServer({
|
|
998
1041
|
request,
|
|
999
1042
|
client,
|