@@ -501,6 +501,12 @@ class ReadableStream {
501501 current : undefined ,
502502 } ;
503503 let started = false ;
504+ // A single reusable read request: at most one read is ever in flight
505+ // (next() chains through state.current), and the request is consumed
506+ // before the next read starts, so only its promise record changes
507+ // per read.
508+ // eslint-disable-next-line no-use-before-define
509+ const readRequest = new ReadableStreamAsyncIteratorReadRequest ( reader , state , undefined ) ;
504510
505511 // The nextSteps function is not an async function in order
506512 // to make it more efficient. Because nextSteps explicitly
@@ -519,8 +525,8 @@ class ReadableStream {
519525 }
520526 const promise = PromiseWithResolvers ( ) ;
521527
522- // eslint-disable-next-line no-use-before-define
523- readableStreamDefaultReaderRead ( reader , new ReadableStreamAsyncIteratorReadRequest ( reader , state , promise ) ) ;
528+ readRequest . promise = promise ;
529+ readableStreamDefaultReaderRead ( reader , readRequest ) ;
524530 return promise . promise ;
525531 }
526532
@@ -574,28 +580,38 @@ class ReadableStream {
574580 }
575581 // No read is in flight. Mirror the buffered fast path of
576582 // ReadableStreamDefaultReader.read(): when data is already queued
577- // in a default controller, resolve immediately without allocating
578- // a read request. The result settles synchronously, so leaving
583+ // in the controller, resolve immediately without allocating a
584+ // read request. The result settles synchronously, so leaving
579585 // state.current undefined matches the state the slow path reaches
580586 // once its read request callbacks have settled.
581587 const stream = reader [ kState ] . stream ;
582- if ( ! state . done && stream !== undefined ) {
588+ if ( ! state . done && stream !== undefined &&
589+ stream [ kState ] . state === 'readable' ) {
583590 const controller = stream [ kState ] . controller ;
584- if ( stream [ kState ] . state === 'readable' &&
585- isReadableStreamDefaultController ( controller ) &&
586- controller [ kState ] . queue . length > 0 ) {
587- stream [ kState ] . disturbed = true ;
588- const chunk = dequeueValue ( controller ) ;
589-
590- if ( controller [ kState ] . closeRequested &&
591- ! controller [ kState ] . queue . length ) {
592- readableStreamDefaultControllerClearAlgorithms ( controller ) ;
593- readableStreamClose ( stream ) ;
594- } else {
595- readableStreamDefaultControllerCallPullIfNeeded ( controller ) ;
591+ if ( isReadableStreamDefaultController ( controller ) ) {
592+ if ( controller [ kState ] . queue . length > 0 ) {
593+ stream [ kState ] . disturbed = true ;
594+ const chunk = dequeueValue ( controller ) ;
595+
596+ if ( controller [ kState ] . closeRequested &&
597+ ! controller [ kState ] . queue . length ) {
598+ readableStreamDefaultControllerClearAlgorithms ( controller ) ;
599+ readableStreamClose ( stream ) ;
600+ } else {
601+ readableStreamDefaultControllerCallPullIfNeeded ( controller ) ;
602+ }
603+
604+ return PromiseResolve ( { done : false , value : chunk } ) ;
596605 }
606+ } else if ( controller [ kState ] . queueTotalSize > 0 ) {
607+ // Byte controller with buffered data: same shape as above via
608+ // the queue-filled arm of the byte controller's pull steps.
609+ stream [ kState ] . disturbed = true ;
610+ return PromiseResolve ( {
611+ done : false ,
597612
598- return PromiseResolve ( { done : false , value : chunk } ) ;
613+ value : readableByteStreamControllerDequeueChunk ( controller ) ,
614+ } ) ;
599615 }
600616 }
601617 state . current = nextSteps ( ) ;
@@ -918,24 +934,36 @@ class ReadableStreamDefaultReader {
918934 const stream = this [ kState ] . stream ;
919935 const controller = stream [ kState ] . controller ;
920936
921- // Fast path: if data is already buffered in a default controller,
937+ // Fast path: if data is already buffered in the controller's queue ,
922938 // return a resolved promise immediately without creating a read request.
923939 // This is spec-compliant because read() returns a Promise, and
924940 // Promise.resolve() callbacks still run in the microtask queue.
925- if ( stream [ kState ] . state === 'readable' &&
926- isReadableStreamDefaultController ( controller ) &&
927- controller [ kState ] . queue . length > 0 ) {
928- stream [ kState ] . disturbed = true ;
929- const chunk = dequeueValue ( controller ) ;
941+ if ( stream [ kState ] . state === 'readable' ) {
942+ if ( isReadableStreamDefaultController ( controller ) ) {
943+ if ( controller [ kState ] . queue . length > 0 ) {
944+ stream [ kState ] . disturbed = true ;
945+ const chunk = dequeueValue ( controller ) ;
946+
947+ if ( controller [ kState ] . closeRequested && ! controller [ kState ] . queue . length ) {
948+ readableStreamDefaultControllerClearAlgorithms ( controller ) ;
949+ readableStreamClose ( stream ) ;
950+ } else {
951+ readableStreamDefaultControllerCallPullIfNeeded ( controller ) ;
952+ }
930953
931- if ( controller [ kState ] . closeRequested && ! controller [ kState ] . queue . length ) {
932- readableStreamDefaultControllerClearAlgorithms ( controller ) ;
933- readableStreamClose ( stream ) ;
934- } else {
935- readableStreamDefaultControllerCallPullIfNeeded ( controller ) ;
954+ return PromiseResolve ( { done : false , value : chunk } ) ;
955+ }
956+ } else if ( controller [ kState ] . queueTotalSize > 0 ) {
957+ // Byte controller with buffered data: mirror the queue-filled arm
958+ // of its pull steps (which never consults pendingPullIntos) minus
959+ // the read request.
960+ stream [ kState ] . disturbed = true ;
961+ return PromiseResolve ( {
962+ done : false ,
963+
964+ value : readableByteStreamControllerDequeueChunk ( controller ) ,
965+ } ) ;
936966 }
937-
938- return PromiseResolve ( { done : false , value : chunk } ) ;
939967 }
940968
941969 // Slow path: create request and go through normal flow
@@ -3044,9 +3072,23 @@ function readableByteStreamControllerEnqueue(controller, chunk) {
30443072 }
30453073 }
30463074
3047- if ( readableStreamHasDefaultReader ( stream ) ) {
3048- readableByteStreamControllerProcessReadRequestsUsingQueue ( controller ) ;
3049- if ( ! readableStreamGetNumReadRequests ( stream ) ) {
3075+ // Single consolidated pass over the reader state. The spec routes this
3076+ // through HasDefaultReader / ProcessReadRequestsUsingQueue /
3077+ // GetNumReadRequests / FulfillReadRequest, which would re-run the same
3078+ // reader brand check and re-load the read request list four times on
3079+ // this per-chunk path.
3080+ const { reader } = stream [ kState ] ;
3081+ if ( reader !== undefined &&
3082+ reader [ kState ] !== undefined &&
3083+ reader [ kType ] === 'ReadableStreamDefaultReader' ) {
3084+ const { readRequests } = reader [ kState ] ;
3085+ if ( readRequests . length && controller [ kState ] . queueTotalSize > 0 ) {
3086+ // Only possible when data was enqueued while the stream was not
3087+ // being read; read requests otherwise never coexist with a
3088+ // non-empty queue.
3089+ readableByteStreamControllerProcessReadRequestsUsingQueue ( controller ) ;
3090+ }
3091+ if ( ! readRequests . length ) {
30503092 readableByteStreamControllerEnqueueChunkToQueue (
30513093 controller ,
30523094 transferredBuffer ,
@@ -3060,7 +3102,8 @@ function readableByteStreamControllerEnqueue(controller, chunk) {
30603102 }
30613103 const transferredView =
30623104 new Uint8Array ( transferredBuffer , byteOffset , byteLength ) ;
3063- readableStreamFulfillReadRequest ( stream , transferredView , false ) ;
3105+ const readRequest = ArrayPrototypeShift ( readRequests ) ;
3106+ readRequest [ kChunk ] ( transferredView ) ;
30643107 }
30653108 } else if ( readableStreamHasBYOBReader ( stream ) ) {
30663109 readableByteStreamControllerEnqueueChunkToQueue (
@@ -3395,22 +3438,28 @@ function readableByteStreamControllerCancelSteps(controller, reason) {
33953438 return result ;
33963439}
33973440
3398- function readableByteStreamControllerFillReadRequestFromQueue ( controller , readRequest ) {
3399- const {
3400- queue,
3401- queueTotalSize,
3402- } = controller [ kState ] ;
3403- assert ( queueTotalSize > 0 ) ;
3441+ // Dequeues the first chunk of the byte queue as a Uint8Array view,
3442+ // handling queue drain (close-on-empty or pull) before the view is
3443+ // created. This is the [[queueTotalSize]] > 0 arm of the byte
3444+ // controller's pull steps; it is also called directly from the
3445+ // buffered fast paths in ReadableStreamDefaultReader.read() and the
3446+ // async iterator, which resolve with the view without allocating a
3447+ // read request.
3448+ function readableByteStreamControllerDequeueChunk ( controller ) {
3449+ assert ( controller [ kState ] . queueTotalSize > 0 ) ;
34043450 const {
34053451 buffer,
34063452 byteOffset,
34073453 byteLength,
3408- } = ArrayPrototypeShift ( queue ) ;
3454+ } = ArrayPrototypeShift ( controller [ kState ] . queue ) ;
34093455
34103456 controller [ kState ] . queueTotalSize -= byteLength ;
34113457 readableByteStreamControllerHandleQueueDrain ( controller ) ;
3412- const view = new Uint8Array ( buffer , byteOffset , byteLength ) ;
3413- readRequest [ kChunk ] ( view ) ;
3458+ return new Uint8Array ( buffer , byteOffset , byteLength ) ;
3459+ }
3460+
3461+ function readableByteStreamControllerFillReadRequestFromQueue ( controller , readRequest ) {
3462+ readRequest [ kChunk ] ( readableByteStreamControllerDequeueChunk ( controller ) ) ;
34143463}
34153464
34163465function readableByteStreamControllerProcessReadRequestsUsingQueue ( controller ) {
0 commit comments