@@ -46,10 +46,6 @@ const {
4646 DOMException,
4747} = internalBinding ( 'messaging' ) ;
4848
49- const {
50- markPromiseAsHandled,
51- } = internalBinding ( 'util' ) ;
52-
5349const {
5450 isArrayBufferView,
5551 isDataView,
@@ -139,7 +135,7 @@ const {
139135 writableStreamCloseQueuedOrInFlight,
140136 writableStreamDefaultWriterCloseWithErrorPropagation,
141137 writableStreamDefaultWriterRelease,
142- writableStreamDefaultWriterWrite ,
138+ writableStreamDefaultWriterWriteWithRequest ,
143139 writerClosedPromise,
144140 writerReadyPromise,
145141} = require ( 'internal/webstreams/writablestream' ) ;
@@ -1530,8 +1526,38 @@ function readableStreamPipeTo(
15301526
15311527 const promise = PromiseWithResolvers ( ) ;
15321528
1533- const state = {
1534- currentWrite : PromiseResolve ( ) ,
1529+ // One shared write request tracks every chunk written to the
1530+ // destination, instead of a { promise, resolve, reject } record per
1531+ // write. `stall` is armed by waitForPendingWrites() during shutdown;
1532+ // `failed`/`failure` latch a write that could not proceed.
1533+ const writeTracker = {
1534+ // Non-undefined: queue entries are discriminated from kNilRequest
1535+ // by `promise === undefined`.
1536+ promise : null ,
1537+ pending : 0 ,
1538+ failed : false ,
1539+ failure : undefined ,
1540+ stall : null ,
1541+ resolve ( ) {
1542+ if ( -- this . pending === 0 && this . stall !== null ) {
1543+ const stall = this . stall ;
1544+ this . stall = null ;
1545+ if ( this . failed )
1546+ stall . reject ( this . failure ) ;
1547+ else
1548+ stall . resolve ( ) ;
1549+ }
1550+ } ,
1551+ reject ( error ) {
1552+ this . pending -- ;
1553+ this . failed = true ;
1554+ this . failure = error ;
1555+ if ( this . stall !== null ) {
1556+ const stall = this . stall ;
1557+ this . stall = null ;
1558+ stall . reject ( error ) ;
1559+ }
1560+ } ,
15351561 } ;
15361562
15371563 // The error here can be undefined. The rejected arg
@@ -1548,11 +1574,14 @@ function readableStreamPipeTo(
15481574 promise . resolve ( ) ;
15491575 }
15501576
1551- async function waitForCurrentWrite ( ) {
1552- const write = state . currentWrite ;
1553- await write ;
1554- if ( write !== state . currentWrite )
1555- await waitForCurrentWrite ( ) ;
1577+ function waitForPendingWrites ( ) {
1578+ if ( writeTracker . pending === 0 ) {
1579+ return writeTracker . failed ?
1580+ PromiseReject ( writeTracker . failure ) :
1581+ PromiseResolve ( ) ;
1582+ }
1583+ writeTracker . stall = PromiseWithResolvers ( ) ;
1584+ return writeTracker . stall . promise ;
15561585 }
15571586
15581587 function shutdownWithAnAction ( action , rejected , originalError ) {
@@ -1561,7 +1590,7 @@ function readableStreamPipeTo(
15611590 if ( dest [ kState ] . state === 'writable' &&
15621591 ! writableStreamCloseQueuedOrInFlight ( dest ) ) {
15631592 PromisePrototypeThen (
1564- waitForCurrentWrite ( ) ,
1593+ waitForPendingWrites ( ) ,
15651594 complete ,
15661595 ( error ) => finalize ( true , error ) ) ;
15671596 return ;
@@ -1582,7 +1611,7 @@ function readableStreamPipeTo(
15821611 if ( dest [ kState ] . state === 'writable' &&
15831612 ! writableStreamCloseQueuedOrInFlight ( dest ) ) {
15841613 PromisePrototypeThen (
1585- waitForCurrentWrite ( ) ,
1614+ waitForPendingWrites ( ) ,
15861615 ( ) => finalize ( rejected , error ) ,
15871616 ( error ) => finalize ( true , error ) ) ;
15881617 return ;
@@ -1639,25 +1668,46 @@ function readableStreamPipeTo(
16391668 PromisePrototypeThen ( promise , action , ( ) => { } ) ;
16401669 }
16411670
1642- async function step ( ) {
1643- if ( shuttingDown ) return true ;
1671+ // The pump loop is callback-driven to avoid per-iteration promise
1672+ // allocations. At most one read is in flight at a time, so one read
1673+ // request and one forwarding function are reused for every chunk;
1674+ // the chunk travels through `pendingChunk`.
1675+ let pendingChunk ;
1676+ let readRequest ;
1677+
1678+ // Ready promise rejection is handled by the destination-errored
1679+ // watcher.
1680+ function ignoreReadyRejection ( ) { }
1681+
1682+ function forwardChunk ( ) {
1683+ const chunk = pendingChunk ;
1684+ pendingChunk = undefined ;
1685+ writableStreamDefaultWriterWriteWithRequest ( writer , chunk , writeTracker ) ;
1686+ pump ( ) ;
1687+ }
1688+
1689+ function pump ( ) {
1690+ if ( shuttingDown ) return ;
16441691
16451692 if ( dest [ kState ] . backpressure ) {
1646- await writerReadyPromise ( writer ) . promise ;
1647- if ( shuttingDown ) return true ;
1693+ PromisePrototypeThen (
1694+ writerReadyPromise ( writer ) . promise ,
1695+ pump ,
1696+ ignoreReadyRejection ) ;
1697+ return ;
16481698 }
16491699
16501700 const controller = source [ kState ] . controller ;
16511701
16521702 // Fast path: batch reads when data is buffered in a default controller.
1653- // This avoids creating PipeToReadableStreamReadRequest objects and
1654- // reduces promise allocation overhead.
1703+ // This avoids parking read requests and reduces promise allocation
1704+ // overhead.
16551705 if ( source [ kState ] . state === 'readable' &&
16561706 isReadableStreamDefaultController ( controller ) &&
16571707 controller [ kState ] . queue . length > 0 ) {
16581708
16591709 while ( controller [ kState ] . queue . length > 0 ) {
1660- if ( shuttingDown ) return true ;
1710+ if ( shuttingDown ) return ;
16611711
16621712 const chunk = dequeueValue ( controller ) ;
16631713
@@ -1668,8 +1718,7 @@ function readableStreamPipeTo(
16681718
16691719 // Write the chunk - we're already in a separate microtask from enqueue
16701720 // because we awaited the writer ready promise above.
1671- state . currentWrite = writableStreamDefaultWriterWrite ( writer , chunk ) ;
1672- markPromiseAsHandled ( state . currentWrite ) ;
1721+ writableStreamDefaultWriterWriteWithRequest ( writer , chunk , writeTracker ) ;
16731722
16741723 // Check backpressure after each write
16751724 if ( dest [ kState ] . backpressure ) {
@@ -1686,24 +1735,29 @@ function readableStreamPipeTo(
16861735
16871736 // Check if stream closed during batch
16881737 if ( source [ kState ] . state === 'closed' ) {
1689- return true ;
1738+ return ;
16901739 }
16911740
1692- // Yield to microtask queue between batches to allow events/signals to fire
1693- return false ;
1741+ // Yield to microtask queue between batches to allow events/signals
1742+ // to fire
1743+ queueMicrotask ( pump ) ;
1744+ return ;
16941745 }
16951746
1696- // Slow path: use read request for async reads
1697- const promise = PromiseWithResolvers ( ) ;
1698- // eslint-disable-next-line no-use-before-define
1699- readableStreamDefaultReaderRead ( reader , new PipeToReadableStreamReadRequest ( writer , state , promise ) ) ;
1700-
1701- return promise . promise ;
1702- }
1703-
1704- async function run ( ) {
1705- // Run until step resolves as true
1706- while ( ! await step ( ) ) ;
1747+ // Slow path: park a lazily materialized read request. Close and
1748+ // error are handled by the source watchers.
1749+ readRequest ??= {
1750+ [ kChunk ] ( chunk ) {
1751+ // Per spec, pipeTo must queue a microtask for the write to avoid
1752+ // synchronous write during enqueue(). See WHATWG Streams spec
1753+ // "ReadableStreamPipeTo" step 15's "chunk steps".
1754+ pendingChunk = chunk ;
1755+ queueMicrotask ( forwardChunk ) ;
1756+ } ,
1757+ [ kClose ] ( ) { } ,
1758+ [ kError ] ( ) { } ,
1759+ } ;
1760+ readableStreamDefaultReaderRead ( reader , readRequest ) ;
17071761 }
17081762
17091763 if ( signal !== undefined ) {
@@ -1715,7 +1769,7 @@ function readableStreamPipeTo(
17151769 disposable = addAbortListener ( signal , abortAlgorithm ) ;
17161770 }
17171771
1718- setPromiseHandled ( run ( ) ) ;
1772+ pump ( ) ;
17191773
17201774 watchErrored ( source , readerClosedPromise ( reader ) . promise , ( error ) => {
17211775 if ( ! preventAbort ) {
@@ -1760,33 +1814,6 @@ function readableStreamPipeTo(
17601814 return promise . promise ;
17611815}
17621816
1763- class PipeToReadableStreamReadRequest {
1764- constructor ( writer , state , promise ) {
1765- this . writer = writer ;
1766- this . state = state ;
1767- this . promise = promise ;
1768- }
1769-
1770- [ kChunk ] ( chunk ) {
1771- // Per spec, pipeTo must queue a microtask for the write to avoid
1772- // synchronous write during enqueue(). See WHATWG Streams spec
1773- // "ReadableStreamPipeTo" step 15's "chunk steps".
1774- queueMicrotask ( ( ) => {
1775- this . state . currentWrite = writableStreamDefaultWriterWrite ( this . writer , chunk ) ;
1776- markPromiseAsHandled ( this . state . currentWrite ) ;
1777- this . promise . resolve ( false ) ;
1778- } ) ;
1779- }
1780-
1781- [ kClose ] ( ) {
1782- this . promise . resolve ( true ) ;
1783- }
1784-
1785- [ kError ] ( error ) {
1786- this . promise . reject ( error ) ;
1787- }
1788- }
1789-
17901817function readableStreamTee ( stream , cloneForBranch2 ) {
17911818 if ( isReadableByteStreamController ( stream [ kState ] . controller ) ) {
17921819 return readableByteStreamTee ( stream ) ;
0 commit comments