msw 2.14.7 → 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/lib/browser/index.js +1 -1
- package/lib/browser/index.mjs +1 -1
- package/lib/core/{HttpResponse-BTyqrRB6.d.mts → HttpResponse-BE_Y2Ak9.d.mts} +18 -0
- package/lib/core/{HttpResponse-BXyXQFMJ.d.ts → HttpResponse-DL-P1EeG.d.ts} +18 -0
- package/lib/core/HttpResponse.d.mts +1 -1
- package/lib/core/HttpResponse.d.ts +1 -1
- package/lib/core/experimental/compat.d.mts +1 -1
- package/lib/core/experimental/compat.d.ts +1 -1
- package/lib/core/experimental/define-network.d.mts +1 -1
- package/lib/core/experimental/define-network.d.ts +1 -1
- package/lib/core/experimental/frames/http-frame.d.mts +1 -1
- package/lib/core/experimental/frames/http-frame.d.ts +1 -1
- package/lib/core/experimental/frames/network-frame.d.mts +1 -1
- package/lib/core/experimental/frames/network-frame.d.ts +1 -1
- package/lib/core/experimental/frames/websocket-frame.d.mts +1 -1
- package/lib/core/experimental/frames/websocket-frame.d.ts +1 -1
- 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 +1 -1
- package/lib/core/experimental/index.d.ts +1 -1
- package/lib/core/experimental/on-unhandled-frame.d.mts +1 -1
- package/lib/core/experimental/on-unhandled-frame.d.ts +1 -1
- 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 +1 -1
- package/lib/core/experimental/sources/interceptor-source.d.ts +1 -1
- package/lib/core/experimental/sources/network-source.d.mts +1 -1
- package/lib/core/experimental/sources/network-source.d.ts +1 -1
- 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/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/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 +66 -48
- package/lib/core/sse.js.map +1 -1
- package/lib/core/sse.mjs +65 -47
- 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 +13 -0
- package/lib/core/utils/HttpResponse/decorators.js.map +1 -1
- package/lib/core/utils/HttpResponse/decorators.mjs +13 -0
- 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/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/handleWebSocketEvent.d.mts +1 -1
- package/lib/core/ws/handleWebSocketEvent.d.ts +1 -1
- package/lib/iife/index.js +270 -137
- package/lib/iife/index.js.map +1 -1
- package/lib/mockServiceWorker.js +21 -9
- package/package.json +2 -2
- package/src/core/handlers/HttpHandler.test.ts +89 -0
- package/src/core/handlers/RequestHandler.ts +142 -32
- package/src/core/sse.ts +85 -53
- 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",
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
"vitest": "^4.1.4",
|
|
286
286
|
"webpack": "^5.106.2",
|
|
287
287
|
"webpack-http-server": "^0.5.0",
|
|
288
|
-
"msw": "^2.
|
|
288
|
+
"msw": "^2.15.0"
|
|
289
289
|
},
|
|
290
290
|
"peerDependencies": {
|
|
291
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,
|
|
@@ -105,11 +105,17 @@ class ServerSentEventHandler<
|
|
|
105
105
|
this.#attachClientLogger(info.request, emitter)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
)
|
|
113
119
|
|
|
114
120
|
return response
|
|
115
121
|
})
|
|
@@ -151,7 +157,14 @@ class ServerSentEventHandler<
|
|
|
151
157
|
return matches
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
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
|
+
|
|
155
168
|
/**
|
|
156
169
|
* @note Skip the default `this.log()` logic so that when this handler is logged
|
|
157
170
|
* upon handling the request, nothing is printed (we log SSE requests early).
|
|
@@ -166,16 +179,14 @@ class ServerSentEventHandler<
|
|
|
166
179
|
const publicUrl = toPublicUrl(request.url)
|
|
167
180
|
|
|
168
181
|
/* eslint-disable no-console */
|
|
169
|
-
emitter.on('message', (
|
|
182
|
+
emitter.on('message', ({ data }) => {
|
|
170
183
|
console.groupCollapsed(
|
|
171
|
-
devUtils.formatMessage(
|
|
172
|
-
`${getTimestamp()} SSE %s %c⇣%c ${payload.event}`,
|
|
173
|
-
),
|
|
184
|
+
devUtils.formatMessage(`${getTimestamp()} SSE %s %c⇣%c ${data.event}`),
|
|
174
185
|
publicUrl,
|
|
175
186
|
`color:${colors.mocked}`,
|
|
176
187
|
'color:inherit',
|
|
177
188
|
)
|
|
178
|
-
console.log(
|
|
189
|
+
console.log(data.frames)
|
|
179
190
|
console.groupEnd()
|
|
180
191
|
})
|
|
181
192
|
|
|
@@ -227,9 +238,9 @@ type ToEventDiscriminatedUnion<T> = Values<{
|
|
|
227
238
|
}>
|
|
228
239
|
|
|
229
240
|
type ServerSentEventClientEventMap = {
|
|
230
|
-
message:
|
|
231
|
-
error:
|
|
232
|
-
close:
|
|
241
|
+
message: TypedEvent<EventStreamMessage>
|
|
242
|
+
error: TypedEvent
|
|
243
|
+
close: TypedEvent
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
const kClientEmitter = Symbol.for('kClientEmitter')
|
|
@@ -240,11 +251,13 @@ class ServerSentEventClient<
|
|
|
240
251
|
private [kClientEmitter]?: Emitter<ServerSentEventClientEventMap>
|
|
241
252
|
|
|
242
253
|
#encoder: TextEncoder
|
|
243
|
-
#
|
|
254
|
+
#controller: ReadableStreamDefaultController<Uint8Array>
|
|
255
|
+
#closed: DeferredPromise<void>
|
|
244
256
|
|
|
245
|
-
constructor(
|
|
257
|
+
constructor(controller: ReadableStreamDefaultController<Uint8Array>) {
|
|
246
258
|
this.#encoder = new TextEncoder()
|
|
247
|
-
this.#
|
|
259
|
+
this.#controller = controller
|
|
260
|
+
this.#closed = new DeferredPromise()
|
|
248
261
|
}
|
|
249
262
|
|
|
250
263
|
/**
|
|
@@ -300,37 +313,50 @@ class ServerSentEventClient<
|
|
|
300
313
|
* error.
|
|
301
314
|
*/
|
|
302
315
|
public error(): void {
|
|
303
|
-
this.#
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
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'))
|
|
310
323
|
}
|
|
311
324
|
|
|
312
325
|
/**
|
|
313
326
|
* Closes the underlying `EventSource`, closing the connection.
|
|
314
327
|
*/
|
|
315
328
|
public close(): void {
|
|
316
|
-
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) {
|
|
317
351
|
console.error(error)
|
|
318
352
|
devUtils.error(
|
|
319
|
-
'Failed to
|
|
353
|
+
'Failed to write to server-side EventSource. Please see the original error above.',
|
|
320
354
|
)
|
|
321
|
-
}
|
|
322
|
-
this[kClientEmitter]?.emit('close')
|
|
355
|
+
}
|
|
323
356
|
}
|
|
324
357
|
|
|
325
358
|
#sendRetry(retry: number): void {
|
|
326
|
-
this.#
|
|
327
|
-
.write(this.#encoder.encode(`retry:${retry}\n\n`))
|
|
328
|
-
.catch((error) => {
|
|
329
|
-
console.error(error)
|
|
330
|
-
devUtils.error(
|
|
331
|
-
'Failed to send a retry packet to server-side EventSource. Please see the original error above.',
|
|
332
|
-
)
|
|
333
|
-
})
|
|
359
|
+
this.#enqueue(this.#encoder.encode(`retry:${retry}\n\n`))
|
|
334
360
|
}
|
|
335
361
|
|
|
336
362
|
#sendMessage(message: {
|
|
@@ -361,21 +387,18 @@ class ServerSentEventClient<
|
|
|
361
387
|
|
|
362
388
|
frames.push('', '')
|
|
363
389
|
|
|
364
|
-
this.#
|
|
365
|
-
.write(this.#encoder.encode(frames.join('\n')))
|
|
366
|
-
.catch((error) => {
|
|
367
|
-
console.error(error)
|
|
368
|
-
devUtils.error(
|
|
369
|
-
'Failed to send a message to server-side EventSource. Please see the original error above.',
|
|
370
|
-
)
|
|
371
|
-
})
|
|
390
|
+
this.#enqueue(this.#encoder.encode(frames.join('\n')))
|
|
372
391
|
|
|
373
|
-
this[kClientEmitter]?.emit(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
)
|
|
379
402
|
}
|
|
380
403
|
}
|
|
381
404
|
|
|
@@ -1002,9 +1025,18 @@ function createEventStream<EventMap extends EventMapConstraint>(
|
|
|
1002
1025
|
request.url,
|
|
1003
1026
|
)
|
|
1004
1027
|
|
|
1005
|
-
|
|
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
|
+
})
|
|
1006
1038
|
|
|
1007
|
-
const client = new ServerSentEventClient<EventMap>(
|
|
1039
|
+
const client = new ServerSentEventClient<EventMap>(controller)
|
|
1008
1040
|
const server = new ServerSentEventServer({
|
|
1009
1041
|
request,
|
|
1010
1042
|
client,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import statuses from '../../../shims/statuses'
|
|
2
|
-
import type { HttpResponseInit } from '../../HttpResponse'
|
|
2
|
+
import type { HttpResponse, HttpResponseInit } from '../../HttpResponse'
|
|
3
3
|
|
|
4
4
|
const { message } = statuses
|
|
5
5
|
|
|
@@ -27,7 +27,7 @@ export function normalizeResponseInit(
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function decorateResponse(
|
|
30
|
-
response:
|
|
30
|
+
response: HttpResponse<any>,
|
|
31
31
|
init: HttpResponseDecoratedInit,
|
|
32
32
|
): Response {
|
|
33
33
|
// Allow mocking the response type.
|
|
@@ -59,3 +59,24 @@ export function decorateResponse(
|
|
|
59
59
|
export function getRawSetCookie(response: Response): string | undefined {
|
|
60
60
|
return Reflect.get(response, kSetCookie)
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Copy the given response own properties, like internal symbols,
|
|
65
|
+
* onto another response. Used for faithful internal copying of responses.
|
|
66
|
+
*/
|
|
67
|
+
export function copyResponseOwnProperties(
|
|
68
|
+
source: Response,
|
|
69
|
+
target: Response,
|
|
70
|
+
): void {
|
|
71
|
+
for (const propertyName of Reflect.ownKeys(source)) {
|
|
72
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, propertyName)
|
|
73
|
+
const existingDescriptor = Object.getOwnPropertyDescriptor(
|
|
74
|
+
target,
|
|
75
|
+
propertyName,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if (descriptor && existingDescriptor == null) {
|
|
79
|
+
Object.defineProperty(target, propertyName, descriptor)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|