@@ -35,6 +35,7 @@ const {
3535 Symbol,
3636 SymbolAsyncDispose,
3737 SymbolAsyncIterator,
38+ SymbolFor,
3839 SymbolSpecies,
3940 TypedArrayPrototypeSet,
4041} = primordials ;
@@ -52,6 +53,8 @@ const {
5253} = require ( 'internal/streams/add-abort-signal' ) ;
5354const { eos } = require ( 'internal/streams/end-of-stream' ) ;
5455
56+ const { getOptionValue } = require ( 'internal/options' ) ;
57+
5558let debug = require ( 'internal/util/debuglog' ) . debuglog ( 'stream' , ( fn ) => {
5659 debug = fn ;
5760} ) ;
@@ -82,6 +85,7 @@ const {
8285 ERR_INVALID_ARG_TYPE ,
8386 ERR_METHOD_NOT_IMPLEMENTED ,
8487 ERR_OUT_OF_RANGE ,
88+ ERR_STREAM_ITER_MISSING_FLAG ,
8589 ERR_STREAM_PUSH_AFTER_EOF ,
8690 ERR_STREAM_UNSHIFT_AFTER_END_EVENT ,
8791 ERR_UNKNOWN_ENCODING ,
@@ -1796,3 +1800,144 @@ Readable.wrap = function(src, options) {
17961800 } ,
17971801 } ) . wrap ( src ) ;
17981802} ;
1803+
1804+ // Efficient interop with the stream/iter API via toAsyncStreamable protocol.
1805+ // Provides a batched async iterator that drains the internal buffer into
1806+ // Uint8Array[] batches, avoiding the per-chunk Promise overhead of the
1807+ // standard Symbol.asyncIterator path.
1808+ //
1809+ // The flag cannot be checked at module load time (readable.js loads during
1810+ // bootstrap before options are available). Instead, toAsyncStreamable is
1811+ // always defined but lazily initializes on first call - throwing if the
1812+ // flag is not set, or installing the real implementation if it is.
1813+ {
1814+ const toAsyncStreamable = SymbolFor ( 'Stream.toAsyncStreamable' ) ;
1815+ let kTrustedSource ;
1816+ let normalizeAsyncValue ;
1817+ let isU8 ;
1818+
1819+ // Maximum chunks to drain into a single batch. Bounds peak memory when
1820+ // _read() synchronously pushes many chunks into the buffer.
1821+ const MAX_DRAIN_BATCH = 128 ;
1822+
1823+ function lazyInit ( ) {
1824+ if ( kTrustedSource !== undefined ) return ;
1825+ if ( ! getOptionValue ( '--experimental-stream-iter' ) ) {
1826+ throw new ERR_STREAM_ITER_MISSING_FLAG ( ) ;
1827+ }
1828+ ( { kTrustedSource } = require ( 'internal/streams/iter/types' ) ) ;
1829+ ( { normalizeAsyncValue } = require ( 'internal/streams/iter/from' ) ) ;
1830+ ( { isUint8Array : isU8 } = require ( 'internal/util/types' ) ) ;
1831+ }
1832+
1833+ // Normalize a batch of raw chunks from an object-mode or encoded
1834+ // Readable into Uint8Array values. Returns the normalized batch,
1835+ // or null if normalization produced no output.
1836+ async function normalizeBatch ( raw ) {
1837+ const batch = [ ] ;
1838+ for ( let i = 0 ; i < raw . length ; i ++ ) {
1839+ const value = raw [ i ] ;
1840+ if ( isU8 ( value ) ) {
1841+ batch . push ( value ) ;
1842+ } else {
1843+ // normalizeAsyncValue may await for async protocols (e.g.
1844+ // toAsyncStreamable on yielded objects). Stream events during
1845+ // the suspension are queued, not lost - errors will surface
1846+ // on the next loop iteration after this yield completes.
1847+ for await ( const normalized of normalizeAsyncValue ( value ) ) {
1848+ batch . push ( normalized ) ;
1849+ }
1850+ }
1851+ }
1852+ return batch . length > 0 ? batch : null ;
1853+ }
1854+
1855+ // Batched async iterator for Readable streams. Same mechanism as
1856+ // createAsyncIterator (same event setup, same stream.read() to
1857+ // trigger _read(), same teardown) but drains all currently buffered
1858+ // chunks into a single Uint8Array[] batch per yield, amortizing the
1859+ // Promise/microtask cost across multiple chunks.
1860+ //
1861+ // When normalize is provided (object-mode / encoded streams), each
1862+ // drained batch is passed through it to convert chunks to Uint8Array.
1863+ // When normalize is null (byte-mode), chunks are already Buffers
1864+ // (Uint8Array subclass) and are yielded directly.
1865+ async function * createBatchedAsyncIterator ( stream , normalize ) {
1866+ let callback = nop ;
1867+
1868+ function next ( resolve ) {
1869+ if ( this === stream ) {
1870+ callback ( ) ;
1871+ callback = nop ;
1872+ } else {
1873+ callback = resolve ;
1874+ }
1875+ }
1876+
1877+ stream . on ( 'readable' , next ) ;
1878+
1879+ let error ;
1880+ const cleanup = eos ( stream , { writable : false } , ( err ) => {
1881+ error = err ? aggregateTwoErrors ( error , err ) : null ;
1882+ callback ( ) ;
1883+ callback = nop ;
1884+ } ) ;
1885+
1886+ try {
1887+ while ( true ) {
1888+ const chunk = stream . destroyed ? null : stream . read ( ) ;
1889+ if ( chunk !== null ) {
1890+ // Drain any additional already-buffered chunks into the same
1891+ // batch. The first read() may trigger _read() which
1892+ // synchronously pushes more data into the buffer. We drain
1893+ // that buffered data without issuing unbounded _read() calls -
1894+ // once state.length hits 0 or MAX_DRAIN_BATCH is reached, we
1895+ // stop and yield what we have.
1896+ const batch = [ chunk ] ;
1897+ while ( batch . length < MAX_DRAIN_BATCH &&
1898+ stream . _readableState . length > 0 ) {
1899+ const c = stream . read ( ) ;
1900+ if ( c === null ) break ;
1901+ batch . push ( c ) ;
1902+ }
1903+ if ( normalize !== null ) {
1904+ const result = await normalize ( batch ) ;
1905+ if ( result !== null ) {
1906+ yield result ;
1907+ }
1908+ } else {
1909+ yield batch ;
1910+ }
1911+ } else if ( error ) {
1912+ throw error ;
1913+ } else if ( error === null ) {
1914+ return ;
1915+ } else {
1916+ await new Promise ( next ) ;
1917+ }
1918+ }
1919+ } catch ( err ) {
1920+ error = aggregateTwoErrors ( error , err ) ;
1921+ throw error ;
1922+ } finally {
1923+ if ( error === undefined || stream . _readableState . autoDestroy ) {
1924+ destroyImpl . destroyer ( stream , null ) ;
1925+ } else {
1926+ stream . off ( 'readable' , next ) ;
1927+ cleanup ( ) ;
1928+ }
1929+ }
1930+ }
1931+
1932+ Readable . prototype [ toAsyncStreamable ] = function ( ) {
1933+ lazyInit ( ) ;
1934+ const state = this . _readableState ;
1935+ const normalize = ( state . objectMode || state . encoding ) ?
1936+ normalizeBatch :
1937+ null ;
1938+ const iter = createBatchedAsyncIterator ( this , normalize ) ;
1939+ iter [ kTrustedSource ] = true ;
1940+ iter . stream = this ;
1941+ return iter ;
1942+ } ;
1943+ }
0 commit comments