@@ -17,7 +17,9 @@ import type {
1717 CassetteEntry ,
1818 CassetteMode ,
1919 RecordedRequest ,
20+ RecordedResponse ,
2021} from "./cassette" ;
22+ import { trackConnectRequest } from "./internal/connect-request" ;
2123import { AggregateCassetteMissError , CassetteMissError } from "./errors" ;
2224import { applyFilters } from "./normalizer" ;
2325import type { FilterSpec } from "./normalizer" ;
@@ -46,7 +48,11 @@ export type RequestUrlMatcher = string | RegExp;
4648export type IgnoredRequestMatcher = RequestUrlMatcher ;
4749export type StreamingRequestMatcher =
4850 | RequestUrlMatcher
49- | { url : RequestUrlMatcher ; httpVersion : "2" } ;
51+ | {
52+ url : RequestUrlMatcher ;
53+ httpVersion ?: "1.1" | "2" ;
54+ requestFraming ?: "connect" ;
55+ } ;
5056
5157// ---- Internal draft types ------------------------------------------------
5258
@@ -126,18 +132,32 @@ async function handleReplayWithoutRequestBody(
126132 req : IncomingMessage ,
127133 res : ServerResponse ,
128134 upstreamUrl : string ,
135+ requestFraming ?: "connect" ,
129136) : Promise < void > {
130137 // Some duplex APIs keep the request body open while waiting for response
131138 // messages. For opted-in URLs, replay can respond immediately because the
132139 // cassette match is configured to ignore the request body.
140+ const tracker =
141+ requestFraming === "connect" ? trackConnectRequest ( req ) : undefined ;
142+ const onClose = ( ) => tracker ?. dispose ( ) ;
143+ res . on ( "close" , onClose ) ;
133144 req . resume ( ) ;
134- const response = await handleReplay ( ctx , {
135- method : req . method ?? "GET" ,
136- url : upstreamUrl ,
137- headers : headersToRecord ( req . headers ) ,
138- body : { kind : "empty" } ,
139- } ) ;
140- await writeFetchResponse ( res , response ) ;
145+ try {
146+ const response = await handleReplay (
147+ ctx ,
148+ {
149+ method : req . method ?? "GET" ,
150+ url : upstreamUrl ,
151+ headers : headersToRecord ( req . headers ) ,
152+ body : { kind : "empty" } ,
153+ } ,
154+ tracker ?. waitFor ,
155+ ) ;
156+ await writeFetchResponse ( res , response ) ;
157+ } finally {
158+ res . off ( "close" , onClose ) ;
159+ tracker ?. dispose ( ) ;
160+ }
141161}
142162
143163async function handleStreamingRecord (
@@ -146,10 +166,13 @@ async function handleStreamingRecord(
146166 res : ServerResponse ,
147167 upstreamUrl : string ,
148168 httpVersion : "1.1" | "2" ,
169+ requestFraming ?: "connect" ,
149170) : Promise < void > {
150171 const upstream = new URL ( upstreamUrl ) ;
151172 const responseChunks : Buffer [ ] = [ ] ;
152- const streamChunks : Array < { size : number ; delayMs : number } > = [ ] ;
173+ const streamChunks : NonNullable < RecordedResponse [ "streamChunks" ] > = [ ] ;
174+ const tracker =
175+ requestFraming === "connect" ? trackConnectRequest ( req ) : undefined ;
153176 let lastChunkAt = performance . now ( ) ;
154177 let responseHeaders : IncomingHttpHeaders = { } ;
155178 let responseStatus = 502 ;
@@ -202,6 +225,7 @@ async function handleStreamingRecord(
202225 streamChunks . push ( {
203226 size : bytes . length ,
204227 delayMs : Math . round ( now - lastChunkAt ) ,
228+ ...( tracker ? { requestFrames : tracker . frames } : { } ) ,
205229 } ) ;
206230 lastChunkAt = now ;
207231 responseChunks . push ( bytes ) ;
@@ -239,6 +263,7 @@ async function handleStreamingRecord(
239263 upstreamUrl,
240264 } ) ;
241265 } finally {
266+ tracker ?. dispose ( ) ;
242267 session ?. destroy ( ) ;
243268 }
244269}
@@ -249,7 +274,7 @@ function pushStreamingEntry(
249274 requestHeaders : Record < string , number | string | string [ ] | undefined > ;
250275 requestMethod : string ;
251276 responseBody : Uint8Array ;
252- streamChunks : Array < { size : number ; delayMs : number } > ;
277+ streamChunks : NonNullable < RecordedResponse [ "streamChunks" ] > ;
253278 responseHeaders : Record < string , number | string | string [ ] | undefined > ;
254279 responseStatus : number ;
255280 responseStatusText ?: string ;
@@ -284,6 +309,7 @@ function pushStreamingEntry(
284309function handleReplay (
285310 ctx : CassetteContext ,
286311 recorded : RecordedRequest ,
312+ waitForRequestFrames ?: ( count : number ) => Promise < void > ,
287313) : Promise < Response > {
288314 const filtered = asNormalized ( applyFilters ( recorded , ctx . filters ) ) ;
289315 const matchKey = filtered . matchKey ;
@@ -310,7 +336,11 @@ function handleReplay(
310336 ) ;
311337 }
312338
313- return buildResponse ( match . response , { store : ctx . store , name : ctx . name } ) ;
339+ return buildResponse ( match . response , {
340+ store : ctx . store ,
341+ name : ctx . name ,
342+ waitForRequestFrames,
343+ } ) ;
314344}
315345
316346async function handleRecord (
@@ -575,8 +605,18 @@ export function createCassetteServer(
575605 ) ,
576606 ) ;
577607 if ( streamingRequest ) {
608+ const options =
609+ typeof streamingRequest === "object" && "url" in streamingRequest
610+ ? streamingRequest
611+ : undefined ;
578612 if ( ctx . mode === "replay" ) {
579- await handleReplayWithoutRequestBody ( ctx , req , res , upstreamUrl ) ;
613+ await handleReplayWithoutRequestBody (
614+ ctx ,
615+ req ,
616+ res ,
617+ upstreamUrl ,
618+ options ?. requestFraming ,
619+ ) ;
580620 return ;
581621 }
582622 if ( ctx . mode === "record" ) {
@@ -585,10 +625,8 @@ export function createCassetteServer(
585625 req ,
586626 res ,
587627 upstreamUrl ,
588- typeof streamingRequest === "object" &&
589- "httpVersion" in streamingRequest
590- ? streamingRequest . httpVersion
591- : "1.1" ,
628+ options ?. httpVersion ?? "1.1" ,
629+ options ?. requestFraming ,
592630 ) ;
593631 return ;
594632 }
0 commit comments