11import type { Transform } from "stream" ;
22import {
3+ ReadableByteStreamController ,
34 ReadableStream ,
45 ReadableStreamBYOBReadResult ,
56 ReadableStreamBYOBReader ,
@@ -15,6 +16,57 @@ import {
1516 isBufferSource ,
1617} from "./helpers" ;
1718
19+ /** @internal */
20+ export function _isByteStream (
21+ stream : ReadableStream
22+ ) : stream is ReadableStream < Uint8Array > {
23+ // Try to determine if stream is a byte stream by inspecting its state.
24+ // It doesn't matter too much if the internal representation changes in the
25+ // future: this code shouldn't throw. Currently we only use this as an
26+ // optimisation to avoid creating a byte stream if it's already one.
27+ for ( const symbol of Object . getOwnPropertySymbols ( stream ) ) {
28+ if ( symbol . description === "kState" ) {
29+ // @ts -expect-error symbol properties are not included in type definitions
30+ const controller = stream [ symbol ] . controller ;
31+ return controller instanceof ReadableByteStreamController ;
32+ }
33+ }
34+ return false ;
35+ }
36+
37+ function convertToByteStream (
38+ stream : ReadableStream < Uint8Array > ,
39+ clone = false
40+ ) {
41+ let reader : ReadableStreamDefaultReader < Uint8Array > ;
42+ return new ReadableStream ( {
43+ type : "bytes" ,
44+ start ( ) {
45+ reader = stream . getReader ( ) ;
46+ } ,
47+ async pull ( controller ) {
48+ let result = await reader . read ( ) ;
49+ while ( ! result . done && result . value . byteLength === 0 ) {
50+ result = await reader . read ( ) ;
51+ }
52+ if ( result . done ) {
53+ queueMicrotask ( ( ) => {
54+ controller . close ( ) ;
55+ // Not documented in MDN but if there's an ongoing request that's
56+ // waiting, we need to tell it that there were 0 bytes delivered so
57+ // that it unblocks and notices the end of stream.
58+ // @ts -expect-error `byobRequest` has type `undefined` in `@types/node`
59+ controller . byobRequest ?. respond ( 0 ) ;
60+ } ) ;
61+ } else if ( result . value . byteLength > 0 ) {
62+ if ( clone ) result . value = result . value . slice ( ) ;
63+ controller . enqueue ( result . value ) ;
64+ }
65+ } ,
66+ cancel : ( reason ) => reader . cancel ( reason ) ,
67+ } ) ;
68+ }
69+
1870export type ArrayBufferViewConstructor =
1971 | typeof Int8Array
2072 | typeof Uint8Array
@@ -83,6 +135,28 @@ ReadableStreamBYOBReader.prototype.readAtLeast = async function <
83135 return { value : value as any , done } ;
84136} ;
85137
138+ // See comment above about manipulating the prototype.
139+ // Rewrite tee() to return byte streams when tee()ing byte streams:
140+ // https://github.com/cloudflare/miniflare/issues/317
141+ const originalTee = ReadableStream . prototype . tee ;
142+ ReadableStream . prototype . tee = function ( ) {
143+ if ( ! ( this instanceof ReadableStream ) ) {
144+ throw new TypeError ( "Illegal invocation" ) ;
145+ }
146+ if ( _isByteStream ( this ) ) {
147+ const [ stream1 , stream2 ] = originalTee . call ( this ) ;
148+ // We need to clone chunks here, as either of the tee()ed streams might be
149+ // passed to `new Response()`, which will detach array buffers when reading:
150+ // https://github.com/cloudflare/miniflare/issues/375
151+ return [
152+ convertToByteStream ( stream1 , true /* clone */ ) ,
153+ convertToByteStream ( stream2 , true /* clone */ ) ,
154+ ] ;
155+ } else {
156+ return originalTee . call ( this ) ;
157+ }
158+ } ;
159+
86160const kTransformHook = Symbol ( "kTransformHook" ) ;
87161const kFlushHook = Symbol ( "kFlushHook" ) ;
88162
@@ -119,41 +193,7 @@ export class IdentityTransformStream extends TransformStream<
119193
120194 get readable ( ) {
121195 if ( this . #readableByteStream !== undefined ) return this . #readableByteStream;
122- const readable = super . readable ;
123- let reader : ReadableStreamDefaultReader ;
124- return ( this . #readableByteStream = new ReadableStream ( {
125- type : "bytes" ,
126- start ( ) {
127- reader = readable . getReader ( ) ;
128- } ,
129- async pull ( controller ) {
130- let { done, value } = await reader . read ( ) ;
131- // Make sure we eventually call a `controller` method, either because
132- // we're done, or there's data to enqueue
133- while ( ! done && value . byteLength === 0 ) {
134- const result = await reader . read ( ) ;
135- done = result . done ;
136- value = result . value ;
137- }
138- if ( done ) {
139- queueMicrotask ( ( ) => {
140- controller . close ( ) ;
141- // Not documented in MDN but if there's an ongoing request that's waiting,
142- // we need to tell it that there were 0 bytes delivered so that it unblocks
143- // and notices the end of stream.
144- // @ts -expect-error `byobRequest` has type `undefined` in `@types/node`
145- controller . byobRequest ?. respond ( 0 ) ;
146- } ) ;
147- } else if ( value . byteLength > 0 ) {
148- // Ensure chunk is non-empty before enqueuing:
149- // https://github.com/cloudflare/miniflare/issues/374
150- controller . enqueue ( value ) ;
151- }
152- } ,
153- cancel ( ) {
154- return reader . cancel ( ) ;
155- } ,
156- } ) ) ;
196+ return ( this . #readableByteStream = convertToByteStream ( super . readable ) ) ;
157197 }
158198}
159199
0 commit comments